Modified: sis/branches/JDK9/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java URL: http://svn.apache.org/viewvc/sis/branches/JDK9/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java?rev=1807624&r1=1807623&r2=1807624&view=diff ============================================================================== --- sis/branches/JDK9/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java [UTF-8] (original) +++ sis/branches/JDK9/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java [UTF-8] Thu Sep 7 15:47:24 2017 @@ -17,35 +17,46 @@ package org.apache.sis.storage; import java.util.Map; -import java.util.Queue; import java.util.Iterator; import java.util.Collections; -import java.util.LinkedList; import java.util.IdentityHashMap; -import java.util.ConcurrentModificationException; import java.io.Reader; import java.io.DataInput; import java.io.InputStream; import java.io.IOException; +import java.io.LineNumberReader; import java.io.InputStreamReader; +import java.io.BufferedInputStream; import java.io.Serializable; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.nio.channels.Channel; import java.nio.channels.ReadableByteChannel; -import javax.imageio.ImageIO; +import java.nio.channels.SeekableByteChannel; import javax.imageio.stream.ImageInputStream; +import javax.imageio.ImageIO; import java.sql.Connection; +import java.sql.SQLException; import javax.sql.DataSource; import org.apache.sis.util.Debug; import org.apache.sis.util.Classes; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.ObjectConverters; import org.apache.sis.util.resources.Errors; +import org.apache.sis.util.logging.Logging; +import org.apache.sis.util.collection.TreeTable; +import org.apache.sis.util.collection.TableColumn; +import org.apache.sis.util.collection.DefaultTreeTable; +import org.apache.sis.util.UnconvertibleObjectException; +import org.apache.sis.internal.storage.Resources; import org.apache.sis.internal.storage.io.IOUtilities; import org.apache.sis.internal.storage.io.ChannelFactory; import org.apache.sis.internal.storage.io.ChannelDataInput; import org.apache.sis.internal.storage.io.ChannelImageInputStream; import org.apache.sis.internal.storage.io.InputStreamAdapter; +import org.apache.sis.internal.system.Modules; +import org.apache.sis.internal.util.Utilities; +import org.apache.sis.io.InvalidSeekException; import org.apache.sis.setup.OptionKey; @@ -97,6 +108,90 @@ public class StorageConnector implements static final int MINIMAL_BUFFER_SIZE = 256; /** + * A flag for <code>{@linkplain #addView(Class, Object, Class, byte) addView}(…, view, source, flags)</code> + * telling that after closing the {@code view}, we also need to close the {@code source}. + * This flag should be set when the view is an {@link ImageInputStream} because Java I/O + * {@link javax.imageio.stream.FileCacheImageInputStream#close()} does not close the underlying stream. + * For most other kinds of view, this flag should not be set. + * + * @see Coupled#cascadeOnClose() + */ + private static final byte CASCADE_ON_CLOSE = 1; + + /** + * A flag for <code>{@linkplain #addView(Class, Object, Class, byte) addView}(…, view, source, flags)</code> + * telling that before reseting the {@code view}, we need to reset the {@code source} first. This flag should + * can be unset if any change in the position of {@code view} is immediately reflected in the position of + * {@code source}, and vis-versa. + * + * @see Coupled#cascadeOnReset() + */ + private static final byte CASCADE_ON_RESET = 2; + + /** + * A flag for <code>{@linkplain #addView(Class, Object, Class, byte) addView}(…, view, source, flags)</code> + * telling that {@code view} can not be reseted, so it should be set to {@code null} instead. This implies + * that a new view of the same type will be recreated next time it will be requested. + * + * <p>When this flag is set, the {@link #CASCADE_ON_RESET} should usually be set in same time.</p> + */ + private static final byte CLEAR_ON_RESET = 4; + + /** + * Handler to {@code StorageConnector.createFoo()} methods associated to given storage types. + * Each {@code createFoo()} method may be invoked once for opening an input stream, character + * reader, database connection, <i>etc</i> from user-supplied path, URI, <i>etc</i>. + * + * @param <T> the type of input created by this {@code Opener} instance. + */ + @FunctionalInterface + private interface Opener<T> { + /** + * Invoked when first needed for creating an input of the requested type. + * This method should invoke {@link #addView(Class, Object, Class, byte)} + * for caching the result before to return the view. + */ + T open(StorageConnector c) throws Exception; + } + + /** Helper method for {@link #OPENERS} static initialization. */ + private static <T> void add(final Class<T> type, final Opener<T> op) { + if (OPENERS.put(type, op) != null) throw new AssertionError(type); + } + + /** + * List of types recognized by {@link #getStorageAs(Class)}, associated to the methods for opening stream + * of those types. This map shall contain every types documented in {@link #getStorageAs(Class)} javadoc. + * {@code null} values means to use {@link ObjectConverters} for that particular type. + */ + private static final Map<Class<?>, Opener<?>> OPENERS = new IdentityHashMap<>(13); + static { + add(String.class, StorageConnector::createString); + add(ByteBuffer.class, StorageConnector::createByteBuffer); + add(DataInput.class, StorageConnector::createDataInput); + add(ImageInputStream.class, StorageConnector::createImageInputStream); + add(InputStream.class, StorageConnector::createInputStream); + add(Reader.class, StorageConnector::createReader); + add(Connection.class, StorageConnector::createConnection); + add(ChannelDataInput.class, (s) -> s.createChannelDataInput(false)); // Undocumented case (SIS internal) + add(ChannelFactory.class, (s) -> null); // Undocumented. Shall not cache. + /* + * ChannelFactory may have been created as a side effect of creating a ReadableByteChannel. + * Caller should have asked for another type (e.g. InputStream) before to ask for that type. + * Consequently null value for ChannelFactory shall not be cached since the actual value may + * be computed later. + * + * Following classes will be converted using ObjectConverters, but without throwing an + * exception if the conversion fail. Instead, getStorageAs(Class) will return null. + * Classes not listed here will let the UnconvertibleObjectException propagates. + */ + add(java.net.URI.class, null); + add(java.net.URL.class, null); + add(java.io.File.class, null); + add(java.nio.file.Path.class, null); + } + + /** * The input/output object given at construction time. * * @see #getStorage() @@ -116,57 +211,329 @@ public class StorageConnector implements private transient String extension; /** - * Views of {@link #storage} as some of the following supported types: + * The options, created only when first needed. * + * @see #getOption(OptionKey) + * @see #setOption(OptionKey, Object) + */ + private Map<OptionKey<?>, Object> options; + + /** + * Views of {@link #storage} as instances of different types than the type of the object given to the constructor. + * The {@code null} reference can appear in various places: * <ul> - * <li>{@link ByteBuffer}: - * A read-only view of the buffer over the first bytes of the stream.</li> - * - * <li>{@link DataInput}: - * The input as a data input stream. Unless the {@link #storage} is already an instance of {@link DataInput}, - * this entry will be given an instance of {@link ChannelImageInputStream} if possible rather than an arbitrary - * stream. In particular, we invoke the {@link ImageIO#createImageInputStream(Object)} factory method only in - * last resort because some SIS data stores will want to access the channel and buffer directly.</li> - * - * <li>{@link ImageInputStream}: - * Same as {@code DataInput} if it can be casted, or {@code null} otherwise.</li> - * - * <li>{@link InputStream}: - * If not explicitely provided, this is a wrapper around the above {@link ImageInputStream}.</li> - * - * <li>{@link Reader}: - * If not explicitely provided, this is a wrapper around the above {@link InputStream}.</li> - * - * <li>{@link Connection}: - * The storage object as a JDBC connection.</li> + * <li>A non-existent entry (equivalent to an entry associated to the {@code null} value) means that the value + * has not yet been computed.</li> + * <li>A {@linkplain Coupled#isValid valid entry} with {@link Coupled#view} set to {@code null} means the value + * has been computed and we have determined that {@link #getStorageAs(Class)} shall return {@code null} for + * that type.</li> + * <li>By convention, the {@code null} key is associated to the {@link #storage} value.</li> * </ul> * - * A non-existent entry means that the value has not yet been computed. A {@link Void#TYPE} value means the value - * has been computed and we have determined that {@link #getStorageAs(Class)} shall returns {@code null} for that - * type. - * + * @see #addView(Class, Object, Class, byte) + * @see #getView(Class) * @see #getStorageAs(Class) */ - private transient Map<Class<?>, Object> views; + private transient Map<Class<?>, Coupled> views; /** - * Objects which will need to be closed by the {@link #closeAllExcept(Object)} method. - * For each (<var>key</var>, <var>value</var>) entry, if the object to close (the key) - * is a wrapper around an other object (e.g. an {@link InputStreamReader} wrapping an - * {@link InputStream}), then the value is the other object. + * Wraps an instance of @link InputStream}, {@link DataInput}, {@link Reader}, <i>etc.</i> together with additional + * information about other objects that are coupled with the wrapped object. For example if a {@link Reader} is a + * wrapper around the user-supplied {@link InputStream}, then those two objects will be wrapped in {@code Coupled} + * instances together with information about how they are related + * + * One purpose of {@code Coupled} information is to keep trace of objects which will need to be closed by the + * {@link StorageConnector#closeAllExcept(Object)} method (for example an {@link InputStreamReader} wrapping + * an {@link InputStream}). + * + * Another purpose is to determine which views need to be synchronized if {@link StorageConnector#storage} is + * used independently. They are views that may advance {@code storage} position, but not in same time than the + * {@link #view} position (typically because the view reads some bytes in advance and stores them in a buffer). + * Such coupling may occur when the storage is an {@link InputStream}, an {@link java.io.OutputStream} or a + * {@link java.nio.channels.Channel}. The coupled {@link #view} can be: * - * @see #addViewToClose(Object, Object) - * @see #closeAllExcept(Object) + * <ul> + * <li>{@link Reader} that are wrappers around {@code InputStream}.</li> + * <li>{@link ChannelDataInput} when the channel come from an {@code InputStream}.</li> + * <li>{@link ChannelDataInput} when the channel has been explicitely given to the constructor.</li> + * </ul> */ - private transient Map<Object, Object> viewsToClose; + private static final class Coupled { + /** + * The {@link StorageConnector#storage} viewed as another kind of object. + * Supported types are: + * + * <ul> + * <li>{@link ByteBuffer}: + * A read-only view of the buffer over the first bytes of the stream.</li> + * + * <li>{@link DataInput}: + * The input as a data input stream. Unless the {@link #storage} is already an instance of {@link DataInput}, + * this entry will be given an instance of {@link ChannelImageInputStream} if possible rather than an arbitrary + * stream. In particular, we invoke the {@link ImageIO#createImageInputStream(Object)} factory method only in + * last resort because some SIS data stores will want to access the channel and buffer directly.</li> + * + * <li>{@link ImageInputStream}: + * Same as {@code DataInput} if it can be casted, or {@code null} otherwise.</li> + * + * <li>{@link InputStream}: + * If not explicitely provided, this is a wrapper around the above {@link ImageInputStream}.</li> + * + * <li>{@link Reader}: + * If not explicitely provided, this is a wrapper around the above {@link InputStream}.</li> + * + * <li>{@link Connection}: + * The storage object as a JDBC connection.</li> + * </ul> + */ + Object view; - /** - * The options, created only when first needed. - * - * @see #getOption(OptionKey) - * @see #setOption(OptionKey, Object) - */ - private transient Map<OptionKey<?>, Object> options; + /** + * The object that {@link #view} is wrapping. For example if {@code view} is an {@link InputStreamReader}, + * then {@code wrapperFor.view} is an {@link InputStream}. This field is {@code null} if {@link #view} == + * {@link StorageConnector#storage}. + */ + final Coupled wrapperFor; + + /** + * The other views that are consuming {@link #view}, or {@code null} if none. For each element in this array, + * {@code wrappedBy[i].wrapperFor == this}. + */ + private Coupled[] wrappedBy; + + /** + * Bitwise combination of {@link #CASCADE_ON_CLOSE}, {@link #CASCADE_ON_RESET} or {@link #CLEAR_ON_RESET}. + */ + final byte cascade; + + /** + * {@code true} if the position of {@link #view} is synchronized with the position of {@link #wrapperFor}. + */ + boolean isValid; + + /** + * Creates a wrapper for {@link StorageConnector#storage}. This constructor is used when we need to create + * a {@code Coupled} instance for another view wrapping {@code storage}. + */ + Coupled(final Object storage) { + view = storage; + wrapperFor = null; + cascade = 0; + isValid = true; + } + + /** + * Creates a wrapper for a view wrapping the given {@code Coupled} instance. + * Caller is responsible to set the {@link #view} field after this constructor call. + * + * @param wrapperFor the object that {@link #view} will wrap, or {@code null} if none. + * @param cascade bitwise combination of {@link #CASCADE_ON_CLOSE}, {@link #CASCADE_ON_RESET} + * or {@link #CLEAR_ON_RESET}. + */ + @SuppressWarnings("ThisEscapedInObjectConstruction") + Coupled(final Coupled wrapperFor, final byte cascade) { + this.wrapperFor = wrapperFor; + this.cascade = cascade; + if (wrapperFor != null) { + final Coupled[] w = wrapperFor.wrappedBy; + final int n = (w != null) ? w.length : 0; + final Coupled[] e = new Coupled[n + 1]; + if (n != 0) System.arraycopy(w, 0, e, 0, n); + e[n] = this; + wrapperFor.wrappedBy = e; + } + } + + /** + * {@code true} if after closing the {@link #view}, we need to also close the {@link #wrapperFor}. + * Should be {@code true} when the view is an {@link ImageInputStream} because Java I/O + * {@link javax.imageio.stream.FileCacheImageInputStream#close()} does not close the underlying stream. + * For most other kinds of view, should be {@code false}. + */ + final boolean cascadeOnClose() { + return (cascade & CASCADE_ON_CLOSE) != 0; + } + + /** + * {@code true} if calls to {@link #reset()} should cascade to {@link #wrapperFor}. + * This is {@code false} if any change in the position of {@link #view} is immediately + * reflected in the position of {@link #wrapperFor}, and vis-versa. + */ + final boolean cascadeOnReset() { + return (cascade & CASCADE_ON_RESET) != 0; + } + + /** + * Declares as invalid all unsynchronized {@code Coupled} instances which are used, directly or indirectly, + * by this instance. This method is invoked before {@link StorageConnector#getStorageAs(Class)} returns a + * view, in order to remember which views would need to be resynchronized if they are requested. + */ + final void invalidateSources() { + boolean sync = cascadeOnReset(); + for (Coupled c = wrapperFor; sync; c = c.wrapperFor) { + c.isValid = false; + sync = c.cascadeOnReset(); + } + } + + /** + * Declares as invalid all unsynchronized {@code Coupled} instances which are using, directly or indirectly, + * this instance. This method is invoked before {@link StorageConnector#getStorageAs(Class)} returns a view, + * in order to remember which views would need to be resynchronized if they are requested. + */ + final void invalidateUsages() { + if (wrappedBy != null) { + for (final Coupled c : wrappedBy) { + if (c.cascadeOnReset()) { + c.isValid = false; + c.invalidateUsages(); + } + } + } + } + + /** + * Identifies the other views to <strong>not</strong> close if we don't want to close the {@link #view} + * wrapped by this {@code Coupled}. This method identifies only the views that <em>use</em> this view; + * it does not identify the views <em>used</em> by this view. + * + * This method is for {@link StorageConnector#closeAllExcept(Object)} internal usage. + * + * @param toClose the map where to write the list of views to not close. + */ + final void protect(final Map<AutoCloseable,Boolean> toClose) { + if (wrappedBy != null) { + for (final Coupled c : wrappedBy) { + if (!c.cascadeOnClose()) { + if (c.view instanceof AutoCloseable) { + toClose.put((AutoCloseable) c.view, Boolean.FALSE); + } + c.protect(toClose); + } + } + } + } + + /** + * Resets the position of all sources of the {@link #view}, then the view itself. + * + * @return {@code true} if some kind of reset has been performed. + * Note that it does means that the view {@link #isValid} is {@code true}. + */ + final boolean reset() throws IOException { + if (isValid) { + return false; + } + /* + * We need to reset the sources before to reset the view of this Coupled instance. + * For example if this Coupled instance contains a ChannelDataInput, we need to + * reset the underlying InputStream before to reset the ChannelDataInput. + */ + if (cascadeOnReset()) { + wrapperFor.reset(); + } + if ((cascade & CLEAR_ON_RESET) != 0) { + /* + * If the view can not be reset, in some cases we can discard it and recreate a new view when + * first needed. The 'isValid' flag is left to false for telling that a new value is requested. + */ + view = null; + return true; + } else if (view instanceof InputStream) { + /* + * Note on InputStream.reset() behavior documented in java.io: + * + * - It does not discard the mark, so it is okay if reset() is invoked twice. + * - If mark is unsupported, may either throw IOException or reset the stream + * to an implementation-dependent fixed state. + */ + ((InputStream) view).reset(); + } else if (view instanceof Reader) { + /* + * Defined as a matter of principle but should not be needed since we do not wrap java.io.Reader + * (except in BufferedReader if the original storage does not support mark/reset). + */ + ((Reader) view).reset(); + } else if (view instanceof ChannelDataInput) { + /* + * ChannelDataInput can be recycled without the need to discard and recreate them. Note that + * this code requires that SeekableByteChannel has been seek to the channel beginning first. + * This should be done by the above 'wrapperFor.reset()' call. + */ + final ChannelDataInput input = (ChannelDataInput) view; + input.buffer.limit(0); // Must be after channel reset. + input.setStreamPosition(0); // Must be after buffer.limit(0). + } else if (view instanceof Channel) { + /* + * Searches for a ChannelDataInput wrapping the channel, because it contains the original position + * (note: StorageConnector tries to instantiate ChannelDataInput in priority to all other types). + * If we don't find any, this is considered as a non-seekable channel (we do not assume that the + * channel original position was 0 when the user gave it to StorageConnector). + */ + String name = null; + if (wrappedBy != null) { + for (Coupled c : wrappedBy) { + if (c.view instanceof ChannelDataInput) { + final ChannelDataInput in = ((ChannelDataInput) c.view); + if (view instanceof SeekableByteChannel) { + ((SeekableByteChannel) view).position(in.channelOffset); + return true; + } + name = in.filename; // For the error message. + } + } + } + if (name == null) name = Classes.getShortClassName(view); + throw new InvalidSeekException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, name)); + } else { + /* + * For any other kind of object, we don't know how to recycle them. Current implementation + * does nothing on the assumption that the object can be reused (example: NetcdfFile). + */ + } + isValid = true; + return true; + } + + /** + * Returns a string representation for debugging purpose. + */ + @Debug + @Override + public String toString() { + return Utilities.toString(getClass(), + "view", Classes.getShortClassName(view), + "wrapperFor", (wrapperFor != null) ? Classes.getShortClassName(wrapperFor.view) : null, + "cascade", cascade, + "isValid", isValid); + } + + /** + * Formats the current {@code Coupled} and all its children as a tree in the given tree table node. + * This method is used for {@link StorageConnector#toString()} implementation only and may change + * in any future version. + * + * @param appendTo where to write name, value and children. + * @param views reference to the {@link StorageConnector#views} map. Will be read only. + */ + @Debug + final void append(final TreeTable.Node appendTo, final Map<Class<?>, Coupled> views) { + Class<?> type = null; + for (final Map.Entry<Class<?>, Coupled> entry : views.entrySet()) { + if (entry.getValue() == this) { + final Class<?> t = Classes.findCommonClass(type, entry.getKey()); + if (t != Object.class) type = t; + } + } + appendTo.setValue(TableColumn.NAME, Classes.getShortName(type)); + appendTo.setValue(TableColumn.VALUE, Classes.getShortClassName(view)); + if (wrappedBy != null) { + for (final Coupled c : wrappedBy) { + c.append(appendTo.newChild(), views); + } + } + } + } /** * Creates a new data store connection wrapping the given input/output object. @@ -215,10 +582,12 @@ public class StorageConnector implements * The object can be of any type, but the class javadoc lists the most typical ones. * * @return the input/output object as a URL, file, image input stream, <i>etc.</i>. + * @throws DataStoreException if the storage object has already been used and can not be reused. * * @see #getStorageAs(Class) */ - public Object getStorage() { + public Object getStorage() throws DataStoreException { + reset(); return storage; } @@ -283,6 +652,15 @@ public class StorageConnector implements * <li>Otherwise this method returns {@code null}.</li> * </ul> * </li> + * <li>{@link java.nio.file.Path}, {@link java.net.URI}, {@link java.net.URL}, {@link java.io.File}: + * <ul> + * <li>If the {@linkplain #getStorage() storage} object is an instance of the {@link java.nio.file.Path}, + * {@link java.io.File}, {@link java.net.URL}, {@link java.net.URI} or {@link CharSequence} types and + * that type can be converted to the requested type, returned the conversion result.</li> + * + * <li>Otherwise this method returns {@code null}.</li> + * </ul> + * </li> * <li>{@link ByteBuffer}: * <ul> * <li>If the {@linkplain #getStorage() storage} object can be obtained as described in bullet 2 of the @@ -349,6 +727,14 @@ public class StorageConnector implements * <li>Otherwise this method returns {@code null}.</li> * </ul> * </li> + * <li>Any other types: + * <ul> + * <li>If the storage given at construction time is already an instance of the requested type, + * returns it <i>as-is</i>.</li> + * + * <li>Otherwise this method throws {@link IllegalArgumentException}.</li> + * </ul> + * </li> * </ul> * * Multiple invocations of this method on the same {@code StorageConnector} instance will try @@ -369,127 +755,188 @@ public class StorageConnector implements */ public <T> T getStorageAs(final Class<T> type) throws IllegalArgumentException, DataStoreException { ArgumentChecks.ensureNonNull("type", type); - if (views != null) { - final Object view = views.get(type); - if (view != null) { - if (view == storage && view instanceof InputStream) try { - resetInputStream(); - } catch (IOException e) { - throw new DataStoreException(Errors.format(Errors.Keys.CanNotRead_1, getStorageName()), e); + /* + * Verify if the cache contains an instance created by a previous invocation of this method. + * Note that InputStream may need to be reseted if it has been used indirectly by other kind + * of stream (for example a java.io.Reader). Example: + * + * 1) The storage specified at construction time is a java.nio.file.Path. + * 2) getStorageAs(InputStream.class) opens an InputStream. Caller rewinds it after use. + * 3) getStorageAs(Reader.class) wraps the InputStream. Caller rewinds the Reader after use, + * but invoking BufferedReader.reset() has no effect on the underlying InputStream. + * 4) getStorageAs(InputStream.class) needs to rewind the InputStream itself since it was + * not done at step 3. However doing so invalidate the Reader, so we need to discard it. + */ + Coupled value = getView(type); + if (reset(value)) { + return type.cast(value.view); // null is a valid result. + } + /* + * If the storage is already an instance of the requested type, returns the storage as-is. + * We check if the storage needs to be reseted in the same way than in getStorage() method. + * As a special case, we ensure that InputStream and Reader can be marked. + */ + if (type.isInstance(storage)) { + @SuppressWarnings("unchecked") + T view = (T) storage; + reset(); + byte cascade = 0; + if (type == InputStream.class) { + final InputStream in = (InputStream) view; + if (!in.markSupported()) { + view = type.cast(new BufferedInputStream(in)); + cascade = (byte) (CLEAR_ON_RESET | CASCADE_ON_RESET); + } + } else if (type == Reader.class) { + final Reader in = (Reader) view; + if (!in.markSupported()) { + view = type.cast(new LineNumberReader(in)); + cascade = (byte) (CLEAR_ON_RESET | CASCADE_ON_RESET); } - return (view != Void.TYPE) ? type.cast(view) : null; } - } else { - views = new IdentityHashMap<>(); + addView(type, view, null, cascade); + return view; } /* - * Special case for DataInput and ByteBuffer, because those values are created together. - * In addition, ImageInputStream creation assigns a value to the 'streamOrigin' field. - * The ChannelDataInput case is an undocumented (SIS internal) type for avoiding the - * potential call to ImageIO.createImageInputStream(…) when we do not need it. + * If the type is not one of the types listed in OPENERS, we delegate to ObjectConverter. + * It may throw UnconvertibleObjectException (an IllegalArgumentException subtype) if the + * given type is unrecognized. So the IllegalArgumentException documented in method javadoc + * happen (indirectly) here. */ - boolean done = false; - try { - if (type == ByteBuffer.class) { - createByteBuffer(); - done = true; - } else if (type == DataInput.class) { - createDataInput(); - done = true; - } else if (type == ChannelDataInput.class) { // Undocumented case (SIS internal) - createChannelDataInput(false); - done = true; - } else if (type == ChannelFactory.class) { // Undocumented case (SIS internal) - /* - * ChannelFactory may have been created as a side effect of creating a ReadableByteChannel. - * Caller should have asked for another type (e.g. InputStream) before to ask for this type. - */ - done = true; + final Opener<?> method = OPENERS.get(type); + if (method == null) { + T view; + try { + view = ObjectConverters.convert(storage, type); + } catch (UnconvertibleObjectException e) { + if (!OPENERS.containsKey(type)) throw e; + Logging.recoverableException(Logging.getLogger(Modules.STORAGE), StorageConnector.class, "getStorageAs", e); + view = null; } - } catch (IOException e) { - throw new DataStoreException(Errors.format(Errors.Keys.CanNotOpen_1, getStorageName()), e); - } - if (done) { - // Want to exit this method even if the value is null. - return getView(type); + addView(type, view); + return view; } /* - * All other cases. + * No instance has been created previously for the requested type. Open the stream now. + * Some types will need to reset the InputStream or Channel, but the decision of doing + * so or not is left to openers. Result will be cached by the 'createFoo()' method. + * Note that it may cache 'null' value if no stream of the given type can be created. */ - final Object value; + final Object view; try { - value = createView(type); - } catch (RuntimeException | DataStoreException e) { + view = method.open(this); + } catch (DataStoreException e) { throw e; } catch (Exception e) { throw new DataStoreException(Errors.format(Errors.Keys.CanNotOpen_1, getStorageName()), e); } - final T view = type.cast(value); - addView(type, view); - return view; + return type.cast(view); } /** - * Assuming that {@link #storage} is an instance of {@link InputStream}, resets its position. This method - * is the converse of the marks performed at the beginning of {@link #createChannelDataInput(boolean)}. + * Resets the given view. If the view is an instance of {@link InputStream}, {@link ReadableByteChannel} or + * other objects that may be affected by views operations, this method will reset the storage position. + * The view must have been previously marked by {@link InputStream#mark(int)} or equivalent method. + * + * <p>This method is <strong>not</strong> a substitute for the requirement that users leave the + * {@link #getStorageAs(Class)} return value in the same state as they found it. This method is + * only for handling the cases where using a view has an indirect impact on another view.</p> + * + * <div class="note"><b>Rational:</b> + * {@link DataStoreProvider#probeContent(StorageConnector)} contract requires that implementors reset the + * input stream themselves. However if {@link ChannelDataInput} or {@link InputStreamReader} has been used, + * then the user performed a call to {@link ChannelDataInput#reset()} (for instance), which did not reseted + * the underlying input stream. So we need to perform the missing {@link InputStream#reset()} here, then + * synchronize the {@code ChannelDataInput} position accordingly.</div> + * + * @param c container of the view to reset, or {@code null} if none. + * @return {@code true} if the given view, after reset, is valid. + * Note that {@link Coupled#view} may be null and valid. + */ + private boolean reset(final Coupled c) throws DataStoreException { + final boolean done; + if (c == null) { + return false; + } else try { + done = c.reset(); + } catch (IOException e) { + throw new ForwardOnlyStorageException(Resources.format( + Resources.Keys.StreamIsReadOnce_1, getStorageName()), e); + } + if (done) { + c.invalidateSources(); + c.invalidateUsages(); + } + return c.isValid; + } + + /** + * Resets the root {@link #storage} object. + * + * @throws DataStoreException if the storage can not be reseted. */ - private void resetInputStream() throws IOException { - final ChannelDataInput channel = getView(ChannelDataInput.class); - if (channel != null) { - ((InputStream) storage).reset(); // May throw an exception if mark is unsupported. - channel.buffer.limit(0); // Must be after storage.reset(). - channel.setStreamPosition(0); // Must be after buffer.limit(0). + private void reset() throws DataStoreException { + if (views != null && !reset(views.get(null))) { + throw new ForwardOnlyStorageException(Resources.format( + Resources.Keys.StreamIsReadOnce_1, getStorageName())); } } /** * Creates a view for the input as a {@link ChannelDataInput} if possible. - * If the view can not be created, remember that fact in order to avoid new attempts. + * This is also a starting point for {@link #createDataInput()} and {@link #createByteBuffer()}. + * This method is one of the {@link #OPENERS} methods and should be invoked at most once per + * {@code StorageConnector} instance. * * @param asImageInputStream whether the {@code ChannelDataInput} needs to be {@link ChannelImageInputStream} subclass. * @throws IOException if an error occurred while opening a channel for the input. */ - private void createChannelDataInput(final boolean asImageInputStream) throws IOException { + private ChannelDataInput createChannelDataInput(final boolean asImageInputStream) throws IOException, DataStoreException { /* - * Before to try to open an InputStream, mark its position so we can rewind if the user asks for + * Before to try to wrap an InputStream, mark its position so we can rewind if the user asks for * the InputStream directly. We need to reset because ChannelDataInput may have read some bytes. - * Note that if mark is unsupported, the default InputStream.mark() implementation does nothing. - * See above 'resetInputStream()' method. + * Note that if mark is unsupported, the default InputStream.mark(…) implementation does nothing. */ + reset(); if (storage instanceof InputStream) { ((InputStream) storage).mark(DEFAULT_BUFFER_SIZE); } /* * Following method call recognizes ReadableByteChannel, InputStream (with special case for FileInputStream), - * URL, URI, File, Path or other types that may be added in future SIS versions. + * URL, URI, File, Path or other types that may be added in future Apache SIS versions. + * If the given storage is already a ReadableByteChannel, then the factory will return it as-is. */ final ChannelFactory factory = ChannelFactory.prepare(storage, getOption(OptionKey.URL_ENCODING), false, getOption(OptionKey.OPEN_OPTIONS)); - - ChannelDataInput asDataInput = null; - if (factory != null) { - final String name = getStorageName(); - final ReadableByteChannel channel = factory.reader(name); - addViewToClose(channel, storage); - ByteBuffer buffer = getOption(OptionKey.BYTE_BUFFER); - if (buffer == null) { - buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); - } - if (asImageInputStream) { - asDataInput = new ChannelImageInputStream(name, channel, buffer, false); - } else { - asDataInput = new ChannelDataInput(name, channel, buffer, false); - } - addViewToClose(asDataInput, channel); - /* - * Following is an undocumented mechanism for allowing some Apache SIS implementations of DataStore - * to re-open the same channel or input stream another time, typically for re-reading the same data. - */ - if (factory.canOpen()) { - addView(ChannelFactory.class, factory); - } + if (factory == null) { + return null; } - addView(ChannelDataInput.class, asDataInput); + /* + * ChannelDataInput depends on ReadableByteChannel, which itself depends on storage + * (potentially an InputStream). We need to remember this chain in 'Coupled' objects. + */ + final String name = getStorageName(); + final ReadableByteChannel channel = factory.reader(name); + addView(ReadableByteChannel.class, channel, null, factory.isCoupled() ? CASCADE_ON_RESET : 0); + ByteBuffer buffer = getOption(OptionKey.BYTE_BUFFER); // User-supplied buffer. + if (buffer == null) { + buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); // Default buffer if user did not specified any. + } + final ChannelDataInput asDataInput; + if (asImageInputStream) { + asDataInput = new ChannelImageInputStream(name, channel, buffer, false); + } else { + asDataInput = new ChannelDataInput(name, channel, buffer, false); + } + addView(ChannelDataInput.class, asDataInput, ReadableByteChannel.class, CASCADE_ON_RESET); + /* + * Following is an undocumented mechanism for allowing some Apache SIS implementations of DataStore + * to re-open the same channel or input stream another time, typically for re-reading the same data. + */ + if (factory.canOpen()) { + addView(ChannelFactory.class, factory); + } + return asDataInput; } /** @@ -498,62 +945,72 @@ public class StorageConnector implements * data input may imply creating a {@link ByteBuffer}, in which case the buffer will be stored under * the {@code ByteBuffer.class} key together with the {@code DataInput.class} case. * + * <p>This method is one of the {@link #OPENERS} methods and should be invoked at most once per + * {@code StorageConnector} instance.</p> + * * @throws IOException if an error occurred while opening a stream for the input. */ - private void createDataInput() throws IOException { + private DataInput createDataInput() throws IOException, DataStoreException { + /* + * Gets or creates a ChannelImageInputStream instance if possible. We really need that specific + * type because some SIS data stores will want to access directly the channel and the buffer. + * We will fallback on the ImageIO.createImageInputStream(Object) method only in last resort. + */ + Coupled c = getView(ChannelDataInput.class); + final ChannelDataInput in; + if (reset(c)) { + in = (ChannelDataInput) c.view; + } else { + in = createChannelDataInput(true); // May be null. + } final DataInput asDataInput; - if (storage instanceof DataInput) { - asDataInput = (DataInput) storage; + if (in != null) { + c = getView(ChannelDataInput.class); // May have been added by createChannelDataInput(…). + if (in instanceof DataInput) { + asDataInput = (DataInput) in; + } else { + asDataInput = new ChannelImageInputStream(in); // Upgrade existing instance. + c.view = asDataInput; + } + views.put(DataInput.class, c); // Share the same Coupled instance. } else { + reset(); + asDataInput = ImageIO.createImageInputStream(storage); + addView(DataInput.class, asDataInput, null, (byte) (CASCADE_ON_RESET | CASCADE_ON_CLOSE)); /* - * Creates a ChannelImageInputStream instance. We really need that specific type because some - * SIS data stores will want to access directly the channel and the buffer. We will fallback - * on the ImageIO.createImageInputStream(Object) method only in last resort. + * Note: Java Image I/O wrappers for Input/OutputStream do NOT close the underlying streams. + * This is a complication for us. We could mitigate the problem by subclassing the standard + * FileCacheImageInputStream and related classes, but we don't do that for now because this + * code should never be executed for InputStream storage. Instead getChannelDataInput(true) + * should have created a ChannelImageInputStream or ChannelDataInput. */ - if (!views.containsKey(ChannelDataInput.class)) { - createChannelDataInput(true); - } - final ChannelDataInput c = getView(ChannelDataInput.class); - if (c == null) { - asDataInput = ImageIO.createImageInputStream(storage); - addViewToClose(asDataInput, storage); - } else if (c instanceof DataInput) { - asDataInput = (DataInput) c; - // No call to 'addViewToClose' because the instance already exists. - } else { - asDataInput = new ChannelImageInputStream(c); - if (views.put(ChannelDataInput.class, asDataInput) != c) { // Replace the previous instance. - throw new ConcurrentModificationException(); - } - addViewToClose(asDataInput, c.channel); - } } - addView(DataInput.class, asDataInput); + return asDataInput; } /** * Creates a {@link ByteBuffer} from the {@link ChannelDataInput} if possible, or from the * {@link ImageInputStream} otherwise. The buffer will be initialized with an arbitrary amount - * of bytes read from the input. This amount is not sufficient, it can be increased by a call + * of bytes read from the input. If this amount is not sufficient, it can be increased by a call * to {@link #prefetch()}. * + * <p>This method is one of the {@link #OPENERS} methods and should be invoked at most once per + * {@code StorageConnector} instance.</p> + * * @throws IOException if an error occurred while opening a stream for the input. */ - private void createByteBuffer() throws IOException, DataStoreException { + private ByteBuffer createByteBuffer() throws IOException, DataStoreException { /* * First, try to create the ChannelDataInput if it does not already exists. * If successful, this will create a ByteBuffer companion as a side effect. */ - if (!views.containsKey(ChannelDataInput.class)) { - createChannelDataInput(false); - } + final ChannelDataInput c = getStorageAs(ChannelDataInput.class); ByteBuffer asByteBuffer = null; - final ChannelDataInput c = getView(ChannelDataInput.class); if (c != null) { asByteBuffer = c.buffer.asReadOnlyBuffer(); } else { /* - * If no ChannelDataInput has been create by the above code, get the input as an ImageInputStream and + * If no ChannelDataInput has been created by the above code, get the input as an ImageInputStream and * read an arbitrary amount of bytes. Read only a small amount of bytes because, at the contrary of the * buffer created in createChannelDataInput(boolean), the buffer created here is unlikely to be used for * the reading process after the recognition of the file format. @@ -565,13 +1022,14 @@ public class StorageConnector implements final int n = in.read(buffer); in.reset(); if (n >= 1) { + // Can not invoke asReadOnly() because 'prefetch()' need to be able to write in it. asByteBuffer = ByteBuffer.wrap(buffer).order(in.getByteOrder()); asByteBuffer.limit(n); - // Can not invoke asReadOnly() because 'prefetch()' need to be able to write in it. } } } addView(ByteBuffer.class, asByteBuffer); + return asByteBuffer; } /** @@ -587,22 +1045,35 @@ public class StorageConnector implements */ final boolean prefetch() throws DataStoreException { try { - final ChannelDataInput c = getView(ChannelDataInput.class); + /* + * In most Apache SIS data store implementations, we use ChannelDataInput. If the object wrapped + * by ChannelDataInput has not been used directly, then Coupled.isValid should be true. In such + * case, reset(c) does nothing and ChannelDataInput.prefetch() will read new bytes from current + * channel position. Otherwise, a new read operation from the beginning will be required and we + * can only hope that it will read more bytes than last time. + */ + Coupled c = getView(ChannelDataInput.class); if (c != null) { - return c.prefetch() >= 0; + reset(c); // Does nothing is c.isValid is true. + return c.isValid && ((ChannelDataInput) c.view).prefetch() > 0; } /* * The above code is the usual case. The code below this point is the fallback used when only * an ImageInputStream was available. In such case, the ByteBuffer can only be the one created * by the above createByteBuffer() method, which is known to be backed by a writable array. */ - final ImageInputStream input = getView(ImageInputStream.class); - if (input != null) { - final ByteBuffer buffer = getView(ByteBuffer.class); - if (buffer != null) { + c = getView(ImageInputStream.class); + if (reset(c)) { + final ImageInputStream input = (ImageInputStream) c.view; + c = getView(ByteBuffer.class); + if (reset(c)) { // reset(c) as a matter of principle, but (c != null) would have worked. + final ByteBuffer buffer = (ByteBuffer) c.view; final int p = buffer.limit(); + final long mark = input.getStreamPosition(); + input.seek(Math.addExact(mark, p)); final int n = input.read(buffer.array(), p, buffer.capacity() - p); - if (n >= 0) { + input.seek(mark); + if (n > 0) { buffer.limit(p + n); return true; } @@ -615,123 +1086,158 @@ public class StorageConnector implements } /** - * Creates a storage view of the given type if possible, or returns {@code null} otherwise. - * This method is invoked by {@link #getStorageAs(Class)} when first needed, and the result - * is cached by the caller. + * Creates an {@link ImageInputStream} from the {@link DataInput} if possible. This method simply + * casts {@code DataInput} if such cast is allowed. Since {@link #createDataInput()} instantiates + * {@link ChannelImageInputStream}, this cast is usually possible. + * + * <p>This method is one of the {@link #OPENERS} methods and should be invoked at most once per + * {@code StorageConnector} instance.</p> + */ + private ImageInputStream createImageInputStream() throws DataStoreException { + final Class<DataInput> source = DataInput.class; + final DataInput input = getStorageAs(source); + if (input instanceof ImageInputStream) { + views.put(ImageInputStream.class, views.get(source)); // Share the same Coupled instance. + return (ImageInputStream) input; + } else { + addView(ImageInputStream.class, null); // Remember that there is no view. + return null; + } + } + + /** + * Creates an input stream from {@link ReadableByteChannel} if possible, or from {@link ImageInputStream} + * otherwise. * - * @param type the type of the view to create. - * @return the storage as a view of the given type, or {@code null} if no view can be created for the given type. - * @throws IllegalArgumentException if the given {@code type} argument is not a supported type. - * @throws Exception if an error occurred while opening a stream or database connection. + * <p>This method is one of the {@link #OPENERS} methods and should be invoked at most once per + * {@code StorageConnector} instance.</p> */ - private Object createView(final Class<?> type) throws IllegalArgumentException, Exception { - if (type == String.class) { - return IOUtilities.toString(storage); - } - if (type == Connection.class) { - if (storage instanceof Connection) { - return storage; - } - if (storage instanceof DataSource) { - final Connection c = ((DataSource) storage).getConnection(); - addViewToClose(c, storage); - return c; - } + private InputStream createInputStream() throws IOException, DataStoreException { + final Class<DataInput> source = DataInput.class; + final DataInput input = getStorageAs(source); + if (input instanceof InputStream) { + views.put(InputStream.class, views.get(source)); // Share the same Coupled instance. + return (InputStream) input; + } else if (input instanceof ImageInputStream) { + /* + * Wrap the ImageInputStream as an ordinary InputStream. We avoid setting CASCADE_ON_RESET (unless + * reset() needs to propagate further than ImageInputStream) because changes in InputStreamAdapter + * position are immediately reflected by corresponding changes in ImageInputStream position. + */ + final InputStream in = new InputStreamAdapter((ImageInputStream) input); + addView(InputStream.class, in, source, (byte) (getView(source).cascade & CASCADE_ON_RESET)); + return in; + } else { + addView(InputStream.class, null); // Remember that there is no view. return null; } - if (type == ImageInputStream.class) { - final DataInput input = getStorageAs(DataInput.class); - return (input instanceof ImageInputStream) ? input : null; - } - /* - * If the user asked an InputStream, we may return the storage as-is if it was already an InputStream. - * However before doing so, we may need to reset the InputStream position if the stream has been used - * by a ChannelDataInput. - */ - if (type == InputStream.class) { - if (storage instanceof InputStream) { - resetInputStream(); - return storage; - } - final DataInput input = getStorageAs(DataInput.class); - if (input instanceof InputStream) { - return input; - } - if (input instanceof ImageInputStream) { - final InputStream c = new InputStreamAdapter((ImageInputStream) input); - addViewToClose(c, input); - return c; - } + } + + /** + * Creates a character reader if possible. + * + * <p>This method is one of the {@link #OPENERS} methods and should be invoked at most once per + * {@code StorageConnector} instance.</p> + */ + private Reader createReader() throws IOException, DataStoreException { + final InputStream input = getStorageAs(InputStream.class); + if (input == null) { + addView(Reader.class, null); // Remember that there is no view. return null; } - if (type == Reader.class) { - if (storage instanceof Reader) { - return storage; - } - final InputStream input = getStorageAs(InputStream.class); - if (input != null) { - final Charset encoding = getOption(OptionKey.ENCODING); - final Reader c = (encoding != null) ? new InputStreamReader(input, encoding) - : new InputStreamReader(input); - /* - * Current implementation does not wrap the above Reader in a BufferedReader because: - * - * 1) InputStreamReader already uses a buffer internally. - * 2) InputStreamReader does not support mark/reset, which is a desired limitation for now. - * This is because reseting the Reader would not reset the underlying InputStream, which - * would cause other DataStoreProvider.probeContent(…) methods to fail if they try to use - * the InputStream. For now we let the InputStreamReader.mark() to throws an IOException, - * but we may need to provide our own subclass of BufferedReader in a future SIS version - * if mark/reset support is needed here. - */ - addViewToClose(c, input); - return c; - } - return null; + input.mark(DEFAULT_BUFFER_SIZE); + final Charset encoding = getOption(OptionKey.ENCODING); + Reader in = (encoding != null) ? new InputStreamReader(input, encoding) + : new InputStreamReader(input); + in = new LineNumberReader(in); + addView(Reader.class, in, InputStream.class, (byte) (CLEAR_ON_RESET | CASCADE_ON_RESET)); + return in; + } + + /** + * Creates a database connection if possible. + * + * <p>This method is one of the {@link #OPENERS} methods and should be invoked at most once per + * {@code StorageConnector} instance.</p> + */ + private Connection createConnection() throws SQLException { + if (storage instanceof DataSource) { + final Connection c = ((DataSource) storage).getConnection(); + addView(Connection.class, c, null, (byte) 0); + return c; } - return ObjectConverters.convert(storage, type); + return null; } /** - * Adds the given view in the cache. + * Returns the storage as a path if possible, or {@code null} otherwise. + * + * <p>This method is one of the {@link #OPENERS} methods and should be invoked at most once per + * {@code StorageConnector} instance.</p> + */ + private String createString() { + return IOUtilities.toString(storage); + } + + /** + * Adds the given view in the cache, without dependencies. * * @param <T> the compile-time type of the {@code type} argument. * @param type the view type. * @param view the view, or {@code null} if none. */ private <T> void addView(final Class<T> type, final T view) { - if (views.put(type, (view != null) ? view : Void.TYPE) != null) { - throw new ConcurrentModificationException(); - } + addView(type, view, null, (byte) 0); } /** - * Returns the view for the given type from the cache. - * - * @param <T> the compile-time type of the {@code type} argument. - * @param type the view type. - * @return the view, or {@code null} if none. + * Adds the given view in the cache together with information about its dependency. + * For example {@link InputStreamReader} is a wrapper for a {@link InputStream}: read operations + * from the later may change position of the former, and closing the later also close the former. + * + * @param <T> the compile-time type of the {@code type} argument. + * @param type the view type. + * @param view the view, or {@code null} if none. + * @param source the type of input that {@code view} is wrapping, or {@code null} for {@link #storage}. + * @param cascade bitwise combination of {@link #CASCADE_ON_CLOSE}, {@link #CASCADE_ON_RESET} or {@link #CLEAR_ON_RESET}. */ - private <T> T getView(final Class<T> type) { - final Object view = views.get(type); - return (view != Void.TYPE) ? type.cast(view) : null; + private <T> void addView(final Class<T> type, final T view, final Class<?> source, final byte cascade) { + if (views == null) { + views = new IdentityHashMap<>(); + views.put(null, new Coupled(storage)); + } + Coupled c = views.get(type); + if (c == null) { + if (view == storage) { + c = views.get(null); + c.invalidateUsages(); + } else { + c = new Coupled(cascade != 0 ? views.get(source) : null, cascade); + // Newly created objects are not yet used by anyone, so no need to invoke c.invalidateUsages(). + } + views.put(type, c); + } else { + assert c.view == null || c.view == view : c; + assert c.cascade == cascade : cascade; + assert c.wrapperFor == (cascade != 0 ? views.get(source) : null) : c; + c.invalidateUsages(); + } + c.view = view; + c.isValid = true; + c.invalidateSources(); } /** - * Declares that the given {@code input} will need to be closed by the {@link #closeAllExcept(Object)} method. - * The {@code input} argument is always a new instance wrapping, directly or indirectly, the {@link #storage}. - * Callers must specify the wrapped object in the {@code delegate} argument. + * Returns the view for the given type from the cache. + * This method does <strong>not</strong> {@linkplain #reset(Coupled) reset} the view. * - * @param input the newly created object which will need to be closed. - * @param delegate the object wrapped by the given {@code input}. + * @param type the view type, or {@code null} for the {@link #storage} container. + * @return information associated to the given type. May be {@code null} if the view has never been + * requested before. {@link Coupled#view} may be {@code null} if the view has been requested + * and we determined that none can be created. */ - private void addViewToClose(final Object input, final Object delegate) { - if (viewsToClose == null) { - viewsToClose = new IdentityHashMap<>(4); - } - if (viewsToClose.put(input, delegate) != null) { - throw new AssertionError(input); - } + private Coupled getView(final Class<?> type) { + return (views != null) ? views.get(type) : null; } /** @@ -753,18 +1259,54 @@ public class StorageConnector implements * @see DataStoreProvider#open(StorageConnector) */ public void closeAllExcept(final Object view) throws DataStoreException { - final Map<Object,Object> toClose = viewsToClose; - viewsToClose = Collections.emptyMap(); - views = Collections.emptyMap(); - if (toClose == null) { + if (views == null) { + views = Collections.emptyMap(); // For blocking future usage of this StorageConnector instance. if (storage != view && storage instanceof AutoCloseable) try { ((AutoCloseable) storage).close(); + } catch (DataStoreException e) { + throw e; } catch (Exception e) { throw new DataStoreException(e); } return; } /* + * Create a list of all views to close. The boolean value is TRUE if the view should be closed, or FALSE + * if the view should be protected (not closed). FALSE values shall have precedence over TRUE values. + */ + final Map<AutoCloseable,Boolean> toClose = new IdentityHashMap<>(views.size()); + for (Coupled c : views.values()) { + @SuppressWarnings("null") + Object v = c.view; + if (v != view) { + if (v instanceof AutoCloseable) { + toClose.putIfAbsent((AutoCloseable) v, Boolean.TRUE); // Mark 'v' as needing to be closed. + } + } else { + /* + * If there is a view to not close, search for all views that are wrapper for the given view. + * Those wrappers shall not be closed. For example if the caller does not want to close the + * InputStream view, then we shall not close the InputStreamReader wrapper neither. + */ + c.protect(toClose); + do { + v = c.view; + if (v instanceof AutoCloseable) { + toClose.put((AutoCloseable) v, Boolean.FALSE); // Protect 'v' against closing. + } + c = c.wrapperFor; + } while (c != null); + } + } + /* + * Trim the map in order to keep only the views to close. + */ + for (final Iterator<Boolean> it = toClose.values().iterator(); it.hasNext();) { + if (Boolean.FALSE.equals(it.next())) { + it.remove(); + } + } + /* * The "AutoCloseable.close() is not indempotent" problem * ------------------------------------------------------ * We will need a set of objects to close without duplicated values. For example the values associated to the @@ -774,58 +1316,31 @@ public class StorageConnector implements * * Generally speaking, all AutoCloseable instances are not guaranteed to be indempotent because this is not * required by the interface contract. Consequently we must be careful to not invoke the close() method on - * the same instance twice (indirectly or indirectly). - * - * The set of objects to close will be the keys of the 'viewsToClose' map. It can not be the values of the - * 'views' map. + * the same instance twice (indirectly or indirectly). An exception to this rule is ImageInputStream, which + * does not close its underlying stream. Those exceptions are identified by 'cascadeOnClose' set to 'true'. */ - toClose.put(storage, null); - if (view != null) { - /* - * If there is a view to not close, search for all views that are wrapper for the given view. - * Those wrappers shall not be closed. For example if the caller does not want to close the - * InputStream view, then we shall not close the InputStreamReader wrapper neither. - */ - final Queue<Object> deferred = new LinkedList<>(); - Object doNotClose = view; - do { - final Iterator<Map.Entry<Object,Object>> it = toClose.entrySet().iterator(); - while (it.hasNext()) { - final Map.Entry<Object,Object> entry = it.next(); - if (entry.getValue() == doNotClose) { - deferred.add(entry.getKey()); - it.remove(); + if (!toClose.isEmpty()) { + for (Coupled c : views.values()) { + if (!c.cascadeOnClose() && toClose.containsKey(c.view)) { // Keep (do not remove) the "top level" view. + while ((c = c.wrapperFor) != null) { + toClose.remove(c.view); // Remove all views below the "top level" one. + if (c.cascadeOnClose()) break; } } - doNotClose = deferred.poll(); - } while (doNotClose != null); - } - /* - * Remove the view to not close. If that view is a wrapper for an other object, do not close the - * wrapped object neither. Proceed the dependency chain up to the original 'storage' object. - */ - for (Object doNotClose = view; doNotClose != null;) { - doNotClose = toClose.remove(doNotClose); - } - /* - * Remove all wrapped objects. After this loop, only the "top level" objects should remain - * (typically only one object). This block is needed because of the "AutoCloseable.close() - * is not idempotent" issue, otherwise we could have omitted it. - */ - for (final Object delegate : toClose.values().toArray()) { // 'toArray()' is for avoiding ConcurrentModificationException. - toClose.remove(delegate); + } } + views = Collections.emptyMap(); // For blocking future usage of this StorageConnector instance. /* - * Now close all remaining items. If an exception occurs, we will propagate it only after we are - * done closing all items. + * Now close all remaining items. Typically (but not necessarily) there is only one remaining item. + * If an exception occurs, we will propagate it only after we are done closing all items. */ DataStoreException failure = null; - for (final Object c : toClose.keySet()) { - if (c instanceof AutoCloseable) try { - ((AutoCloseable) c).close(); + for (final AutoCloseable c : toClose.keySet()) { + try { + c.close(); } catch (Exception e) { if (failure == null) { - failure = new DataStoreException(e); + failure = (e instanceof DataStoreException) ? (DataStoreException) e : new DataStoreException(e); } else { failure.addSuppressed(e); } @@ -838,15 +1353,25 @@ public class StorageConnector implements /** * Returns a string representation of this {@code StorageConnector} for debugging purpose. + * This string representation is for debugging purpose only and may change in any future version. + * + * @return a string representation of this {@code StorageConnector} for debugging purpose. */ @Debug @Override public String toString() { - final StringBuilder buffer = new StringBuilder(40); - buffer.append(Classes.getShortClassName(this)).append("[“").append(getStorageName()).append('”'); + final TreeTable table = new DefaultTreeTable(TableColumn.NAME, TableColumn.VALUE); + final TreeTable.Node root = table.getRoot(); + root.setValue(TableColumn.NAME, Classes.getShortClassName(this)); + root.setValue(TableColumn.VALUE, getStorageName()); if (options != null) { - buffer.append(", options=").append(options); + final TreeTable.Node op = root.newChild(); + op.setValue(TableColumn.NAME, "options"); + op.setValue(TableColumn.VALUE, options); + } + if (views != null) { + views.get(null).append(root.newChild(), views); } - return buffer.append(']').toString(); + return table.toString(); } }
Modified: sis/branches/JDK9/storage/sis-storage/src/main/java/org/apache/sis/storage/UnsupportedStorageException.java URL: http://svn.apache.org/viewvc/sis/branches/JDK9/storage/sis-storage/src/main/java/org/apache/sis/storage/UnsupportedStorageException.java?rev=1807624&r1=1807623&r2=1807624&view=diff ============================================================================== --- sis/branches/JDK9/storage/sis-storage/src/main/java/org/apache/sis/storage/UnsupportedStorageException.java [UTF-8] (original) +++ sis/branches/JDK9/storage/sis-storage/src/main/java/org/apache/sis/storage/UnsupportedStorageException.java [UTF-8] Thu Sep 7 15:47:24 2017 @@ -25,15 +25,15 @@ import org.apache.sis.internal.storage.i /** * Thrown when no {@link DataStoreProvider} is found for a given storage object. - * May also be thrown if a {@code DataStore} is instantiated directly (without {@code DataStoreProvider} - * for verifying the input) but the data store can not handle the given input or output object. + * May also be thrown if a {@code DataStore} is instantiated directly but the data store + * can not handle the given input or output object. * * @author Martin Desruisseaux (Geomatys) * @version 0.8 * @since 0.4 * @module */ -public class UnsupportedStorageException extends DataStoreException { +public class UnsupportedStorageException extends IllegalOpenParameterException { /** * For cross-version compatibility. */ Modified: sis/branches/JDK9/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java URL: http://svn.apache.org/viewvc/sis/branches/JDK9/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java?rev=1807624&r1=1807623&r2=1807624&view=diff ============================================================================== --- sis/branches/JDK9/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java [UTF-8] (original) +++ sis/branches/JDK9/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java [UTF-8] Thu Sep 7 15:47:24 2017 @@ -17,6 +17,7 @@ package org.apache.sis.storage; import org.opengis.metadata.Metadata; +import org.opengis.parameter.ParameterValueGroup; /** @@ -49,12 +50,12 @@ final strictfp class DataStoreMock exten } @Override - public Metadata getMetadata() { + public ParameterValueGroup getOpenParameters() { return null; } @Override - public Resource getRootResource() throws DataStoreException { + public Metadata getMetadata() { return null; } Modified: sis/branches/JDK9/storage/sis-storage/src/test/java/org/apache/sis/storage/StorageConnectorTest.java URL: http://svn.apache.org/viewvc/sis/branches/JDK9/storage/sis-storage/src/test/java/org/apache/sis/storage/StorageConnectorTest.java?rev=1807624&r1=1807623&r2=1807624&view=diff ============================================================================== --- sis/branches/JDK9/storage/sis-storage/src/test/java/org/apache/sis/storage/StorageConnectorTest.java [UTF-8] (original) +++ sis/branches/JDK9/storage/sis-storage/src/test/java/org/apache/sis/storage/StorageConnectorTest.java [UTF-8] Thu Sep 7 15:47:24 2017 @@ -16,16 +16,19 @@ */ package org.apache.sis.storage; +import java.net.URI; import java.io.DataInput; import java.io.InputStream; import java.io.Reader; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.nio.channels.ReadableByteChannel; -import javax.imageio.ImageIO; import javax.imageio.stream.ImageInputStream; +import javax.imageio.ImageIO; import java.sql.Connection; import org.apache.sis.setup.OptionKey; +import org.apache.sis.util.UnconvertibleObjectException; import org.apache.sis.internal.storage.io.ChannelDataInput; import org.apache.sis.internal.storage.io.ChannelImageInputStream; import org.apache.sis.internal.storage.io.InputStreamAdapter; @@ -46,24 +49,33 @@ import static org.opengis.test.Assert.*; * @since 0.3 * @module */ -@SuppressWarnings("OverlyStrongTypeCast") @DependsOn(org.apache.sis.internal.storage.io.ChannelImageInputStreamTest.class) public final strictfp class StorageConnectorTest extends TestCase { /** - * The magic number of Java class files, used for verifying the content of our test file. + * Name of the test file, in the same directory than this {@code StorageConnectorTest} file. + */ + private static final String FILENAME = "Any.txt"; + + /** + * Beginning of the first sentence in {@value #FILENAME}. */ - private static final int MAGIC_NUMBER = 0xCAFEBABE; + private static final String FIRST_SENTENCE = "The purpose of this file"; /** - * Creates the instance to test. This method uses the {@code StorageConnectorTest} compiled - * class file as the resource to test. The resource can be provided either as a URL or as a stream. + * The 4 first characters of {@link #FIRST_SENTENCE}, encoded as an integer. + */ + private static final int MAGIC_NUMBER = ('T' << 24) | ('h' << 16) | ('e' << 8) | ' '; + + /** + * Creates the instance to test. This method uses the {@code "test.txt"} ASCII file as + * the resource to test. The resource can be provided either as a URL or as a stream. */ private static StorageConnector create(final boolean asStream) { final Class<?> c = StorageConnectorTest.class; - final String name = c.getSimpleName() + ".class"; - final Object storage = asStream ? c.getResourceAsStream(name) : c.getResource(name); + final Object storage = asStream ? c.getResourceAsStream(FILENAME) : c.getResource(FILENAME); assertNotNull(storage); final StorageConnector connector = new StorageConnector(storage); + connector.setOption(OptionKey.ENCODING, StandardCharsets.US_ASCII); connector.setOption(OptionKey.URL_ENCODING, "UTF-8"); return connector; } @@ -74,7 +86,7 @@ public final strictfp class StorageConne @Test public void testGetStorageName() { final StorageConnector c = create(false); - assertEquals("StorageConnectorTest.class", c.getStorageName()); + assertEquals(FILENAME, c.getStorageName()); } /** @@ -83,7 +95,7 @@ public final strictfp class StorageConne @Test public void testGetExtension() { final StorageConnector c = create(false); - assertEquals("class", c.getFileExtension()); + assertEquals("txt", c.getFileExtension()); } /** @@ -95,7 +107,7 @@ public final strictfp class StorageConne @Test public void testGetAsString() throws DataStoreException, IOException { final StorageConnector c = create(false); - assertTrue(c.getStorageAs(String.class).endsWith("org/apache/sis/storage/StorageConnectorTest.class")); + assertTrue(c.getStorageAs(String.class).endsWith("org/apache/sis/storage/" + FILENAME)); } /** @@ -129,7 +141,7 @@ public final strictfp class StorageConne final StorageConnector connection = create(asStream); final DataInput input = connection.getStorageAs(DataInput.class); assertSame("Value shall be cached.", input, connection.getStorageAs(DataInput.class)); - assertInstanceOf("Needs the SIS implementation", ChannelImageInputStream.class, input); + assertInstanceOf("Needs the SIS implementation.", ChannelImageInputStream.class, input); assertSame("Instance shall be shared.", input, connection.getStorageAs(ChannelDataInput.class)); /* * Reads a single integer for checking that the stream is at the right position, then close the stream. @@ -137,7 +149,7 @@ public final strictfp class StorageConne */ final ReadableByteChannel channel = ((ChannelImageInputStream) input).channel; assertTrue("channel.isOpen()", channel.isOpen()); - assertEquals(MAGIC_NUMBER, input.readInt()); + assertEquals("First 4 bytes", MAGIC_NUMBER, input.readInt()); connection.closeAllExcept(null); assertFalse("channel.isOpen()", channel.isOpen()); } @@ -150,6 +162,7 @@ public final strictfp class StorageConne * @throws IOException if an error occurred while reading the test file. */ @Test + @DependsOnMethod("testGetAsDataInputFromURL") public void testGetAsImageInputStream() throws DataStoreException, IOException { final StorageConnector connection = create(false); final ImageInputStream in = connection.getStorageAs(ImageInputStream.class); @@ -206,9 +219,9 @@ public final strictfp class StorageConne final InputStream in = connection.getStorageAs(InputStream.class); assertNotSame(connection.getStorage(), in); assertSame("Expected cached value.", in, connection.getStorageAs(InputStream.class)); - assertInstanceOf("Expected Channel backend", InputStreamAdapter.class, in); + assertInstanceOf("Expected Channel backend.", InputStreamAdapter.class, in); final ImageInputStream input = ((InputStreamAdapter) in).input; - assertInstanceOf("Expected Channel backend", ChannelImageInputStream.class, input); + assertInstanceOf("Expected Channel backend.", ChannelImageInputStream.class, input); assertSame(input, connection.getStorageAs(DataInput.class)); assertSame(input, connection.getStorageAs(ImageInputStream.class)); @@ -225,11 +238,34 @@ public final strictfp class StorageConne * @throws IOException if an error occurred while reading the test file. */ @Test - @DependsOnMethod("testGetAsInputStream") + @DependsOnMethod({"testGetAsInputStream", "testGetAsDataInputFromStream"}) public void testGetAsReader() throws DataStoreException, IOException { final StorageConnector connection = create(true); final Reader in = connection.getStorageAs(Reader.class); + final char[] expected = FIRST_SENTENCE.toCharArray(); + final char[] actual = new char[expected.length]; + in.mark(1000); + assertEquals("Number of characters read.", expected.length, in.read(actual)); + assertArrayEquals("First sentence.", expected, actual); assertSame("Expected cached value.", in, connection.getStorageAs(Reader.class)); + in.reset(); + /* + * Open as an ImageInputStream and verify that reading starts from the beginning. + * This operation should force StorageConnector to discard the previous Reader. + */ + final ImageInputStream im = connection.getStorageAs(ImageInputStream.class); + assertInstanceOf("Needs the SIS implementation.", ChannelImageInputStream.class, im); + im.mark(); + assertEquals("First 4 bytes", MAGIC_NUMBER, im.readInt()); + im.reset(); + /* + * Get a reader again. It should be a new one, in order to read from the beginning again. + */ + final Reader in2 = connection.getStorageAs(Reader.class); + assertNotSame("Expected a new Reader instance.", in, in2); + assertEquals("Number of characters read.", expected.length, in.read(actual)); + assertArrayEquals("First sentence.", expected, actual); + assertSame("Expected cached value.", in2, connection.getStorageAs(Reader.class)); connection.closeAllExcept(null); } @@ -315,6 +351,28 @@ public final strictfp class StorageConne connection.closeAllExcept(null); } + /** + * Verifies that {@link StorageConnector#getStorageAs(Class)} returns {@code null} for unavailable + * target classes, and throws an exception for illegal target classes. + * + * @throws DataStoreException if an error occurred while using the storage connector. + */ + @Test + public void testGetInvalidObject() throws DataStoreException { + final StorageConnector connection = create(true); + assertNotNull("getStorageAs(InputStream.class)", connection.getStorageAs(InputStream.class)); + assertNull ("getStorageAs(URI.class)", connection.getStorageAs(URI.class)); + assertNull ("getStorageAs(String.class)", connection.getStorageAs(String.class)); + try { + connection.getStorageAs(Float.class); // Any unconvertible type. + fail("Should not have accepted Float.class"); + } catch (UnconvertibleObjectException e) { + final String message = e.getMessage(); + assertTrue(message, message.contains("Float")); + } + connection.closeAllExcept(null); + } + /** * Tests the {@link StorageConnector#closeAllExcept(Object)} method. * Modified: sis/branches/JDK9/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Link.java URL: http://svn.apache.org/viewvc/sis/branches/JDK9/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Link.java?rev=1807624&r1=1807623&r2=1807624&view=diff ============================================================================== --- sis/branches/JDK9/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Link.java [UTF-8] (original) +++ sis/branches/JDK9/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Link.java [UTF-8] Thu Sep 7 15:47:24 2017 @@ -116,7 +116,7 @@ public final class Link implements Onlin * </link> * } * - * If we fail to convert the text to an URI, we will leave the object state as-is. + * If we fail to convert the text to a URI, we will leave the object state as-is. */ final void afterUnmarshal(Unmarshaller um, Object parent) { if (uri == null && text != null) { Modified: sis/branches/JDK9/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java URL: http://svn.apache.org/viewvc/sis/branches/JDK9/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java?rev=1807624&r1=1807623&r2=1807624&view=diff ============================================================================== --- sis/branches/JDK9/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java [UTF-8] (original) +++ sis/branches/JDK9/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java [UTF-8] Thu Sep 7 15:47:24 2017 @@ -19,15 +19,17 @@ package org.apache.sis.internal.storage. import java.net.URISyntaxException; import org.opengis.util.NameFactory; import org.opengis.util.FactoryException; +import org.opengis.geometry.Envelope; import org.opengis.metadata.Metadata; import org.opengis.metadata.distribution.Format; -import org.apache.sis.storage.Resource; +import org.apache.sis.storage.FeatureSet; import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.storage.ConcurrentReadException; import org.apache.sis.storage.IllegalNameException; import org.apache.sis.internal.system.DefaultFactories; +import org.apache.sis.internal.storage.AbstractDataSet; import org.apache.sis.internal.storage.xml.stream.StaxDataStore; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.ArgumentChecks; @@ -56,7 +58,7 @@ import org.opengis.feature.FeatureType; * @since 0.8 * @module */ -public final class Store extends StaxDataStore { +public final class Store extends StaxDataStore implements FeatureSet { /** * Version of the GPX file, or {@code null} if unknown. */ @@ -168,13 +170,24 @@ public final class Store extends StaxDat } /** - * Returns the {@code FeatureSet} from which all features in this data store can be accessed. + * Returns the spatio-temporal envelope of this resource. * - * @return the starting point of all features in this data store. + * @return the spatio-temporal resource extent. + * @throws DataStoreException if an error occurred while reading or computing the envelope. */ @Override - public Resource getRootResource() { - return new FeatureAccess(this, listeners); + public Envelope getEnvelope() throws DataStoreException { + return AbstractDataSet.envelope(getMetadata()); + } + + /** + * Returns the base type of all GPX types. + * + * @return base type of all GPX types. + */ + @Override + public FeatureType getType() { + return types.parent; } /** @@ -196,10 +209,12 @@ public final class Store extends StaxDat /** * Returns the stream of features. * + * @param parallel ignored in current implementation. * @return a stream over all features in the XML file. * @throws DataStoreException if an error occurred while creating the feature stream. */ - final synchronized Stream<Feature> features() throws DataStoreException { + @Override + public final synchronized Stream<Feature> features(boolean parallel) throws DataStoreException { Reader r = reader; reader = null; if (r == null) try {
