sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1817597 [18/19] - in /sis/branches/ISO-19115-3: ./ application/ application/sis-console/ application/sis-console/src/main/artifact/ application/sis-console/src/main/artifact/lib/ application/sis-console/src/main/artifact/lib/darwin/ applic...
Date Sat, 09 Dec 2017 10:57:47 GMT
Modified: sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
URL: http://svn.apache.org/viewvc/sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java?rev=1817597&r1=1817596&r2=1817597&view=diff
==============================================================================
--- sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java [UTF-8] (original)
+++ sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java [UTF-8] Sat Dec  9 10:57:44 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.storage.io.RewindableLineReader;
+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;
 
 
@@ -87,6 +98,11 @@ public class StorageConnector implements
     /**
      * The default size of the {@link ByteBuffer} to be created.
      * Users can override this value by providing a value for {@link OptionKey#BYTE_BUFFER}.
+     * Note that it usually don't need to be very large, since {@link RewindableLineReader}
+     * will have its own buffer (which may be larger) and {@link ChannelDataInput} methods
+     * writing in existing destination arrays will bypass the buffer.
+     *
+     * @see RewindableLineReader#BUFFER_SIZE
      */
     static final int DEFAULT_BUFFER_SIZE = 4096;
 
@@ -97,6 +113,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 +216,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,16 +587,20 @@ 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;
     }
 
     /**
-     * Returns a short name of the input/output object. The default implementation performs
-     * the following choices based on the type of the {@linkplain #getStorage() storage} object:
+     * Returns a short name of the input/output object. For example if the storage is a file,
+     * this method returns the filename without the path (but including the file extension).
+     * The default implementation performs the following choices based on the type of the
+     * {@linkplain #getStorage() storage} object:
      *
      * <ul>
      *   <li>For {@link java.nio.file.Path}, {@link java.io.File}, {@link java.net.URI} or {@link java.net.URL}
@@ -283,6 +659,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 +734,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 +762,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;
+        }
+        /*
+         * 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.readable(name, null);
+        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);
+        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 +952,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 +1029,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 +1052,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 +1093,155 @@ 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 Reader in = new RewindableLineReader(input, getOption(OptionKey.ENCODING));
+        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 +1263,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 +1320,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 +1357,26 @@ 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);
+        }
+        final Coupled c = getView(null);
+        if (c != null) {
+            c.append(root.newChild(), views);
         }
-        return buffer.append(']').toString();
+        return table.toString();
     }
 }

Modified: sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/UnsupportedStorageException.java
URL: http://svn.apache.org/viewvc/sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/UnsupportedStorageException.java?rev=1817597&r1=1817596&r2=1817597&view=diff
==============================================================================
--- sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/UnsupportedStorageException.java [UTF-8] (original)
+++ sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/UnsupportedStorageException.java [UTF-8] Sat Dec  9 10:57:44 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/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/package-info.java
URL: http://svn.apache.org/viewvc/sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/package-info.java?rev=1817597&r1=1817596&r2=1817597&view=diff
==============================================================================
--- sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/package-info.java [UTF-8] (original)
+++ sis/branches/ISO-19115-3/storage/sis-storage/src/main/java/org/apache/sis/storage/package-info.java [UTF-8] Sat Dec  9 10:57:44 2017
@@ -26,7 +26,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.3
  * @module
  */

Modified: sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/csv/StoreTest.java
URL: http://svn.apache.org/viewvc/sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/csv/StoreTest.java?rev=1817597&r1=1817596&r2=1817597&view=diff
==============================================================================
--- sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/csv/StoreTest.java [UTF-8] (original)
+++ sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/csv/StoreTest.java [UTF-8] Sat Dec  9 10:57:44 2017
@@ -22,8 +22,10 @@ import java.io.StringReader;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.extent.Extent;
 import org.opengis.metadata.extent.GeographicBoundingBox;
+import org.apache.sis.feature.FoliationRepresentation;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataOptionKey;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 import com.esri.core.geometry.Point2D;
@@ -46,7 +48,7 @@ import org.opengis.feature.AttributeType
  * Tests {@link Store}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.7
  * @module
  */
