/* * #%L * BSD implementations of Bio-Formats readers and writers * %% * Copyright (C) 2005 - 2015 Open Microscopy Environment: * - Board of Regents of the University of Wisconsin-Madison * - Glencoe Software, Inc. * - University of Dundee * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * #L% */ package loci.formats; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.lang.ArrayIndexOutOfBoundsException; import loci.common.Constants; import loci.common.Location; import loci.common.RandomAccessInputStream; import loci.common.RandomAccessOutputStream; import loci.common.services.DependencyException; import loci.common.services.ServiceException; import loci.common.services.ServiceFactory; import loci.formats.meta.MetadataRetrieve; import loci.formats.meta.MetadataStore; import loci.formats.services.OMEXMLService; import loci.formats.services.OMEXMLServiceImpl; import org.perf4j.StopWatch; import org.perf4j.slf4j.Slf4JStopWatch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import org.objenesis.strategy.StdInstantiatorStrategy; /** * {@link ReaderWrapper} implementation which caches the state of the * delegate (including and other {@link ReaderWrapper} instances) * after {@link #setId(String)} has been called. * * Initializing a Bio-Formats reader can consume substantial time and memory. * Most of the initialization time is spent in the {@link #setId(String)} call. * Various factors can impact the performance of this step including the file * size, the amount of metadata in the image and also the file format itself. * * With the {@link Memoizer} reader wrapper, if the time required to call the * {@link #setId(String)} method is larger than {@link #minimumElapsed}, the * initialized reader including all reader wrappers will be cached in a memo * file via {@link #saveMemo()}. * Any subsequent call to {@link #setId(String)} with a reader decorated by * the Memoizer on the same input file will load the reader from the memo file * using {@link #loadMemo()} instead of performing a full reader * initialization. * * In essence, the speed-up gained from memoization will happen only after the * first initialization of the reader for a particular file. */ public class Memoizer extends ReaderWrapper { public interface Deser { void loadStart(File memoFile) throws IOException; Integer loadVersion() throws IOException; String loadReleaseVersion() throws IOException; String loadRevision() throws IOException; IFormatReader loadReader() throws IOException, ClassNotFoundException; void loadStop() throws IOException; void saveStart(File tempFile) throws IOException; void saveVersion(Integer version) throws IOException; void saveReleaseVersion(String version) throws IOException; void saveRevision(String revision) throws IOException; void saveReader(IFormatReader reader) throws IOException; void saveStop() throws IOException; void close(); } public static class KryoDeser implements Deser { final public Kryo kryo = new Kryo(); { // See https://github.com/EsotericSoftware/kryo/issues/216 ((Kryo.DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy()).setFallbackInstantiatorStrategy(new StdInstantiatorStrategy()); } FileInputStream fis; FileOutputStream fos; Input input; Output output; @Override public void close() { loadStop(); saveStop(); kryo.reset(); } @Override public void loadStart(File memoFile) throws FileNotFoundException { fis = new FileInputStream(memoFile); input = new Input(fis); } @Override public Integer loadVersion() { return kryo.readObject(input, Integer.class); } @Override public String loadReleaseVersion() { return kryo.readObject(input, String.class); } @Override public String loadRevision() { return kryo.readObject(input, String.class); } @Override public IFormatReader loadReader() { Class<?> c = kryo.readObject(input, Class.class); return (IFormatReader) kryo.readObject(input, c); } @Override public void loadStop() { if (input != null) { input.close(); input = null; } if (fis != null) { try { fis.close(); } catch (IOException e) { LOGGER.error("failed to close KryoDeser.fis", e); } fis = null; } } @Override public void saveStart(File tempFile) throws FileNotFoundException { fos = new FileOutputStream(tempFile); output = new Output(fos); } @Override public void saveVersion(Integer version) { kryo.writeObject(output, version); } @Override public void saveReleaseVersion(String version) { kryo.writeObject(output, version); } @Override public void saveRevision(String revision) { kryo.writeObject(output, revision); } @Override public void saveReader(IFormatReader reader) { kryo.writeObject(output, reader.getClass()); kryo.writeObject(output, reader); } @Override public void saveStop() { if (output != null) { output.close(); output = null; } if (fos != null) { try { fos.close(); fos = null; } catch (IOException e) { LOGGER.error("failed to close KryoDeser.fis", e); } } } } /** * Helper implementation that can be used to implement {@link Deser} * classes for libraries working solely with byte arrays. */ private static abstract class RandomAccessDeser implements Deser { RandomAccessInputStream loadStream; RandomAccessOutputStream saveStream; @Override public void loadStart(File memoFile) throws IOException { this.loadStream = new RandomAccessInputStream(memoFile.getAbsolutePath()); } @Override public Integer loadVersion() throws IOException { return loadStream.readInt(); } @Override public String loadReleaseVersion() throws IOException { int length = loadStream.readInt(); return loadStream.readString(length); } @Override public String loadRevision() throws IOException { int length = loadStream.readInt(); return loadStream.readString(length); } @Override public IFormatReader loadReader() throws IOException, ClassNotFoundException { int cSize = loadStream.readInt(); byte[] cArr = new byte[cSize]; loadStream.readFully(cArr); // Assuming proper encoding. @SuppressWarnings("unchecked") Class<IFormatReader> c = (Class<IFormatReader>) Class.forName( new String(cArr, Constants.ENCODING)); int rSize = loadStream.readInt(); byte[] rArr = new byte[rSize]; loadStream.readFully(rArr); return readerFromBytes(c, rArr); } protected abstract IFormatReader readerFromBytes(Class<IFormatReader> c, byte[] rArr) throws IOException, ClassNotFoundException; @Override public void loadStop() throws IOException { if (loadStream != null) { loadStream.close(); loadStream = null; } } @Override public void saveStart(File tempFile) throws IOException { this.saveStream = new RandomAccessOutputStream(tempFile.getAbsolutePath()); } @Override public void saveVersion(Integer version) throws IOException { saveStream.writeInt(version); } @Override public void saveReleaseVersion(String version) throws IOException { saveStream.writeInt(version.length()); saveStream.writeBytes(version); } @Override public void saveRevision(String revision) throws IOException { saveStream.writeInt(revision.length()); saveStream.writeBytes(revision); } @Override public void saveReader(IFormatReader reader) throws IOException { byte[] cArr = reader.getClass().getName().getBytes(Constants.ENCODING); saveStream.write(cArr.length); saveStream.write(cArr); byte[] rArr = bytesFromReader(reader); saveStream.write(rArr.length); saveStream.write(rArr); } protected abstract byte[] bytesFromReader(IFormatReader reader) throws IOException; @Override public void saveStop() throws IOException { if (saveStream != null) { saveStream.close(); saveStream = null; } } } // -- Constants -- /** * Default file version. Bumping this number will invalidate all other * cached items. This should happen when the order and type of objects stored * in the memo file changes. */ public static final Integer VERSION = 3; /** * Default value for {@link #minimumElapsed} if none is provided in the * constructor. */ public static final long DEFAULT_MINIMUM_ELAPSED = 100; /** * Default {@link org.slf4j.Logger} for the memoizer class */ private static final Logger LOGGER = LoggerFactory.getLogger(Memoizer.class); // -- Fields -- /** * Minimum number of milliseconds which must elapse during the call to * {@link #setId} before a memo file will be created. Default to * {@link #DEFAULT_MINIMUM_ELAPSED} if not specified via the constructor. */ private final long minimumElapsed; /** * Directory where all memo files should be created. If this value is * non-null, then all memo files will be created under it. Can be * overriden by {@link #doInPlaceCaching}. */ private final File directory; /** * If {@code true}, then all memo files will be created in the same * directory as the original file. */ private boolean doInPlaceCaching = false; protected transient Deser ser; private transient OMEXMLService service; private Location realFile; private File memoFile; private File tempFile; private boolean skipLoad = false; private boolean skipSave = false; /** * Boolean specifying whether to invalidate the memo file based upon * mismatched major/minor version numbers. By default, the Git commit hash * is used to invalidate the memo file. */ private boolean versionChecking = false; /** * Whether the {@link #reader} instance currently active was loaded from * the memo file during {@link #setId(String)}. */ private boolean loadedFromMemo = false; /** * Whether the {@link #reader} instance was saved to a memo file on * {@link #setId(String)}. */ private boolean savedToMemo = false; /** * {@link MetadataStore} set by the caller. This value will be held locally * and <em>not</em> set on the {@link #reader} delegate until the execution * of {@link #setId(String)}. If no value has been set by the caller, then * no special actions are taken during {@link #setId(String)}. If a value * is set, however, we must be careful with attempting to serialize it * * @see #handleMetadataStore(IFormatReader) */ private MetadataStore userMetadataStore = null; /** * {@link MetadataStore} created internally. * * @see #handleMetadataStore(IFormatReader) */ private MetadataStore replacementMetadataStore = null; // -- Constructors -- /** * Constructs a memoizer around a new {@link ImageReader} creating memo * files under the same directory as the original file only if the call to * {@link #setId} takes longer than {@value DEFAULT_MINIMUM_ELAPSED} in * milliseconds. */ public Memoizer() { this(DEFAULT_MINIMUM_ELAPSED); } /** * Constructs a memoizer around a new {@link ImageReader} creating memo * files under the same directory as the original file only if the call to * {@link #setId} takes longer than {@code minimumElapsed} in milliseconds. * * @param minimumElapsed a long specifying the number of milliseconds which * must elapse during the call to {@link #setId} before a memo file * will be created. */ public Memoizer(long minimumElapsed) { this(minimumElapsed, null); this.doInPlaceCaching = true; } /** * Constructs a memoizer around a new {@link ImageReader} creating memo file * files under the {@code directory} argument including the full path of the * original file only if the call to {@link #setId} takes longer than * {@code minimumElapsed} in milliseconds. * * @param minimumElapsed a long specifying the number of milliseconds which * must elapse during the call to {@link #setId} before a memo file * will be created. * @param directory a {@link File} specifying the directory where all memo * files should be created. If {@code null}, disable memoization. */ public Memoizer(long minimumElapsed, File directory) { super(); this.minimumElapsed = minimumElapsed; this.directory = directory; } /** * Constructs a memoizer around the given {@link IFormatReader} creating * memo files under the same directory as the original file only if the * call to {@link #setId} takes longer than * {@value DEFAULT_MINIMUM_ELAPSED} in milliseconds. * * @param r an {@link IFormatReader} instance */ public Memoizer(IFormatReader r) { this(r, DEFAULT_MINIMUM_ELAPSED); } /** * Constructs a memoizer around the given {@link IFormatReader} creating * memo files under the same directory as the original file only if the * call to {@link #setId} takes longer than {@code minimumElapsed} in * milliseconds. * * @param r an {@link IFormatReader} instance * @param minimumElapsed a long specifying the number of milliseconds which * must elapse during the call to {@link #setId} before a memo file * will be created. */ public Memoizer(IFormatReader r, long minimumElapsed) { this(r, minimumElapsed, null); this.doInPlaceCaching = true; } /** * Constructs a memoizer around the given {@link IFormatReader} creating * memo files under the {@code directory} argument including the full path * of the original file only if the call to {@link #setId} takes longer than * {@code minimumElapsed} in milliseconds. * * @param r an {@link IFormatReader} instance * @param minimumElapsed a long specifying the number of milliseconds which * must elapse during the call to {@link #setId} before a memo file * will be created. * @param directory a {@link File} specifying the directory where all memo * files should be created. If {@code null}, disable memoization. */ public Memoizer(IFormatReader r, long minimumElapsed, File directory) { super(r); this.minimumElapsed = minimumElapsed; this.directory = directory; } /** * Returns whether the {@link #reader} instance currently active was loaded * from the memo file during {@link #setId(String)}. * * @return {@code true} if the reader was loaded from the memo file, * {@code false} otherwise. */ public boolean isLoadedFromMemo() { return loadedFromMemo; } /** * Returns whether the {@link #reader} instance currently active was saved * to the memo file during {@link #setId(String)}. * * @return {@code true} if the reader was saved to the memo file, * {@code false} otherwise. */ public boolean isSavedToMemo() { return savedToMemo; } /** * Returns whether or not version checking is done based upon major/minor * version numbers. * * @return {@code true} if version checking is done based upon * major/minor version numbers, {@code false} otherwise. */ public boolean isVersionChecking() { return versionChecking; } /** * Returns {@code true} if the version of the memo file as returned by * {@link Deser#loadReleaseVersion()} and {@link Deser#loadRevision()} * do not match the current version as specified by {@link FormatTools#VERSION} * and {@link FormatTools#VCS_REVISION}, respectively. */ public boolean versionMismatch() throws IOException { final String releaseVersion = ser.loadReleaseVersion(); final String revision = ser.loadRevision(); if (!isVersionChecking()) { return false; } String minor = releaseVersion; int firstDot = minor.indexOf("."); if (firstDot >= 0) { int secondDot = minor.indexOf(".", firstDot + 1); if (secondDot >= 0) { minor = minor.substring(0, secondDot); } } String currentMinor = FormatTools.VERSION.substring(0, FormatTools.VERSION.indexOf(".", FormatTools.VERSION.indexOf(".") + 1)); if (!currentMinor.equals(minor)) { LOGGER.info("Different release version: {} not {}", releaseVersion, FormatTools.VERSION); return true; } // REVISION NUMBER if (!versionChecking && !FormatTools.VCS_REVISION.equals(revision)) { LOGGER.info("Different Git version: {} not {}", revision, FormatTools.VCS_REVISION); return true; } return false; } /** * Set whether version checking is done based upon major/minor version * numbers. * * If {@code true}, then a mismatch between the major/minor version of the * calling code (e.g. 4.4) and the major/minor version saved in the memo * file (e.g. 5.0) will result in the memo file being invalidated. * * If {@code false} (default), a mismatch in the Git commit hashes will * invalidate the memo file. * * This method allows for less strict version checking. * * @param version a boolean specifying whether version checking is done * based upon major/minor version numbers to invalidate the memo file * */ public void setVersionChecking(boolean version) { this.versionChecking = version; } protected void cleanup() { if (ser != null) { ser.close(); ser = null; } } @Override public void close() throws IOException { try { cleanup(); } finally { super.close(); } } @Override public void close(boolean fileOnly) throws IOException { try { cleanup(); } finally { super.close(fileOnly); } } // -- ReaderWrapper API methods -- @Override public void setId(String id) throws FormatException, IOException { StopWatch sw = stopWatch(); try { realFile = new Location(id); memoFile = getMemoFile(id); if (memoFile == null) { // Memoization disabled. if (userMetadataStore != null) { reader.setMetadataStore(userMetadataStore); } super.setId(id); // EARLY EXIT return; } IFormatReader memo = loadMemo(); // Should never throw kryo exceptions loadedFromMemo = false; savedToMemo = false; if (memo != null) { // loadMemo has already called handleMetadataStore with non-null try { loadedFromMemo = true; reader = memo; reader.reopenFile(); } catch (FileNotFoundException e) { LOGGER.info("could not reopen file - deleting invalid memo file: {}", memoFile); deleteQuietly(memoFile); memo = null; reader.close(); loadedFromMemo = false; } } if (memo == null) { OMEXMLService service = getService(); super.setMetadataStore(service.createOMEXMLMetadata()); long start = System.currentTimeMillis(); super.setId(id); long elapsed = System.currentTimeMillis() - start; handleMetadataStore(null); // Between setId and saveMemo if (elapsed < minimumElapsed) { LOGGER.debug("skipping save memo. elapsed millis: {}", elapsed); return; // EARLY EXIT! } savedToMemo = saveMemo(); // Should never throw. } } catch (ServiceException e) { LOGGER.error("Could not create OMEXMLMetadata", e); } finally { sw.stop("loci.formats.Memoizer.setId"); } } @Override public void setMetadataStore(MetadataStore store) { this.userMetadataStore = store; } @Override public MetadataStore getMetadataStore() { if (this.userMetadataStore != null) { return this.userMetadataStore; } return reader.getMetadataStore(); } //-- Helper methods -- /** * Attempts to delete an existing file, logging at * warn if the deletion returns false or at error * if an exception is thrown. * * @return the result from {@link java.io.File#delete} or {@code false} if * an exception is thrown. */ protected boolean deleteQuietly(File file) { try { if (file != null && file.exists()) { if (file.delete()) { LOGGER.trace("deleted {}", file); return true; } else { LOGGER.warn("file deletion failed {}", file); } } } catch (Throwable t) { LOGGER.error("file deletion failed: {}", file, t); } return false; } /** * Returns a configured {@link Kryo} instance. This method can be modified * by consumers. The returned instance is not thread-safe. * * @return a non-null {@link Kryo} instance. */ protected Deser getDeser() { if (ser == null) { ser = new KryoDeser(); } return ser; } // Copied from OMETiffReader. protected OMEXMLService getService() throws MissingLibraryException { if (service == null) { try { ServiceFactory factory = new ServiceFactory(); service = factory.getInstance(OMEXMLService.class); } catch (DependencyException de) { throw new MissingLibraryException(OMEXMLServiceImpl.NO_OME_XML_MSG, de); } } return service; } protected Slf4JStopWatch stopWatch() { return new Slf4JStopWatch(LOGGER, Slf4JStopWatch.DEBUG_LEVEL); } /** * Constructs a {@link File} object from {@code id} string. This method * can be modified by consumers, but then existing memo files will not be * found. * * @param id the path passed to {@link #setId} * @return a {@link File} object pointing at the location of the memo file */ public File getMemoFile(String id) { File f = null; File writeDirectory = null; if (directory == null && !doInPlaceCaching) { // Disabling memoization unless specific directory is provided. // This prevents random cache files from being unknowingly written. LOGGER.debug("skipping memo: no directory given"); return null; } else { // If the memoizer directory is set to be the root folder, the memo file // will be saved in the same folder as the file specified by id. Since // the root folder will likely not be writeable by the user, we want to // exclude this special case from the test below id = new File(id).getAbsolutePath(); String rootPath = id.substring(0, id.indexOf(File.separator) + 1); if (doInPlaceCaching || directory.getAbsolutePath().equals(rootPath)) { f = new File(id); writeDirectory = new File(f.getParent()); } else { // this serves to strip off the drive letter on Windows // since we're using the absolute path, 'id' will either start with // File.separator (as on UNIX), or a drive letter (as on Windows) id = id.substring(id.indexOf(File.separator) + 1); f = new File(directory, id); writeDirectory = directory; } // Check either the in-place folder or the main memoizer directory // exists and is writeable if (!writeDirectory.exists() || !writeDirectory.canWrite()) { LOGGER.warn("skipping memo: directory not writeable - {}", writeDirectory); return null; } f.getParentFile().mkdirs(); } String p = f.getParent(); String n = f.getName(); return new File(p, "." + n + ".bfmemo"); } /** * Load a memo file if possible, returning a null if not. * * Corrupt memo files will be deleted if possible. Kryo * exceptions should never propagate to the caller. Only * the regular Bio-Formats exceptions should be thrown. */ public IFormatReader loadMemo() throws IOException, FormatException { if (skipLoad) { LOGGER.trace("skip load"); return null; } if (!memoFile.exists()) { LOGGER.trace("Memo file doesn't exist: {}", memoFile); return null; } if(!memoFile.canRead()) { LOGGER.trace("Can't read memo file: {}", memoFile); return null; } long memoLast = memoFile.lastModified(); long realLast = realFile.lastModified(); if (memoLast < realLast) { LOGGER.debug("memo(lastModified={}) older than real(lastModified={})", memoLast, realLast); return null; } final Deser ser = getDeser(); final StopWatch sw = stopWatch(); IFormatReader copy = null; ser.loadStart(memoFile); try { // VERSION Integer version = ser.loadVersion(); if (!VERSION.equals(version)) { LOGGER.info("Old version of memo file: {} not {}", version, VERSION); return null; } // RELEASE VERSION NUMBER if (versionMismatch()) { // Logging done in versionMismatch return null; } // CLASS & COPY try { copy = ser.loadReader(); } catch (ClassNotFoundException e) { LOGGER.warn("unknown reader type: {}", e); return null; } boolean equal = false; try { equal = FormatTools.equalReaders(reader, copy); } catch (RuntimeException rt) { copy.close(); throw rt; } catch (Error err) { copy.close(); throw err; } if (!equal) { copy.close(); return null; } copy = handleMetadataStore(copy); if (copy == null) { LOGGER.debug("metadata store invalidated cache: {}", memoFile); } // TODO: // Check flags // DataV1 class? // Handle exceptions on read/write. possibly deleting. LOGGER.debug("loaded memo file: {} ({} bytes)", memoFile, memoFile.length()); return copy; } catch (KryoException e) { LOGGER.warn("deleting invalid memo file: {}", memoFile, e); deleteQuietly(memoFile); return null; } catch (ArrayIndexOutOfBoundsException e) { LOGGER.warn("deleting invalid memo file: {}", memoFile, e); deleteQuietly(memoFile); return null; } catch (Throwable t) { // Logging at error since this is unexpected. LOGGER.error("deleting invalid memo file: {}", memoFile, t); deleteQuietly(memoFile); return null; } finally { ser.loadStop(); sw.stop("loci.formats.Memoizer.loadMemo"); } } /** * Save a reader including all reader wrappers inside a memo file. */ public boolean saveMemo() { if (skipSave) { LOGGER.trace("skip memo"); return false; } final Deser ser = getDeser(); final StopWatch sw = stopWatch(); boolean rv = true; try { // Create temporary location for output // Note: can't rename tempfile until resources are closed. tempFile = File.createTempFile( memoFile.getName(), "", memoFile.getParentFile()); ser.saveStart(tempFile); // Save to temporary location. ser.saveVersion(VERSION); ser.saveReleaseVersion(FormatTools.VERSION); ser.saveRevision(FormatTools.VCS_REVISION); ser.saveReader(reader); ser.saveStop(); LOGGER.debug("saved to temp file: {}", tempFile); } catch (Throwable t) { // Any exception should be ignored, and false returned. LOGGER.warn(String.format("failed to save memo file: %s", memoFile), t); rv = false; } finally { // Close the output stream quietly regardless. try { ser.saveStop(); sw.stop("loci.formats.Memoizer.saveMemo"); } catch (Throwable t) { LOGGER.error("output close failed", t); } // Rename temporary file if successful. // Any failures will have to be ignored. // Note: renaming the tempfile with open // resources can lead to segfaults if (rv) { if (!tempFile.renameTo(memoFile)) { LOGGER.error("temp file rename returned false: {}", tempFile); } else { LOGGER.debug("saved memo file: {} ({} bytes)", memoFile, memoFile.length()); } } deleteQuietly(tempFile); } return rv; } /** * Return the {@link IFormatReader} instance that is passed in or null if * it has been invalidated, which will include the instance being closed. * * <ul> * <li><em>Serialization:</em> If an unknown {@link MetadataStore} * implementation is passed in when no memo file exists, then a replacement * {@link MetadataStore} will be created and set on the {@link #reader} * delegate before calling {@link ReaderWrapper#setId(String)}. This stack * will then be serialized, before any values are copied into * {@link #userMetadataStore}. * </li> * <li><em>Deserialization<em>: If an unknown {@link MetadataStore} * implementation is set before calling {@link #setId(String)} then ... * </li> * </ul> */ protected IFormatReader handleMetadataStore(IFormatReader memo) throws MissingLibraryException { // Then nothing has been requested of us // and we can exit safely. if (userMetadataStore == null) { return memo; // EARLY EXIT. Possibly null. } // If we've been passed a memo object, then it's just been loaded. final boolean onLoad = (memo != null); // TODO: What to do if the contained store is a Dummy? // TODO: Which stores should we handle regularly? if (onLoad) { MetadataStore filledStore = memo.getMetadataStore(); // Return value is important. if (filledStore == null) { // TODO: what now? } else if (!(filledStore instanceof MetadataRetrieve)) { LOGGER.error("Found non-MetadataRetrieve: {}" + filledStore.getClass()); } else { OMEXMLService service = getService(); service.convertMetadata((MetadataRetrieve) filledStore, userMetadataStore); } } else { // on save; we've just called super.setId() // Then pull out the reader and replace it. // Return value is unimportant. MetadataStore filledStore = reader.getMetadataStore(); if (reader.getMetadataStore() == null) { // TODO: what now? LOGGER.error("Found null-MetadataRetrieve: {}" + filledStore.getClass()); } else if (!(filledStore instanceof MetadataRetrieve)) { LOGGER.error("Found non-MetadataRetrieve: {}" + filledStore.getClass()); } else { OMEXMLService service = getService(); service.convertMetadata((MetadataRetrieve) filledStore, userMetadataStore); } } return memo; } public static void main(String[] args) throws Exception { if (args.length == 0 || args.length > 2) { System.err.println("Usage: memoizer file [tmpdir]"); System.exit(2); } File tmp = new File(System.getProperty("java.io.tmpdir")); if (args.length == 2) { tmp = new File(args[1]); } System.out.println("First load of " + args[0]); load(args[0], tmp, true); // initial System.out.println("Second load of " + args[0]); load(args[0], tmp, false); // reload } private static void load(String id, File tmp, boolean delete) throws Exception { Memoizer m = new Memoizer(0L, tmp); File memo = m.getMemoFile(id); if (delete && memo != null && memo.exists()) { System.out.println("Deleting " + memo); memo.delete(); } m.setVersionChecking(false); try { m.setId(id); m.openBytes(0); IFormatReader r = m.getReader(); r = ((ImageReader) r).getReader(); System.out.println(r); } finally { m.close(); } } }