This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 489e03e521e673da39cebae85071dd5b612eabfc
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Mon Jan 3 06:01:58 2022 +0100

    Port the main idea behind the "refactor/strict_storage_connector" branch 
(from by Alexis).
    A `DataStoreProvider.probeContet(…)` method is provided for testing a file 
with mark/reset managed automatically.
    
    Also replace `InvalidMarkException` (which is for buffer) by `IOException` 
if the stream has no mark.
---
 .../org/apache/sis/internal/storage/Resources.java |  10 +
 .../sis/internal/storage/Resources.properties      |   2 +
 .../sis/internal/storage/Resources_fr.properties   |   2 +
 .../sis/internal/storage/io/ChannelData.java       |  12 +-
 .../internal/storage/io/InputStreamAdapter.java    |  54 +++--
 .../apache/sis/internal/storage/io/Markable.java   |  12 +-
 .../internal/storage/io/OutputStreamAdapter.java   |   2 +-
 .../org/apache/sis/storage/DataStoreProvider.java  | 235 ++++++++++++++++++---
 .../org/apache/sis/storage/ProbeInputStream.java   |  87 ++++++++
 .../java/org/apache/sis/storage/ProbeReader.java   |  86 ++++++++
 .../org/apache/sis/storage/StorageConnector.java   |  66 +++++-
 .../internal/storage/io/ChannelDataOutputTest.java |   4 +-
 .../storage/io/ChannelImageOutputStreamTest.java   |   4 +-
 .../apache/sis/storage/DataStoreProviderTest.java  | 202 ++++++++++++++++++
 .../apache/sis/storage/StorageConnectorTest.java   |  86 +++++++-
 .../apache/sis/test/suite/StorageTestSuite.java    |   4 +-
 16 files changed, 789 insertions(+), 79 deletions(-)

diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
index bb4515d..8fb8ce2 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
@@ -262,6 +262,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short LoadedGridCoverage_6 = 59;
 
         /**
+         * Marks are not supported on “{0}” stream.
+         */
+        public static final short MarkNotSupported_1 = 62;
+
+        /**
          * Resource “{0}” does not have an identifier.
          */
         public static final short MissingResourceIdentifier_1 = 42;
@@ -322,6 +327,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short StoreIsReadOnly = 28;
 
         /**
+         * Stream has not mark.
+         */
+        public static final short StreamHasNoMark = 63;
+
+        /**
          * Can not move backward in the “{0}” stream.
          */
         public static final short StreamIsForwardOnly_1 = 13;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
index 92372c0..f825e40 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
@@ -59,6 +59,7 @@ InconsistentNameComponents_2      = Components of the 
\u201c{1}\u201d name are i
 InvalidExpression_2               = Invalid or unsupported \u201c{1}\u201d 
expression at index {0}.
 InvalidSampleDimensionIndex_2     = Sample dimension index {1} is invalid. 
Expected an index from 0 to {0} inclusive.
 LoadedGridCoverage_6              = Loaded grid coverage between {1} \u2013 
{2} and {3} \u2013 {4} from file \u201c{0}\u201d in {5} seconds.
+MarkNotSupported_1                = Marks are not supported on \u201c{0}\u201d 
stream.
 MissingResourceIdentifier_1       = Resource \u201c{0}\u201d does not have an 
identifier.
 MissingSchemeInURI_1              = Missing scheme in \u201c{0}\u201d URI.
 NoSuchResourceDirectory_1         = No directory of resources found at 
\u201c{0}\u201d.
@@ -71,6 +72,7 @@ ResourceNotFound_2                = No resource found for the 
\u201c{1}\u201d id
 ShallBeDeclaredBefore_2           = The \u201c{1}\u201d element must be 
declared before \u201c{0}\u201d.
 SharedDirectory_1                 = The \u201c{0}\u201d directory is used more 
than once because of symbolic links.
 StoreIsReadOnly                   = Write operations are not supported.
+StreamHasNoMark                   = Stream has not mark.
 StreamIsForwardOnly_1             = Can not move backward in the 
\u201c{0}\u201d stream.
 StreamIsNotReadable_1             = Stream \u201c{0}\u201d is not readable.
 StreamIsNotWritable_1             = Stream \u201c{0}\u201d is not writable.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
index 862f835..9f77b1c 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
@@ -64,6 +64,7 @@ InvalidExpression_2               = Expression 
\u00ab\u202f{1}\u202f\u00bb inval
 InvalidSampleDimensionIndex_2     = L\u2019index de dimension 
d\u2019\u00e9chantillonnage {1} est invalide. On attendait un index de 0 \u00e0 
{0} inclusif.
 InconsistentNameComponents_2      = Les \u00e9l\u00e9ments qui composent le 
nom \u00ab\u202f{1}\u202f\u00bb ne sont pas coh\u00e9rents avec ceux du nom qui 
avait \u00e9t\u00e9 pr\u00e9c\u00e9demment li\u00e9 dans les donn\u00e9es de 
\u00ab\u202f{0}\u202f\u00bb.
 LoadedGridCoverage_6              = Lecture d\u2019une couverture de 
donn\u00e9es entre {1} \u2013 {2} et {3} \u2013 {4} \u00e0 partir du fichier 
\u00ab\u202f{0}\u202f\u00bb en {5} secondes.
+MarkNotSupported_1                = Les marques ne sont pas support\u00e9es 
sur le flux \u00ab\u202f{0}\u202f\u00bb.
 MissingResourceIdentifier_1       = La ressource \u00ab\u202f{0}\u202f\u00bb 
n\u2019a pas d\u2019identifiant.
 MissingSchemeInURI_1              = Il manque le sch\u00e9ma dans l\u2019URI 
\u00ab\u202f{0}\u202f\u00bb.
 NoSuchResourceDirectory_1         = Aucun r\u00e9pertoire de ressources 
n\u2019a \u00e9t\u00e9 trouv\u00e9 \u00e0 l\u2019emplacement 
\u00ab\u202f{0}\u202f\u00bb.
@@ -76,6 +77,7 @@ ResourceNotFound_2                = Aucune ressource n\u2019a 
\u00e9t\u00e9 trou
 ShallBeDeclaredBefore_2           = L\u2019\u00e9l\u00e9ment 
\u00ab\u202f{1}\u202f\u00bb doit \u00eatre d\u00e9clar\u00e9 avant 
\u00ab\u202f{0}\u202f\u00bb.
 SharedDirectory_1                 = Le r\u00e9pertoire 
\u00ab\u202f{0}\u202f\u00bb est utilis\u00e9 plus d\u2019une fois \u00e0 cause 
des liens symboliques.
 StoreIsReadOnly                   = Les op\u00e9rations d\u2019\u00e9criture 
ne sont pas support\u00e9es.
+StreamHasNoMark                   = Le flux n\u2019a pas de marque.
 StreamIsForwardOnly_1             = Ne peut pas reculer dans le flux de 