@@ -79,6 +81,15 @@ public final strictfp class StoreTest ex
     }
 
     /**
+     * Opens a CSV store on the test data for reading the lines as-is, without assembling them in a single trajectory.
+     */
+    private static Store open() throws DataStoreException {
+        StorageConnector connector = new StorageConnector(testData());
+        connector.setOption(DataOptionKey.FOLIATION_REPRESENTATION, FoliationRepresentation.FRAGMENTED);
+        return new Store(null, connector);
+    }
+
+    /**
      * Tests {@link Store#getMetadata()}.
      *
      * @throws DataStoreException if an error occurred while parsing the data.
@@ -86,7 +97,7 @@ public final strictfp class StoreTest ex
     @Test
     public void testGetMetadata() throws DataStoreException {
         final Metadata metadata;
-        try (Store store = new Store(null, new StorageConnector(testData()), true)) {
+        try (Store store = open()) {
             metadata = store.getMetadata();
         }
         final Extent extent = getSingleton(getSingleton(metadata.getIdentificationInfo()).getExtents());
@@ -105,7 +116,7 @@ public final strictfp class StoreTest ex
      */
     @Test
     public void testStaticFeatures() throws DataStoreException {
-        try (Store store = new Store(null, new StorageConnector(testData()), true)) {
+        try (Store store = open()) {
             verifyFeatureType(store.featureType, double[].class, 1);
             assertEquals("foliation", Foliation.TIME, store.foliation);
             final Iterator<Feature> it = store.features(false).iterator();
@@ -134,7 +145,7 @@ public final strictfp class StoreTest ex
     @Test
     public void testMovingFeatures() throws DataStoreException {
         isMovingFeature = true;
-        try (Store store = new Store(null, new StorageConnector(testData()), false)) {
+        try (Store store = new Store(null, new StorageConnector(testData()))) {
             verifyFeatureType(store.featureType, Polyline.class, Integer.MAX_VALUE);
             assertEquals("foliation", Foliation.TIME, store.foliation);
             final Iterator<Feature> it = store.features(false).iterator();

Modified: sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataInputTest.java
URL: http://svn.apache.org/viewvc/sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataInputTest.java?rev=1817597&r1=1817596&r2=1817597&view=diff
==============================================================================
--- sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataInputTest.java [UTF-8] (original)
+++ sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataInputTest.java [UTF-8] Sat Dec  9 10:57:44 2017
@@ -21,6 +21,7 @@ import java.io.DataInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
@@ -134,7 +135,7 @@ public final strictfp class ChannelDataI
     }
 
     /**
-     * Tests the {@link ChannelDataInput#readString(int, String)} method.
+     * Tests the {@link ChannelDataInput#readString(int, Charset)} method.
      *
      * @throws IOException should never happen since we read and write in memory only.
      */
@@ -146,7 +147,7 @@ public final strictfp class ChannelDataI
         final ChannelDataInput input = new ChannelDataInput("testReadString",
                 new DripByteChannel(array, random, 1, 32),
                 ByteBuffer.allocate(array.length + 4), false);
-        assertEquals(expected, input.readString(array.length, "UTF-8"));
+        assertEquals(expected, input.readString(array.length, StandardCharsets.UTF_8));
         assertFalse(input.buffer.hasRemaining());
     }
 

Modified: sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/IOUtilitiesTest.java
URL: http://svn.apache.org/viewvc/sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/IOUtilitiesTest.java?rev=1817597&r1=1817596&r2=1817597&view=diff
==============================================================================
--- sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/IOUtilitiesTest.java [UTF-8] (original)
+++ sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/IOUtilitiesTest.java [UTF-8] Sat Dec  9 10:57:44 2017
@@ -35,7 +35,7 @@ import static org.junit.Assert.*;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 0.4
+ * @version 0.8
  * @since   0.3
  * @module
  */
@@ -112,6 +112,17 @@ public final strictfp class IOUtilitiesT
     }
 
     /**
+     * Tests {@link IOUtilities#filenameWithoutExtension(String)}.
+     */
+    @Test
+    public void testFilenameWithoutExtension() {
+        assertEquals("Map",     IOUtilities.filenameWithoutExtension("/Users/name/Map.png"));
+        assertEquals("Map",     IOUtilities.filenameWithoutExtension("/Users/name/Map"));
+        assertEquals("Map",     IOUtilities.filenameWithoutExtension("Map.png"));
+        assertEquals(".hidden", IOUtilities.filenameWithoutExtension("/Users/name/.hidden"));
+    }
+
+    /**
      * Tests {@link IOUtilities#encodeURI(String)}.
      */
     @Test

Modified: sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java
URL: http://svn.apache.org/viewvc/sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java?rev=1817597&r1=1817596&r2=1817597&view=diff
==============================================================================
--- sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java [UTF-8] (original)
+++ sis/branches/ISO-19115-3/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java [UTF-8] Sat Dec  9 10:57:44 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;
     }
 



Mime
View raw message