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) { + * @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,