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 cbc555d3db045b2f5118971dcda922869857726d
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Jan 5 21:17:22 2022 +0100

    Avoid wrapping `BufferedReader` if possible in 
`DataStoreProvider.probeContent(…)`.
    This is for allowing users to invoke `BufferedReader.readLine()`.
---
 .../org/apache/sis/internal/storage/csv/Store.java |  10 +-
 .../internal/storage/io/RewindableLineReader.java  | 104 +++++++++++++++++++--
 .../org/apache/sis/storage/DataStoreProvider.java  |  15 ++-
 .../org/apache/sis/storage/ProbeInputStream.java   |   1 +
 .../java/org/apache/sis/storage/ProbeReader.java   |   4 +
 .../apache/sis/storage/DataStoreProviderTest.java  |  68 +++++++++++---
 6 files changed, 170 insertions(+), 32 deletions(-)

diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
index 9829e26..a89e5fe 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
@@ -66,7 +66,6 @@ import org.apache.sis.storage.DataOptionKey;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreReferencingException;
-import org.apache.sis.storage.UnsupportedStorageException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.setup.OptionKey;
@@ -235,12 +234,7 @@ final class Store extends URIDataStore implements 
FeatureSet {
      */
     public Store(final StoreProvider provider, final StorageConnector 
connector) throws DataStoreException {
         super(provider, connector);
-        final Reader r = connector.getStorageAs(Reader.class);
-        connector.closeAllExcept(r);
-        if (r == null) {
-            throw new UnsupportedStorageException(super.getLocale(), 
StoreProvider.NAME,
-                    connector.getStorage(), 
connector.getOption(OptionKey.OPEN_OPTIONS));
-        }
+        final Reader r = connector.commit(Reader.class, StoreProvider.NAME);
         source     = (r instanceof BufferedReader) ? (BufferedReader) r : new 
LineNumberReader(r);
         geometries = 
Geometries.implementation(connector.getOption(OptionKey.GEOMETRY_LIBRARY));
         dissociate = 
FoliationRepresentation.FRAGMENTED.equals(connector.getOption(DataOptionKey.FOLIATION_REPRESENTATION));
@@ -317,6 +311,8 @@ final class Store extends URIDataStore implements 
FeatureSet {
      * which is set after the last header line. If the mark is no longer 
valid, then we have to create a new line reader.
      * In this later case, we have to skip the header lines (i.e. we reproduce 
the constructor loop, but without parsing
      * metadata).
+     *
+     * @todo Not yet used. This is planned for a future version of {@link 
#features(boolean)} method implementation.
      */
     private void rewind() throws IOException {
         final BufferedReader reader = source;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/RewindableLineReader.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/RewindableLineReader.java
index 7fe6ce4..2e9c49a 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/RewindableLineReader.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/RewindableLineReader.java
@@ -21,6 +21,7 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.LineNumberReader;
 import java.nio.charset.Charset;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.io.InvalidSeekException;
 
 
@@ -35,10 +36,11 @@ import org.apache.sis.io.InvalidSeekException;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.2
  * @since   0.8
  * @module
  */
+@SuppressWarnings("SynchronizeOnNonFinalField")
 public final class RewindableLineReader extends LineNumberReader {
     /**
      * Size of the buffer, in number of characters.
@@ -59,6 +61,15 @@ public final class RewindableLineReader extends 
LineNumberReader {
     private final Charset encoding;
 
     /**
+     * Whether calls to {@link #mark(int)} and {@link #reset()} should throw 
{@link IOException}.
+     * This is initially {@code false} and may be set to {@code true} if the 
caller wants to block
+     * users from overwriting the mark (s)he just did. This flag changes also 
the {@link #close()}
+     * behavior. It is used for probing the content of the same file by 
different data stores and we
+     * want a safety against implementations that do not follow the {@code 
probeContent(…)} contract.
+     */
+    private boolean isMarkProtected;
+
+    /**
      * Creates a line reader wrapping the given input stream.
      *
      * @param  input     the input stream from which to read characters.
@@ -72,13 +83,13 @@ public final class RewindableLineReader extends 
LineNumberReader {
             this.input = (InputStreamAdapter) input;
         }
         this.encoding = encoding;
-        super.mark(BUFFER_SIZE);
         /*
          * By default, this.lock is set to InputStreamReader. But 
InputStreamReader.lock has itself
          * been set on the given InputStreamAdapter.  So we set this.lock to 
InputStreamReader.lock
          * in order to have a single synchronization lock.
          */
         lock = input;
+        super.mark(BUFFER_SIZE);
     }
 
     /**
@@ -89,11 +100,10 @@ public final class RewindableLineReader extends 
LineNumberReader {
      * @return the reader to use for next read operation (may be {@code this}).
      * @throws IOException if an error occurred while rewinding the reader.
      */
-    @SuppressWarnings("SynchronizeOnNonFinalField")
     public RewindableLineReader rewind() throws IOException {
         synchronized (lock) {
             try {
-                reset();
+                super.reset();
                 return this;
             } catch (IOException e1) {
                 final InputStreamAdapter stream = input;
@@ -114,8 +124,8 @@ public final class RewindableLineReader extends 
LineNumberReader {
                     stream.keepOpen = false;
                 }
                 /*
-                 * Try to seek to the data origin. Note that 'seek(0)' below 
does not necessarily
-                 * move to the beginning of file, since ChannelDataInput may 
contain an offset.
+                 * Try to seek to the data origin. Note that `seek(0)` below 
does not necessarily
+                 * move to the beginning of file, because `ChannelDataInput` 
may contain an offset.
                  */
                 try {
                     stream.input.seek(0);
@@ -129,16 +139,92 @@ public final class RewindableLineReader extends 
LineNumberReader {
     }
 
     /**
-     * Closes this reader.
+     * Marks current stream position and blocks all subsequent calls to {@link 
#mark(int)},
+     * {@link #reset()} and {@link #close()} until {@link #protectedReset()} 
is invoked.
+     *
+     * @throws IOException if the stream can not be marked.
+     */
+    public final void protectedMark() throws IOException {
+        synchronized (lock) {
+            super.mark(BUFFER_SIZE);
+            isMarkProtected = true;
+        }
+    }
+
+    /**
+     * Stops the protection given by {@link #protectedMark()} and reset the 
stream to its marked position.
+     *
+     * @throws IOException if the stream can not be reset.
+     */
+    public final void protectedReset() throws IOException {
+        synchronized (lock) {
+            isMarkProtected = false;
+            super.reset();
+        }
+    }
+
+    /**
+     * Tells whether this stream supports the mark and reset operations.
+     * This is {@code true} by default but can be set to {@code false}
+     * if the mark is reserved to internal usage.
+     *
+     * @return true if this stream supports the mark operation.
+     */
+    @Override
+    public boolean markSupported() {
+        synchronized (lock) {
+            return !isMarkProtected;
+        }
+    }
+
+    /**
+     * Marks the present position in the stream if allowed to do so.
+     * If {@link #isMarkProtected} is {@code true}, then this method throws an 
exception.
+     *
+     * @param  readlimit  limit on the number of characters that may be read.
+     * @throws IOException if the stream can not be marked.
+     */
+    @Override
+    public void mark(final int readlimit) throws IOException {
+        synchronized (lock) {
+            if (isMarkProtected) {
+                throw new 
IOException(Errors.format(Errors.Keys.UnsupportedOperation_1, "mark"));
+            }
+            super.mark(readlimit);
+        }
+    }
+
+    /**
+     * Resets the stream if allowed to do so.
+     * If {@link #isMarkProtected} is {@code true}, then this method throws an 
exception.
+     *
+     * @throws IOException if the stream can not be reset.
+     */
+    @Override
+    public void reset() throws IOException {
+        synchronized (lock) {
+            if (isMarkProtected) {
+                throw new 
IOException(Errors.format(Errors.Keys.UnsupportedOperation_1, "reset"));
+            }
+            super.reset();
+        }
+    }
+
+    /**
+     * Closes this reader. The underlying stream will be either reset or closed
+     * depending on the {@link #isMarkProtected} mode.
      *
      * @throws IOException if an error occurred while closing the reader.
      */
     @Override
-    @SuppressWarnings("SynchronizeOnNonFinalField")
     public void close() throws IOException {
         synchronized (lock) {
             input = null;
-            super.close();
+            if (isMarkProtected) {
+                super.reset();
+            } else {
+                super.close();
+            }
         }
     }
 }
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 c3c927f..235ca30 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
@@ -27,6 +27,7 @@ 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.internal.storage.io.RewindableLineReader;
 import org.apache.sis.metadata.iso.citation.DefaultCitation;
 import org.apache.sis.metadata.iso.distribution.DefaultFormat;
 import org.apache.sis.measure.Range;
@@ -392,12 +393,22 @@ public abstract class DataStoreProvider {
                     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.
+                     * `InputStream` supports at most one mark. So we keep it 
for ourselves
+                     * and wrap the stream in an object that prevent users 
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 RewindableLineReader) {
+                    /*
+                     * `Reader` supports at most one mark. So we keep it for 
ourselves and prevent users
+                     * from using marks, but without wrapper if we can safely 
expose a `BufferedReader`
+                     * (because users may want to use the 
`BufferedReader.readLine()` method).
+                     */
+                    final RewindableLineReader r = (RewindableLineReader) 
input;
+                    r.protectedMark();
+                    result = prober.test(input);
+                    r.protectedReset();
                 } else if (input instanceof Reader) {
                     final Reader stream = new ProbeReader(connector, (Reader) 
input);
                     result = prober.test(type.cast(stream));
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
index df1448e..a32c4f0 100644
--- 
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
@@ -26,6 +26,7 @@ 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.
+ * The {@link #close()} method closes this stream but not the wrapped stream, 
which is only reset.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
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
index d65db0d..de869fa 100644
--- 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
@@ -26,6 +26,10 @@ 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.
+ * The {@link #close()} method closes this reader but not the wrapped reader, 
which is only reset.
+ *
+ * <p>Note: this wrapper is not used if the reader is an instance of
+ * {@link org.apache.sis.internal.storage.io.RewindableLineReader}.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
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
index f2a4d5e..c311793 100644
--- 
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
@@ -17,9 +17,12 @@
 package org.apache.sis.storage;
 
 import java.io.Reader;
+import java.io.BufferedReader;
 import java.io.InputStream;
 import java.io.IOException;
+import java.io.InputStreamReader;
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
@@ -116,7 +119,7 @@ public final strictfp class DataStoreProviderTest extends 
TestCase {
      */
     @Test
     public void testProbeWithURL() throws DataStoreException {
-        testProbe(false);
+        testProbeWithInputStream(false);
     }
 
     /**
@@ -127,13 +130,13 @@ public final strictfp class DataStoreProviderTest extends 
TestCase {
      */
     @Test
     public void testProbeWithInputStream() throws DataStoreException {
-        testProbe(true);
+        testProbeWithInputStream(true);
     }
 
     /**
      * Implementation of {@link #testProbeWithURL()} and {@link 
#testProbeWithInputStream()}.
      */
-    private void testProbe(final boolean asStream) throws DataStoreException {
+    private void testProbeWithInputStream(final boolean asStream) throws 
DataStoreException {
         /*
          * Read a few bytes and verify that user can not overwrite the mark.
          */
@@ -168,11 +171,57 @@ public final strictfp class DataStoreProviderTest extends 
TestCase {
      */
     @Test
     public void testProbeWithReader() throws DataStoreException {
+        final StorageConnector connector = testProbeWithReader(false);
+        /*
+         * Attempt to read with `InputStream` should not be possible because
+         * the connector does not know the original stream (we wrapped it).
+         */
+        try {
+            verifyProbeWithInputStream(connector);
+            fail("Operation should not be allowed.");
+        } catch (ForwardOnlyStorageException e) {
+            final String message = e.getMessage();
+            assertTrue(message, message.contains("InputStreamReader"));
+        }
+    }
+
+    /**
+     * Verifies that {@link DataStoreProvider#probeContent(StorageConnector, 
Class, Prober)}
+     * with a {@link BufferedReader} leaves the position of the original 
stream unchanged.
+     *
+     * @throws DataStoreException if an error occurred while using the storage 
connector.
+     */
+    @Test
+    public void testProbeWithBufferedReader() throws DataStoreException {
+        final StorageConnector connector = testProbeWithReader(true);
+        /*
+         * Get the input in another form (InputStream) and verifies that we 
still
+         * have the same first characters. Then try again reading with a 
`Reader`.
+         * The intent is to verify that it is not corrupted by `InputStream` 
use.
+         */
+        verifyProbeWithInputStream(connector);
+        verifyProbeWithReader(connector);
+    }
+
+    /**
+     * Implementation of {@link #testProbeWithReader()} and {@link 
#testProbeWithBufferedReader()}.
+     * If {@code buffered} is {@code true}, this method will creates itself a 
non-buffered reader
+     * (because reader created by {@link StorageConnector} are buffered by 
default).
+     *
+     * @param  buffered  whether to use a buffered reader.
+     * @return the storage connector created by this method, for allowing 
caller to do more tests.
+     */
+    private StorageConnector testProbeWithReader(final boolean buffered) 
throws DataStoreException {
+        StorageConnector connector = StorageConnectorTest.create(true);
+        if (!buffered) {
+            final InputStream stream = (InputStream) connector.getStorage();
+            connector = new StorageConnector(new InputStreamReader(stream, 
StandardCharsets.US_ASCII));
+        }
         /*
          * 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 -> {
+            assertEquals(buffered, stream instanceof BufferedReader);
             assertFalse(stream.markSupported());
             stream.skip(5);
             try {
@@ -188,15 +237,6 @@ public final strictfp class DataStoreProviderTest extends 
TestCase {
          * 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);
+        return connector;
     }
 }

Reply via email to