donn\u00e9es \u00ab\u202f{0}\u202f\u00bb.
 StreamIsNotReadable_1             = Les donn\u00e9es de 
\u00ab\u202f{0}\u202f\u00bb ne sont pas accessibles en lecture.
 StreamIsNotWritable_1             = Le flux de donn\u00e9es 
\u00ab\u202f{0}\u202f\u00bb ne g\u00e8re pas les \u00e9critures.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
index 4bf35b6..36a1caf 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
@@ -18,10 +18,10 @@ package org.apache.sis.internal.storage.io;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.nio.InvalidMarkException;
 import java.nio.channels.Channel;
 import java.nio.channels.SeekableByteChannel;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.storage.Resources;
 
 import static org.apache.sis.util.ArgumentChecks.ensureBetween;
 
@@ -314,17 +314,17 @@ public abstract class ChannelData implements Markable {
      *
      * <h4>Departure from Image I/O specification</h4>
      * The {@link javax.imageio.stream.ImageInputStream#reset()} contract 
specifies that if there is no matching mark,
-     * then this method shall do nothing. This method throws {@link 
InvalidMarkException} instead; silently ignoring
-     * the mismatch is considered too dangerous. This is a departure from 
{@code ImageInputStream} but is consistent
-     * with {@link java.io.InputStream#reset()} contract.
+     * then this method shall do nothing. This method throws {@link 
IOException} instead; silently ignoring mismatch
+     * is considered too dangerous. This is a departure from {@code 
ImageInputStream} but is consistent with
+     * {@link java.io.InputStream#reset()} contract.
      *
-     * @throws IOException if an I/O error occurs.
+     * @throws IOException if this stream can not move to the last mark 
position.
      */
     @Override
     public final void reset() throws IOException {
         final Mark m = mark;
         if (m == null) {
-            throw new InvalidMarkException();
+            throw new 
IOException(Resources.format(Resources.Keys.StreamHasNoMark));
         }
         mark = m.next;
         seek(m.position);
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/InputStreamAdapter.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/InputStreamAdapter.java
index 5cdb9c6..76ee111 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/InputStreamAdapter.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/InputStreamAdapter.java
@@ -20,6 +20,7 @@ import java.io.InputStream;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import javax.imageio.stream.ImageInputStream;
+import org.apache.sis.internal.storage.Resources;
 
 
 /**
@@ -34,7 +35,7 @@ import javax.imageio.stream.ImageInputStream;
  * explicit synchronization lock (contrarily to {@link java.io.Reader}.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.2
  *
  * @see OutputStreamAdapter
  *
@@ -50,11 +51,19 @@ public final class InputStreamAdapter extends InputStream 
implements Markable {
     public final ImageInputStream input;
 
     /**
-     * Position of the last mark created by {@link #mark(int)}, or the file 
beginning if there is no mark.
+     * Position of the last mark created by {@link #mark(int)}. Undefined if 
{@link #markIndex} is negative.
      */
     private long markPosition;
 
     /**
+     * Value of {@link #nestedMarks} at the time when {@link #markPosition} 
has been set, or -1 if none.
+     * Used for differentiating the (single) mark created by {@link 
#mark(int)} from the (possibly many)
+     * marks created by {@link #mark()}. This complexity exists because {@link 
#reset()} must comply with
+     * two inconsistent {@code mark(…)} method contracts.
+     */
+    private int markIndex;
+
+    /**
      * Count of marks created by {@link #mark()}, not counting the mark 
created by {@link #mark(int)}.
      * We have to keep this count ourselves because {@link 
ImageInputStream#reset()} does nothing if
      * there is no mark, and provides no API for letting us know if {@code 
reset()} worked.
@@ -77,7 +86,7 @@ public final class InputStreamAdapter extends InputStream 
implements Markable {
     public InputStreamAdapter(final ImageInputStream input) throws IOException 
{
         assert !(input instanceof InputStream);
         this.input = input;
-        markPosition = input.getStreamPosition();
+        markIndex = -1;
     }
 
     /**
@@ -135,7 +144,7 @@ public final class InputStreamAdapter extends InputStream 
implements Markable {
     }
 
     /**
-     * Discards all previous marks and marks the current position in this 
input stream.
+     * Discards the previous mark created by {@code mark(int)} and marks the 
current stream position.
      * This method is part of {@link InputStream} API, where only one mark can 
be set and multiple
      * calls to {@code reset()} move to the same position until {@code 
mark(int)} is invoked again.
      *
@@ -146,8 +155,10 @@ public final class InputStreamAdapter extends InputStream 
implements Markable {
     public synchronized void mark(final int readlimit) {
         try {
             markPosition = input.getStreamPosition();
-            input.flushBefore(markPosition);
-            nestedMarks = 0;
+            if (nestedMarks == 0) {
+                input.flushBefore(markPosition);
+            }
+            markIndex = nestedMarks;                // Set only on success.
         } catch (IOException e) {
             throw new UncheckedIOException(e);      // InputStream.mark() does 
not allow us to throw IOException.
         }
@@ -156,7 +167,6 @@ public final class InputStreamAdapter extends InputStream 
implements Markable {
     /**
      * Marks the current position in this input stream.
      * This method is part of {@link Markable} API, where marks can be nested.
-     * It is okay to invoke this method after {@link #mark(int)} (but not 
before).
      */
     @Override
     public synchronized void mark() {
@@ -167,27 +177,29 @@ public final class InputStreamAdapter extends InputStream 
implements Markable {
     /**
      * Repositions this stream to the position at the time the {@code mark} 
method was last called.
      * This method has to comply with both {@link InputStream#reset()} and 
{@link Markable#reset()}
-     * contracts. It does that by choosing the first option in following list:
+     * contracts. It does that by pulling from most recent mark to oldest mark 
regardless if marks
+     * were created by {@link #mark()} or {@link #mark(int)}, except that all 
marks created by
+     * {@link #mark(int)} are ignored except the most recent one.
      *
-     * <ul>
-     *   <li>If there is nested {@link #mark()} calls, then this {@code 
reset()} method sets the stream
-     *       position to the most recent unmatched call to {@code mark()}.</li>
-     *   <li>Otherwise if the {@link #mark(int)} method has been invoked, then 
this method sets the stream
-     *       position to the mark created by the most recent call to {@code 
mark(int)}. The {@code reset()}
-     *       method can be invoked many time; it will always set the position 
to the same mark
-     *       (this behavior is required by {@link InputStream} contract).</li>
-     *   <li>Otherwise this method sets the stream position to the position it 
had when this
-     *       {@code InputStreamAdapter} has been created.</li>
-     * </ul>
+     * <p>Implementations of {@code reset()} if Java I/O package does not 
discard the mark.
+     * The implementation in this {@code InputStreamAdapter} class does not 
discard the mark
+     * neither if the mark done by a call to {@link #mark(int)} is the only 
mark remaining.
+     * Some code depends on the ability to do many {@code reset()} for the 
same mark.</p>
      *
-     * @throws IOException if an I/O error occurs.
+     * @throws IOException if this stream can not move to the last mark 
position.
      */
     @Override
     public synchronized void reset() throws IOException {
-        if (--nestedMarks >= 0) {
+        if (markIndex == nestedMarks) {
+            if (markIndex != 0) {           // Do not clear if it is the only 
mark (see javadoc).
+                markIndex = -1;             // Clear first in case of failure 
in next line.
+            }
+            input.seek(markPosition);
+        } else if (nestedMarks > 0) {
+            nestedMarks--;
             input.reset();
         } else {
-            input.seek(markPosition);
+            throw new 
IOException(Resources.format(Resources.Keys.StreamHasNoMark));
         }
     }
 
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/Markable.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/Markable.java
index 6aea473..044df5d 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/Markable.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/Markable.java
@@ -68,14 +68,18 @@ public interface Markable {
      * An {@code IOException} may be be thrown if the previous marked position 
lies in the
      * discarded portion of the stream.
      *
-     * <p>If there is no mark, then the behavior is undefined.
-     * {@link java.io.InputStream#reset()} specifies that we shall move to the 
file beginning.
+     * <h4>Behavior if there is no mark</h4>
+     * Calls to {@code reset()} without a corresponding call to {@code mark()}
+     * have different behavior depending on which interface defines the 
mark/reset methods:
+     * {@link java.io.InputStream#reset()} specifies that we might move to the 
file beginning or throw an exception.
      * {@link javax.imageio.stream.ImageInputStream#reset()} specifies that we 
shall do nothing.
-     * {@link ChannelDataInput#reset()} throws {@link 
java.nio.InvalidMarkException}.</p>
+     * For this {@code Markable} interface we recommend to throw an {@link 
IOException}.
      *
-     * @throws IOException if a mark was defined but this stream can not move 
to that position.
+     * @throws IOException if this stream can not move to the last mark 
position.
      *
+     * @see java.io.InputStream#reset()
      * @see javax.imageio.stream.ImageInputStream#reset()
+     * @see org.apache.sis.io.InvalidSeekException
      */
     void reset() throws IOException;
 }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/OutputStreamAdapter.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/OutputStreamAdapter.java
index 6b27713..5942685 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/OutputStreamAdapter.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/OutputStreamAdapter.java
@@ -97,7 +97,7 @@ final class OutputStreamAdapter extends OutputStream 
implements Markable {
     /**
      * Repositions this stream to the position at the time the {@code mark} 
method was last called.
      *
-     * @throws IOException if an I/O error occurs.
+     * @throws IOException if this stream can not move to the last mark 
position.
      */
     @Override
     public void reset() throws IOException {
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreProvider.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreProvider.java
index a3471da..c3c927f 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreProvider.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreProvider.java
@@ -16,18 +16,24 @@
  */
 package org.apache.sis.storage;
 
+import java.io.Reader;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import javax.imageio.stream.ImageInputStream;
 import java.util.logging.Logger;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.metadata.distribution.Format;
 import org.apache.sis.internal.simple.SimpleFormat;
 import org.apache.sis.internal.storage.URIDataStore;
+import org.apache.sis.internal.storage.io.Markable;
 import org.apache.sis.metadata.iso.citation.DefaultCitation;
 import org.apache.sis.metadata.iso.distribution.DefaultFormat;
 import org.apache.sis.measure.Range;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Version;
+import org.apache.sis.util.resources.Errors;
 
 
 /**
@@ -59,7 +65,8 @@ import org.apache.sis.util.Version;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.0
+ * @author  Alexis Manin (Geomatys)
+ * @version 1.2
  * @since   0.3
  * @module
  */
@@ -227,7 +234,8 @@ public abstract class DataStoreProvider {
 
     /**
      * Indicates if the given storage appears to be supported by the {@code 
DataStore}s created by this provider.
-     * The most typical return values are:
+     * Implementations will typically check the first bytes of the input 
stream for a "magic number" associated
+     * with the format. The most typical return values are:
      *
      * <ul>
      *   <li>{@link ProbeResult#SUPPORTED} if the {@code DataStore}s created 
by this provider
@@ -240,46 +248,211 @@ public abstract class DataStoreProvider {
      * only that there appears to be a reasonable chance of success based on a 
brief inspection of the
      * {@linkplain StorageConnector#getStorage() storage object} or contents.
      *
-     * <p>Implementers are responsible for restoring the input to its original 
stream position on return of this method.
-     * Implementers can use a mark/reset pair for this purpose. Marks are 
available as
-     * {@link java.nio.ByteBuffer#mark()}, {@link 
java.io.InputStream#mark(int)} and
-     * {@link javax.imageio.stream.ImageInputStream#mark()}.</p>
+     * <div class="note"><b>Note for implementers</b>:
+     * Implementations are responsible for restoring the storage object to its 
original position
+     * on return of this method. Implementers can use the mark/reset mechanism 
for this purpose.
+     * Marks are available as {@link java.nio.ByteBuffer#mark()}, {@link 
java.io.InputStream#mark(int)}
+     * and {@link javax.imageio.stream.ImageInputStream#mark()}.
+     * Alternatively the {@link #probeContent(StorageConnector, Class, Prober)}
+     * helper method manages automatically the marks for a set of known types.
+     * </div>
      *
-     * <div class="note"><b>Implementation example</b>:
-     * implementations will typically check the first bytes of the stream for 
a "magic number" associated
-     * with the format, as in the following example:
+     * @param  connector  information about the storage (URL, stream, JDBC 
connection, <i>etc</i>).
+     * @return {@link ProbeResult#SUPPORTED} if the given storage seems to be 
readable by the {@code DataStore}
+     *         instances created by this provider.
+     * @throws DataStoreException if an I/O or SQL error occurred. The error 
shall be unrelated to the logical
+     *         structure of the storage.
+     */
+    public abstract ProbeResult probeContent(StorageConnector connector) 
throws DataStoreException;
+
+    /**
+     * Applies the specified test on the storage content without modifying 
buffer or input stream position.
+     * This is a helper method for {@link #probeContent(StorageConnector)} 
implementations,
+     * providing an alternative safer than {@link 
StorageConnector#getStorageAs(Class)}
+     * for performing an arbitrary number of independent tests on the same 
{@code StorageConnector}.
+     * Current implementation accepts the following types (this list may be 
expanded in future versions):
+     *
+     * <blockquote>
+     * {@link java.nio.ByteBuffer},
+     * {@link java.io.InputStream},
+     * {@link java.io.DataInput},
+     * {@link javax.imageio.stream.ImageInputStream} and
+     * {@link java.io.Reader}.
+     * </blockquote>
+     *
+     * The following types are also accepted for completeness but provide no 
additional safety
+     * compared to direct use of {@link StorageConnector#getStorageAs(Class)}:
+     *
+     * <blockquote>
+     * {@link java.net.URI},
+     * {@link java.net.URL},
+     * {@link java.io.File},
+     * {@link java.nio.file.Path} and
+     * {@link String} (interpreted as a file path).
+     * </blockquote>
+     *
+     * This method {@linkplain InputStream#mark(int) marks} and {@linkplain 
InputStream#reset() resets}
+     * streams automatically with an arbitrary read-ahead limit (typically 
okay for the first 8 kilobytes).
+     *
+     * <h4>Usage example</h4>
+     * {@link #probeContent(StorageConnector)} implementations often check the 
first bytes of the
+     * input stream for a "magic number" associated with the format, as in the 
following example:
      *
      * {@preformat java
-     *     public ProbeResult probeContent(StorageConnector storage) throws 
DataStoreException {
-     *         final ByteBuffer buffer = 
storage.getStorageAs(ByteBuffer.class);
-     *         if (buffer == null) {
-     *             // If StorageConnector can not provide a ByteBuffer, then 
the storage is
-     *             // probably not a File, URL, URI, InputStream neither a 
ReadableChannel.
-     *             return ProbeResult.UNSUPPORTED_STORAGE;
-     *         }
-     *         if (buffer.remaining() < Integer.BYTES) {
+     *     &#64;Override
+     *     public ProbeResult probeContent(StorageConnector connector) throws 
DataStoreException {
+     *         return probeContent(connector, ByteBuffer.class, (buffer) -> {
+     *             if (buffer.remaining() >= Integer.BYTES) {
+     *                 if (buffer.getInt() == MAGIC_NUMBER) {
+     *                     return ProbeResult.SUPPORTED;
+     *                 }
+     *                 return ProbeResult.UNSUPPORTED_STORAGE;
+     *             }
      *             // If the buffer does not contain enough bytes for the 
integer type, this is not
      *             // necessarily because the file is truncated. It may be 
because the data were not
      *             // yet available at the time this method has been invoked.
      *             return ProbeResult.INSUFFICIENT_BYTES;
-     *         }
-     *         if (buffer.getInt(buffer.position()) != MAGIC_NUMBER) {
-     *             // We used ByteBuffer.getInt(int) instead of 
ByteBuffer.getInt() above
-     *             // in order to keep the buffer position unchanged after 
this method call.
-     *             return ProbeResult.UNSUPPORTED_STORAGE;
-     *         }
-     *         return ProbeResult.SUPPORTED;
+     *         });
      *     }
      * }
-     * </div>
      *
-     * @param  connector information about the storage (URL, stream, JDBC 
connection, <i>etc</i>).
-     * @return {@link ProbeResult#SUPPORTED} if the given storage seems to be 
readable by the {@code DataStore}
-     *         instances created by this provider.
-     * @throws DataStoreException if an I/O or SQL error occurred. The error 
shall be unrelated to the logical
-     *         structure of the storage.
+     * @param  <S>        the compile-time type of the {@code type} argument 
(the source or storage type).
+     * @param  connector  information about the storage (URL, stream, JDBC 
connection, <i>etc</i>).
+     * @param  type       the desired type as one of {@code ByteBuffer}, 
{@code DataInput}, {@code Connection}
+     *                    class or other types documented in {@link 
#getStorageAs(Class)}.
+     * @param  prober     the test to apply on the source of the given type.
+     * @return the result of executing the probe action with a source of the 
given type,
+     *         or {@link ProbeResult#UNSUPPORTED_STORAGE} if the given type is 
supported
+     *         but no view can be created for the source given at construction 
time.
+     * @throws IllegalArgumentException if the given {@code type} argument is 
not one of the supported types.
+     * @throws IllegalStateException if this {@code StorageConnector} has been 
{@linkplain #closeAllExcept closed}.
+     * @throws DataStoreException if an error occurred while opening a stream 
or database connection,
+     *         or during the execution of the probe action.
+     *
+     * @see #probeContent(StorageConnector)
+     * @see StorageConnector#getStorageAs(Class)
+     *
+     * @since 1.2
      */
-    public abstract ProbeResult probeContent(StorageConnector connector) 
throws DataStoreException;
+    protected <S> ProbeResult probeContent(final StorageConnector connector,
+            final Class<S> type, final Prober<? super S> prober) throws 
DataStoreException
+    {
+        ArgumentChecks.ensureNonNull("prober", prober);
+        /*
+         * Synchronization is not a documented feature for now because the 
policy may change in future version.
+         * Current version uses the storage source as the synchronization lock 
because using `StorageConnector`
+         * as the lock is not sufficient; the stream may be in use outside the 
connector. We have no way to know
+         * which lock (if any) is used by the source. But `InputStream` for 
example uses `this`.
+         */
+        synchronized (connector.storage) {
+            final S input = connector.getStorageAs(type);
+            if (input == null) {        // Means that the given type is valid 
but not applicable for current storage.
+                return ProbeResult.UNSUPPORTED_STORAGE;
+            }
+            if (input == connector.storage && 
!StorageConnector.isSupportedType(type)) {
+                /*
+                 * The given type is not one of the types known to 
`StorageConnector` (the list of supported types
+                 * is hard-coded). We could give the input as-is to the 
prober, but we have no idea how to fulfill
+                 * the method contract saying that the use of the input is 
safe. We throw an exception for telling
+                 * to the users that they should manage the input themselves.
+                 */
+                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedType_1, type));
+            }
+            ProbeResult result = null;
+            try {
+                if (input instanceof ByteBuffer) {
+                    /*
+                     * No need to save buffer position because 
`asReadOnlyBuffer()`
+                     * creates an independent buffer with its own mark and 
position.
+                     * Byte order of the view is intentionally left to the 
default
+                     * because we expect the callers to set the order that 
they need.
+                     */
+                    final ByteBuffer buffer = (ByteBuffer) input;
+                    result = prober.test(type.cast(buffer.asReadOnlyBuffer()));
+                } else if (input instanceof Markable) {
+                    /*
+                     * `Markable` stream can nest an arbitrary number of 
marks. So we allow users to create
+                     * their own marks. In principle a single call to 
`reset()` is enough, but we check the
+                     * position in case the user has done some marks without 
resets.
+                     */
+                    final Markable stream = (Markable) input;
+                    final long position = stream.getStreamPosition();
+                    stream.mark();
+                    result = prober.test(input);
+                    do stream.reset();
+                    while (stream.getStreamPosition() != position);
+                } else if (input instanceof ImageInputStream) {
+                    /*
+                     * `ImageInputStream` supports an arbitrary number of 
marks as well,
+                     * but we use absolute positioning for simplicity.
+                     */
+                    final ImageInputStream stream = (ImageInputStream) input;
+                    final long position = stream.getStreamPosition();
+                    result = prober.test(input);
+                    stream.seek(position);
+                } else if (input instanceof InputStream) {
+                    /*
+                     * `InputStream` supports at most one mark. So we keep it 
for ourselve
+                     * and wrap the stream in an object that prevent user from 
using marks.
+                     */
+                    final ProbeInputStream stream = new 
ProbeInputStream(connector, (InputStream) input);
+                    result = prober.test(type.cast(stream));
+                    stream.close();                 // Reset (not close) the 
wrapped stream.
+                } else if (input instanceof Reader) {
+                    final Reader stream = new ProbeReader(connector, (Reader) 
input);
+                    result = prober.test(type.cast(stream));
+                    stream.close();                 // Reset (not close) the 
wrapped reader.
+                } else {
+                    /*
+                     * All other cases are objects like File, URL, etc. which 
can be used without mark/reset.
+                     * Note that if the type was not known to be safe, an 
exception would have been thrown at
+                     * the beginning of this method.
+                     */
+                    result = prober.test(input);
+                }
+            } catch (DataStoreException e) {
+                throw e;
+            } catch (Exception e) {
+                final String message = Errors.format(Errors.Keys.CanNotRead_1, 
connector.getStorageName());
+                if (result != null) {
+                    throw new ForwardOnlyStorageException(message, e);
+                }
+                throw new CanNotProbeException(this, connector, e);
+            }
+            return result;
+        }
+    }
+
+    /**
+     * An action to execute for testing if a {@link StorageConnector} input 
can be read.
+     * This action is invoked by {@link #probeContent(StorageConnector, Class, 
Prober)}
+     * with an input of the type {@code <S>} specified to the {@code probe(…)} 
method.
+     * The {@code DataStoreProvider} is responsible for restoring the input to 
its initial position
+     * after the probe action completed.
+     *
+     * @param  <S>  the source type as one of {@code ByteBuffer}, {@code 
DataInput} or other classes
+     *              documented in {@link StorageConnector#getStorageAs(Class)}.
+     *
+     * @version 1.2
+     *
+     * @see #probeContent(StorageConnector, Class, Prober)
+     *
+     * @since 1.2
+     */
+    @FunctionalInterface
+    public interface Prober<S> {
+        /**
+         * Probes the given input and returns an indication about whether that 
input is supported.
+         * This method may return {@code SUPPORTED} if there is reasonable 
chance of success based
+         * on a brief inspection of the given input;
+         * the supported status does not need to be guaranteed.
+         *
+         * @param  input  the input to probe. This is for example a {@code 
ByteBuffer} or a {@code DataInput}.
+         * @return the result of executing the probe action with the given 
source. Should not be null.
+         * @throws Exception if an error occurred during the execution of the 
probe action.
+         */
+        ProbeResult test(S input) throws Exception;
+    }
 
     /**
      * Returns a data store implementation associated with this provider.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/ProbeInputStream.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/ProbeInputStream.java
new file mode 100644
index 0000000..df1448e
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/ProbeInputStream.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.FilterInputStream;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.storage.Resources;
+
+
+/**
+ * A temporary input stream used for probing purposes.
+ * This stream does not allow mark/reset operations because the mark is 
reserved for this class.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ *
+ * @see ProbeReader
+ * @see DataStoreProvider#probeContent(StorageConnector, Class, Prober)
+ *
+ * @since 1.2
+ * @module
+ */
+final class ProbeInputStream extends FilterInputStream {
+    /**
+     * Creates a new input stream which delegates everything to the given 
input except the mark/reset operations.
+     */
+    ProbeInputStream(final StorageConnector owner, final InputStream input) 
throws IOException, DataStoreException {
+        super(input);
+        if (!input.markSupported()) {
+            throw new 
DataStoreException(Resources.format(Resources.Keys.MarkNotSupported_1, 
owner.getStorageName()));
+        }
+        input.mark(StorageConnector.DEFAULT_BUFFER_SIZE);
+    }
+
+    /**
+     * Notifies the caller that marks are not supported on this input stream.
+     */
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    /**
+     * Does nothing since marks are not supported on this input stream.
+     * Note that doing nothing is the behavior of the default {@link 
InputStream#mark(int)} implementation.
+     * In particular, we can not declare the checked {@link IOException} here.
+     */
+    @Override
+    public void mark(int readlimit) {
+    }
+
+    /**
+     * Throws an exception since marks are not supported on this input stream.
+     */
+    @Override
+    public void reset() throws IOException {
+        throw new 
IOException(Errors.format(Errors.Keys.UnsupportedOperation_1, "reset"));
+    }
+
+    /**
+     * Closes this stream and resets the wrapped stream to its original 
position.
+     */
+    @Override
+    public void close() throws IOException {
+        final InputStream input = in;
+        in = null;
+        if (input != null) {
+            input.reset();
+        }
+    }
+}
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/ProbeReader.java 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/ProbeReader.java
new file mode 100644
index 0000000..d65db0d
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/ProbeReader.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.FilterReader;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.storage.Resources;
+
+
+/**
+ * A temporary character reader used for probing purposes.
+ * This reader does not allow mark/reset operations because the mark is 
reserved for this class.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ *
+ * @see ProbeInputStream
+ * @see DataStoreProvider#probeContent(StorageConnector, Class, Prober)
+ *
+ * @since 1.2
+ * @module
+ */
+final class ProbeReader extends FilterReader {
+    /**
+     * Creates a new reader which delegates everything to the given reader 
except the mark/reset operations.
+     */
+    ProbeReader(final StorageConnector owner, final Reader input) throws 
IOException, DataStoreException {
+        super(input);
+        if (!input.markSupported()) {
+            throw new 
DataStoreException(Resources.format(Resources.Keys.MarkNotSupported_1, 
owner.getStorageName()));
+        }
+        input.mark(StorageConnector.DEFAULT_BUFFER_SIZE);
+    }
+
+    /**
+     * Notifies the caller that marks are not supported on this reader.
+     */
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    /**
+     * Throws an exception since marks are not supported on this reader.
+     */
+    @Override
+    public void mark(int readlimit) throws IOException {
+        throw new 
IOException(Errors.format(Errors.Keys.UnsupportedOperation_1, "mark"));
+    }
+
+    /**
+     * Throws an exception since marks are not supported on this reader.
+     */
+    @Override
+    public void reset() throws IOException {
+        throw new 
IOException(Errors.format(Errors.Keys.UnsupportedOperation_1, "reset"));
+    }
+
+    /**
+     * Closes this reader and resets the wrapped reader to its original 
position.
+     */
+    @Override
+    public void close() throws IOException {
+        final Reader input = in;
+        in = null;
+        if (input != null) {
+            input.reset();
+        }
+    }
+}
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
index 67ee55a..5a569a6 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
@@ -84,13 +84,15 @@ import org.apache.sis.setup.OptionKey;
  * discarded since each data store implementation will use their own 
input/output objects.</p>
  *
  * <h2>Limitations</h2>
- * This class is not thread-safe. Not only {@code StorageConnector} should be 
used by a single thread,
+ * This class is not thread-safe.
+ * Not only {@code StorageConnector} should be used by a single thread,
  * but the objects returned by {@link #getStorageAs(Class)} should also be 
used by the same thread.
  *
  * <p>Instances of this class are serializable if the {@code storage} object 
given at construction time
  * is serializable.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
+ * @author  Alexis Manin (Geomatys)
  * @version 1.2
  * @since   0.3
  * @module
@@ -105,6 +107,11 @@ public class StorageConnector implements Serializable {
      * The default size of the {@link ByteBuffer} to be created.
      * Users can override this value by providing a value for {@link 
OptionKey#BYTE_BUFFER}.
      *
+     * <p>This buffer capacity is also used as read-ahead limit for mark 
operations.
+     * The rational is to allow as many bytes as contained in buffers of 
default size.
+     * For increasing the chances to meet that goal, this size should be the 
same than
+     * {@link java.io.BufferedInputStream} default buffer size.</p>
+     *
      * @see RewindableLineReader#BUFFER_SIZE
      */
     static final int DEFAULT_BUFFER_SIZE = 8192;
@@ -651,6 +658,14 @@ public class StorageConnector implements Serializable {
     }
 
     /**
+     * Returns {@code true} if the given type is one of the types supported by 
{@code StorageConnector}.
+     * The list of supported types is hard-coded and may change in any future 
version.
+     */
+    static boolean isSupportedType(final Class<?> type) {
+        return OPENERS.containsKey(type);
+    }
+
+    /**
      * Returns the storage as a view of the given type if possible, or {@code 
null} otherwise.
      * The default implementation accepts the following types:
      *
@@ -749,15 +764,18 @@ public class StorageConnector implements Serializable {
      *   </li>
      * </ul>
      *
+     * <h4>Usage for probing operations</h4>
      * Multiple invocations of this method on the same {@code 
StorageConnector} instance will try
      * to return the same instance on a <cite>best effort</cite> basis. 
Consequently, implementations of
      * {@link DataStoreProvider#probeContent(StorageConnector)} methods shall 
not close the stream or
      * database connection returned by this method. In addition, those {@code 
probeContent(StorageConnector)}
      * methods are responsible for restoring the stream or byte buffer to its 
original position on return.
+     * For an easier and safer way to ensure that the storage position is not 
modified,
+     * see {@link DataStoreProvider#probeContent(StorageConnector, Class, 
Prober)}.
      *
      * @param  <S>   the compile-time type of the {@code type} argument (the 
source or storage type).
      * @param  type  the desired type as one of {@code ByteBuffer}, {@code 
DataInput}, {@code Connection}
-     *               class or other type supported by {@code StorageConnector} 
subclasses.
+     *               class or other types supported by {@code 
StorageConnector} subclasses.
      * @return the storage as a view of the given type, or {@code null} if the 
given type is one of the supported
      *         types listed in javadoc but no view can be created for the 
source given at construction time.
      * @throws IllegalArgumentException if the given {@code type} argument is 
not one of the supported types
@@ -767,6 +785,7 @@ public class StorageConnector implements Serializable {
      *
      * @see #getStorage()
      * @see #closeAllExcept(Object)
+     * @see DataStoreProvider#probeContent(StorageConnector, Class, Prober)
      */
     public <S> S getStorageAs(final Class<S> type) throws 
IllegalArgumentException, DataStoreException {
         ArgumentChecks.ensureNonNull("type", type);
@@ -1033,6 +1052,8 @@ public class StorageConnector implements Serializable {
         /*
          * First, try to create the ChannelDataInput if it does not already 
exists.
          * If successful, this will create a ByteBuffer companion as a side 
effect.
+         * Byte order of the view is intentionally left to the default (big 
endian)
+         * because we expect the callers to set the order that they need.
          */
         final ChannelDataInput c = getStorageAs(ChannelDataInput.class);
         ByteBuffer asByteBuffer = null;
@@ -1270,6 +1291,47 @@ public class StorageConnector implements Serializable {
     }
 
     /**
+     * Returns the storage as a view of the given type and closes all other 
views.
+     * Invoking this method is equivalent to invoking {@link 
#getStorageAs(Class)}
+     * followed by {@link #closeAllExcept(Object)} except that the later 
method is
+     * always invoked (in a way similar to "try with resource") and that this 
method
+     * never returns {@code null}.
+     *
+     * @param  <S>     the compile-time type of the {@code type} argument (the 
source or storage type).
+     * @param  type    the desired type as one of the types documented in 
{@link #getStorageAs(Class)}
+     *                 (example: {@code ByteBuffer}, {@code DataInput}, {@code 
Connection}).
+     * @param  format  short name or abbreviation of the data format (e.g. 
"CSV", "GML", "WKT", <i>etc</i>).
+     *                 Used for information purpose in error messages if 
needed.
+     * @return the storage as a view of the given type. Never {@code null}.
+     * @throws IllegalArgumentException if the given {@code type} argument is 
not one of the supported types.
+     * @throws IllegalStateException if this {@code StorageConnector} has been 
{@linkplain #closeAllExcept closed}.
+     * @throws DataStoreException if an error occurred while opening a stream 
or database connection.
+     *
+     * @see #getStorageAs(Class)
+     * @see #closeAllExcept(Object)
+     *
+     * @since 1.2
+     */
+    public <S> S commit(final Class<S> type, final String format) throws 
IllegalArgumentException, DataStoreException {
+        final S view;
+        try {
+            view = getStorageAs(type);
+        } catch (Throwable ex) {
+            try {
+                closeAllExcept(null);
+            } catch (Throwable se) {
+                ex.addSuppressed(se);
+            }
+            throw ex;
+        }
+        closeAllExcept(view);
+        if (view == null) {
+            throw new UnsupportedStorageException(null, format, storage, 
getOption(OptionKey.OPEN_OPTIONS));
+        }
+        return view;
+    }
+
+    /**
      * Closes all streams and connections created by this {@code 
StorageConnector} except the given view.
      * This method closes all objects created by the {@link 
#getStorageAs(Class)} method except the given {@code view}.
      * If {@code view} is {@code null}, then this method closes everything 
including the {@linkplain #getStorage()
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataOutputTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataOutputTest.java
index cfacc0b..5a42216 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataOutputTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataOutputTest.java
@@ -23,7 +23,6 @@ import java.io.DataOutput;
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.nio.InvalidMarkException;
 import java.nio.channels.ByteChannel;
 import javax.imageio.stream.ImageOutputStream;
 import org.apache.sis.test.DependsOnMethod;
@@ -205,8 +204,9 @@ public strictfp class ChannelDataOutputTest extends 
ChannelDataTestCase {
         try {
             testedStream.reset();
             fail("Shall not accept reset without mark.");
-        } catch (InvalidMarkException e) {
+        } catch (IOException e) {
             // This is the expected exception.
+            assertNotNull(e.getMessage());
         }
         /*
          * flushBefore(int).
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelImageOutputStreamTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelImageOutputStreamTest.java
index a78e3d4..20764be 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelImageOutputStreamTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelImageOutputStreamTest.java
@@ -19,7 +19,6 @@ package org.apache.sis.internal.storage.io;
 import java.io.IOException;
 import java.io.ByteArrayOutputStream;
 import java.nio.ByteBuffer;
-import java.nio.InvalidMarkException;
 import javax.imageio.stream.ImageOutputStream;
 import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.DependsOn;
@@ -129,8 +128,9 @@ public final strictfp class ChannelImageOutputStreamTest 
extends ChannelDataOutp
         try {
             testedStream.reset();
             fail("Expected no remaining marks.");
-        } catch (InvalidMarkException e) {
+        } catch (IOException e) {
             // This is the expected exception.
+            assertNotNull(e.getMessage());
         }
         assertStreamContentEquals();
     }
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreProviderTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreProviderTest.java
new file mode 100644
index 0000000..ebf2875
--- /dev/null
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreProviderTest.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage;
+
+import java.io.Reader;
+import java.io.InputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.apache.sis.test.DependsOn;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link DataStoreProvider}.
+ *
+ * @author  Alexis Manin (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+@DependsOn(StorageConnectorTest.class)
+public final strictfp class DataStoreProviderTest extends TestCase {
+    /**
+     * A dummy provider instance to test. Only the
+     * {@link DataStoreProvider#probeContent(StorageConnector, Class, Prober)} 
method is useful on this instance.
+     */
+    private final DataStoreProvider provider;
+
+    /**
+     * Creates a new test case.
+     */
+    public DataStoreProviderTest() {
+        provider = new DataStoreProvider() {
+            @Override public String getShortName() {return "Provider mock";}
+            @Override public ParameterDescriptorGroup getOpenParameters() 
{throw new AssertionError();}
+            @Override public ProbeResult probeContent(StorageConnector 
connector) {throw new AssertionError();}
+            @Override public DataStore open(StorageConnector connector) {throw 
new AssertionError();}
+        };
+    }
+
+    /**
+     * Asserts that probing with {@link InputStream} input gives the expected 
result.
+     */
+    private void verifyProbeWithInputStream(final StorageConnector connector) 
throws DataStoreException {
+        assertEquals(provider.probeContent(connector, InputStream.class, 
stream -> {
+            StorageConnectorTest.assertExpectedBytes(stream);
+            return ProbeResult.SUPPORTED;
+        }), ProbeResult.SUPPORTED);
+    }
+
+    /**
+     * Asserts that probing with {@link Reader} input gives the expected 
result.
+     */
+    private void verifyProbeWithReader(final StorageConnector connector) 
throws DataStoreException {
+        assertEquals(provider.probeContent(connector, Reader.class, stream -> {
+            StorageConnectorTest.assertExpectedChars(stream);
+            return ProbeResult.SUPPORTED;
+        }), ProbeResult.SUPPORTED);
+    }
+
+    /**
+     * Verifies that {@link DataStoreProvider#probeContent(StorageConnector, 
Class, Prober)}
+     * with a {@link ByteBuffer} leaves the position of the original buffer 
unchanged.
+     *
+     * @throws DataStoreException if an error occurred while using the storage 
connector.
+     */
+    @Test
+    public void testProbeWithByteBuffer() throws DataStoreException {
+        /*
+         * Change the buffer position for simulating a read operation
+         * without reseting the buffer position.
+         */
+        final StorageConnector connector = StorageConnectorTest.create(false);
+        assertEquals(provider.probeContent(connector, ByteBuffer.class, buffer 
-> {
+            assertEquals(0, buffer.position());
+            buffer.position(15).mark();
+            return ProbeResult.UNDETERMINED;
+        }), ProbeResult.UNDETERMINED);
+        /*
+         * Read again. The buffer position should be the original position
+         * (i.e. above call to `position(15)` shall have no effect below).
+         */
+        assertEquals(provider.probeContent(connector, ByteBuffer.class, buffer 
-> {
+            assertEquals(0, buffer.position());
+            final byte[] expected = 
StorageConnectorTest.getFirstExpectedBytes();
+            final byte[] actual = new byte[expected.length];
+            buffer.get(actual);
+            assertArrayEquals(expected, actual);
+            return ProbeResult.SUPPORTED;
+        }), ProbeResult.SUPPORTED);
+    }
+
+    /**
+     * Tests {@link DataStoreProvider#probeContent(StorageConnector, Class, 
Prober)} with an {@link java.net.URL}.
+     *
+     * @throws DataStoreException if an error occurred while using the storage 
connector.
+     */
+    @Test
+    public void testProbeWithURL() throws DataStoreException {
+        testProbe(false);
+    }
+
+    /**
+     * Verifies that {@link DataStoreProvider#probeContent(StorageConnector, 
Class, Prober)}
+     * with an {@link InputStream} leaves the position of the original stream 
unchanged.
+     *
+     * @throws DataStoreException if an error occurred while using the storage 
connector.
+     */
+    @Test
+    public void testProbeWithInputStream() throws DataStoreException {
+        testProbe(true);
+    }
+
+    /**
+     * Implementation of {@link #testProbeWithURL()} and {@link 
#testProbeWithInputStream()}.
+     */
+    private void testProbe(final boolean asStream) throws DataStoreException {
+        /*
+         * Read a few bytes and verify that user can not overwrite the mark.
+         */
+        final StorageConnector connector = 
StorageConnectorTest.create(asStream);
+        assertEquals(provider.probeContent(connector, InputStream.class, 
stream -> {
+            assertEquals(!asStream, stream.markSupported());
+            stream.skip(5);
+            stream.mark(10);
+            stream.skip(4);
+            if (asStream) try {
+                stream.reset();
+                fail("Mark/reset should not be supported.");
+            } catch (IOException e) {
+                assertTrue(e.getMessage().contains("reset"));
+            } else {
+                stream.reset();         // Should be supported if opened from 
URL.
+            }
+            return ProbeResult.SUPPORTED;
+        }), ProbeResult.SUPPORTED);
+        /*
+         * Read the first bytes and verify that they are really the
+         * beginning of the file despite above reading of some bytes.
+         */
+        verifyProbeWithInputStream(connector);
+    }
+
+    /**
+     * Verifies that {@link DataStoreProvider#probeContent(StorageConnector, 
Class, Prober)}
+     * with a {@link Reader} leaves the position of the original stream 
unchanged.
+     *
+     * @throws DataStoreException if an error occurred while using the storage 
connector.
+     */
+    @Test
+    public void testProbeWithReader() throws DataStoreException {
+        /*
+         * Read a few bytes and verify that user can not overwrite the mark.
+         */
+        final StorageConnector connector = StorageConnectorTest.create(true);
+        assertEquals(provider.probeContent(connector, Reader.class, stream -> {
+            assertFalse(stream.markSupported());
+            stream.skip(5);
+            try {
+                stream.mark(10);
+                fail("Mark/reset should not be supported.");
+            } catch (IOException e) {
+                assertTrue(e.getMessage().contains("mark"));
+            }
+            return ProbeResult.SUPPORTED;
+        }), ProbeResult.SUPPORTED);
+        /*
+         * Read the first bytes and verify that they are really the
+         * beginning of the file despite above reading of some bytes.
+         */
+        verifyProbeWithReader(connector);
+        /*
+         * Get the input in another form (InputStream) and verifies
+         * that we still have the same first characters.
+         */
+        verifyProbeWithInputStream(connector);
+        /*
+         * Try again reading with a `Reader`. The intent is to verify
+         * that it is not corrupted by above use of `InputStream`.
+         */
+        verifyProbeWithReader(connector);
+    }
+}
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/StorageConnectorTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/StorageConnectorTest.java
index 6a3085f..613143c 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/StorageConnectorTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/StorageConnectorTest.java
@@ -21,6 +21,7 @@ import java.io.DataInput;
 import java.io.InputStream;
 import java.io.Reader;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.nio.channels.ReadableByteChannel;
@@ -45,7 +46,8 @@ import static org.opengis.test.Assert.*;
  * Tests {@link StorageConnector}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @author  Alexis Manin (Geomatys)
+ * @version 1.2
  * @since   0.3
  * @module
  */
@@ -58,6 +60,8 @@ public final strictfp class StorageConnectorTest extends 
TestCase {
 
     /**
      * Beginning of the first sentence in {@value #FILENAME}.
+     *
+     * @see #getFirstExpectedBytes()
      */
     private static final String FIRST_SENTENCE = "The purpose of this file";
 
@@ -67,10 +71,10 @@ public final strictfp class StorageConnectorTest extends 
TestCase {
     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
+     * Creates the instance to test. This method uses the {@code "Any.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) {
+    static StorageConnector create(final boolean asStream) {
         final Class<?> c = StorageConnectorTest.class;
         final Object storage = asStream ? c.getResourceAsStream(FILENAME) : 
c.getResource(FILENAME);
         assertNotNull(storage);
@@ -81,6 +85,35 @@ public final strictfp class StorageConnectorTest extends 
TestCase {
     }
 
     /**
+     * Returns the first bytes expected to be found in the {@value #FILENAME} 
file.
+     */
+    static byte[] getFirstExpectedBytes() {
+        return FIRST_SENTENCE.getBytes(StandardCharsets.US_ASCII);
+    }
+
+    /**
+     * Reads the first bytes from the given input stream and verifies
+     * that they are equal to the expected content.
+     */
+    static void assertExpectedBytes(final InputStream stream) throws 
IOException {
+        final byte[] expected = getFirstExpectedBytes();
+        final byte[] content = new byte[expected.length];
+        assertEquals(content.length, stream.read(content));
+        assertArrayEquals(expected, content);
+    }
+
+    /**
+     * Reads the first characters from the given reader and verifies
+     * that they are equal to the expected content.
+     */
+    static void assertExpectedChars(final Reader stream) throws IOException {
+        final char[] expected = FIRST_SENTENCE.toCharArray();
+        final char[] content = new char[expected.length];
+        assertEquals(content.length, stream.read(content));
+        assertArrayEquals(expected, content);
+    }
+
+    /**
      * Tests the {@link StorageConnector#getStorageName()} method.
      */
     @Test
@@ -139,6 +172,8 @@ public final strictfp class StorageConnectorTest extends 
TestCase {
      */
     private void testGetAsDataInput(final boolean asStream) throws 
DataStoreException, IOException {
         final StorageConnector connector = create(asStream);
+        assertEquals(asStream, connector.getStorageAs(URI.class)  == null);
+        assertEquals(asStream, connector.getStorageAs(Path.class) == null);
         final DataInput input = connector.getStorageAs(DataInput.class);
         assertSame("Value shall be cached.", input, 
connector.getStorageAs(DataInput.class));
         assertInstanceOf("Needs the SIS implementation.", 
ChannelImageInputStream.class, input);
@@ -242,11 +277,8 @@ public final strictfp class StorageConnectorTest extends 
TestCase {
     public void testGetAsReader() throws DataStoreException, IOException {
         final StorageConnector connector = create(true);
         final Reader in = connector.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);
+        assertExpectedChars(in);
         assertSame("Expected cached value.", in, 
connector.getStorageAs(Reader.class));
         in.reset();
         /*
@@ -263,8 +295,7 @@ public final strictfp class StorageConnectorTest extends 
TestCase {
          */
         final Reader in2 = connector.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);
+        assertExpectedChars(in2);
         assertSame("Expected cached value.", in2, 
connector.getStorageAs(Reader.class));
         connector.closeAllExcept(null);
     }
@@ -390,4 +421,41 @@ public final strictfp class StorageConnectorTest extends 
TestCase {
         assertTrue("channel.isOpen()", channel.isOpen());
         channel.close();
     }
+
+    /**
+     * Tests the {@link StorageConnector#commit(Class, String)} method.
+     *
+     * @throws DataStoreException if an error occurred while using the storage 
connector.
+     * @throws IOException if an error occurred while reading the test file.
+     */
+    @Test
+    @DependsOnMethod("testCloseAllExcept")
+    public void testCommit() throws DataStoreException, IOException {
+        final StorageConnector connector = create(false);
+        final InputStream stream = connector.commit(InputStream.class, "Test");
+        try {
+            connector.getStorageAs(ByteBuffer.class);
+            fail("Connector should be closed.");
+        } catch (IllegalStateException e) {
+            assertNotNull(e.getMessage());
+        }
+        assertExpectedBytes(stream);
+        stream.close();                 // No "try-with-resource" for easier 
debugging if needed.
+    }
+
+    /**
+     * Verifies that the {@link StorageConnector#closeAllExcept(Object)} 
method is idempotent
+     * (i.e. calls after the first call have no effect).
+     *
+     * @throws DataStoreException if an error occurred while using the storage 
connector.
+     * @throws IOException if an error occurred while closing the test file.
+     */
+    @Test
+    public void testCloseIsIdempotent() throws DataStoreException, IOException 
{
+        final StorageConnector connector = StorageConnectorTest.create(true);
+        final InputStream stream = connector.commit(InputStream.class, "Test");
+        connector.closeAllExcept(null);
+        assertExpectedBytes(stream);
+        stream.close();                 // No "try-with-resource" for easier 
debugging if needed.
+    }
 }
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
index 1231745..a638d5d 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
@@ -25,7 +25,8 @@ import org.junit.BeforeClass;
  * All tests from the {@code sis-storage} module, in rough dependency order.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @author  Alexis Manin (Geomatys)
+ * @version 1.2
  * @since   0.3
  * @module
  */
@@ -45,6 +46,7 @@ import org.junit.BeforeClass;
     org.apache.sis.storage.FeatureNamingTest.class,
     org.apache.sis.storage.ProbeResultTest.class,
     org.apache.sis.storage.StorageConnectorTest.class,
+    org.apache.sis.storage.DataStoreProviderTest.class,
     org.apache.sis.storage.event.StoreListenersTest.class,
     org.apache.sis.storage.CoverageQueryTest.class,
     org.apache.sis.storage.FeatureQueryTest.class,

Reply via email to