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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 88c8bbb Add a `Prober.orElse(…)` method for testing probers with different types. 88c8bbb is described below commit 88c8bbbd3ae7ee867cc9311cd14c833150dfb0aa Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Mon Jan 10 01:29:17 2022 +0100 Add a `Prober.orElse(…)` method for testing probers with different types. --- .../apache/sis/internal/storage/folder/Store.java | 2 +- .../sis/internal/storage/xml/AbstractProvider.java | 24 +- .../org/apache/sis/storage/DataStoreProvider.java | 272 +++++++++++++++------ .../apache/sis/storage/DataStoreProviderTest.java | 28 +-- 4 files changed, 213 insertions(+), 113 deletions(-) diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java index d03cbf3..fd83e34 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java @@ -190,10 +190,10 @@ class Store extends DataStore implements StoreResource, Aggregate, DirectoryStre private Store(final Store parent, final StorageConnector connector, final NameFactory nameFactory) throws DataStoreException { super(parent, parent.getProvider(), connector, false); originator = parent; - location = connector.getStorageAs(Path.class); locale = connector.getOption(OptionKey.LOCALE); timezone = connector.getOption(OptionKey.TIMEZONE); encoding = connector.getOption(OptionKey.ENCODING); + location = connector.commit(Path.class, StoreProvider.NAME); children = parent.children; componentProvider = parent.componentProvider; identifier = nameFactory.createLocalName(parent.identifier(nameFactory).scope(), super.getDisplayName()); diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/AbstractProvider.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/AbstractProvider.java index 2908d59..30a33b2 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/AbstractProvider.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/AbstractProvider.java @@ -111,26 +111,18 @@ public abstract class AbstractProvider extends DocumentedStoreProvider { /* * Usual case. This includes InputStream, DataInput, File, Path, URL, URI. */ - final ByteBuffer buffer = connector.getStorageAs(ByteBuffer.class); - if (buffer != null) { - /* - * We do not use the safer `probeContent(…)` method because we do not have a mechanism - * for telling if `UNSUPPORTED_STORAGE` was determined by this block or if we got that - * result because the buffer was null. - */ + Prober<ByteBuffer> prober = (buffer) -> { if (buffer.remaining() < HEADER.length) { return ProbeResult.INSUFFICIENT_BYTES; } // Quick check for "<?xml " header. - final int p = buffer.position(); for (int i=0; i<HEADER.length; i++) { - if (buffer.get(p + i) != HEADER[i]) { // TODO: use ByteBuffer.mismatch(…) with JDK11. + if (buffer.get() != HEADER[i]) { // TODO: use ByteBuffer.mismatch(…) with JDK11. return ProbeResult.UNSUPPORTED_STORAGE; } } // Now check for a more accurate MIME type. - buffer.position(p + HEADER.length); - final ProbeResult result = new MimeTypeDetector(mimeForNameSpaces, mimeForRootElements) { + return new MimeTypeDetector(mimeForNameSpaces, mimeForRootElements) { @Override int read() { if (buffer.hasRemaining()) { return buffer.get(); @@ -139,14 +131,12 @@ public abstract class AbstractProvider extends DocumentedStoreProvider { return -1; } }.probeContent(); - buffer.position(p); - return result; - } + }; /* * We should enter in this block only if the user gave us explicitly a Reader. * A common case is a StringReader wrapping a String object. */ - return probeContent(connector, Reader.class, (reader) -> { + prober = prober.orElse(Reader.class, (reader) -> { // Quick check for "<?xml " header. for (int i=0; i<HEADER.length; i++) { if (reader.read() != HEADER[i]) { @@ -154,13 +144,13 @@ public abstract class AbstractProvider extends DocumentedStoreProvider { } } // Now check for a more accurate MIME type. - final ProbeResult result = new MimeTypeDetector(mimeForNameSpaces, mimeForRootElements) { + return new MimeTypeDetector(mimeForNameSpaces, mimeForRootElements) { private int remaining = READ_AHEAD_LIMIT; @Override int read() throws IOException { return (--remaining >= 0) ? IOUtilities.readCodePoint(reader) : -1; } }.probeContent(); - return result; }); + return probeContent(connector, ByteBuffer.class, prober); } } 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 fb6bab3..ec30d09 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 @@ -119,9 +119,9 @@ public abstract class DataStoreProvider { /** * The logger where to reports warnings or change events. Created when first needed and kept - * by strong reference for avoiding configuration lost if the logger if garbage collected. - * This strategy assumes that {@code URIDataStore.Provider} instances are kept alive for - * the duration of JVM lifetime, which is the case with {@link DataStoreRegistry}. + * by strong reference for avoiding configuration lost if the logger is garbage collected. + * This strategy assumes that {@code DataStoreProvider} instances are kept alive for the + * duration of JVM lifetime, which is the case with {@link DataStoreRegistry}. * * @see #getLogger() */ @@ -339,6 +339,7 @@ public abstract class DataStoreProvider { final Class<S> type, final Prober<? super S> prober) throws DataStoreException { ArgumentChecks.ensureNonNull("prober", prober); + boolean undetermined = false; /* * 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` @@ -346,90 +347,141 @@ public abstract class DataStoreProvider { * 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; + ProbeResult result = tryProber(connector, type, prober); + undetermined = (result == ProbeResult.UNDETERMINED); + if (result != null && !undetermined) { + return result; } - if (input == connector.storage && !StorageConnector.isSupportedType(type)) { + /* + * If the storage connector can not provide the type of source required by the specified prober, + * verify if there is any other probers specified by `Prober.orElse(…)`. + */ + Prober<?> next = prober; + while (next instanceof ProberList<?,?>) { + final ProberList<?,?> list = (ProberList<?,?>) next; + result = tryNextProber(connector, list); + if (result != null && result != ProbeResult.UNDETERMINED) { + return result; + } + undetermined |= (result == ProbeResult.UNDETERMINED); + next = list.next; + } + } + return undetermined ? ProbeResult.UNDETERMINED : ProbeResult.UNSUPPORTED_STORAGE; + } + + /** + * Tries the {@link ProberList#next} probe. This method is defined for type parameterization + * (the caller has only {@code <?>} and we need a specific type {@code <N>}). + * + * @param <N> type of input requested by the next probe. + * @param connector information about the storage (URL, stream, JDBC connection, <i>etc</i>). + * @param list root of the chained list of next probes. + */ + private <N> ProbeResult tryNextProber(final StorageConnector connector, final ProberList<?,N> list) throws DataStoreException { + return tryProber(connector, list.type, list.next); + } + + /** + * Implementation of {@link #probeContent(StorageConnector, Class, Prober)} + * for a single element in a list of probe. + * + * @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}, <i>etc</i>. + * @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 {@code null} if the given type is supported but no view can be created. + * @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 another kind of error occurred. + */ + private <S> ProbeResult tryProber(final StorageConnector connector, + final Class<S> type, final Prober<? super S> prober) throws DataStoreException + { + final S input = connector.getStorageAs(type); + if (input == null) { // Means that the given type is valid but not applicable for current storage. + return null; + } + 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 fixed to BIG_ENDIAN + * (the default) regardless the byte order of the original buffer. + */ + 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); + stream.reset(position); + } else if (input instanceof ImageInputStream) { /* - * 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. + * `ImageInputStream` supports an arbitrary number of marks as well, + * but we use absolute positioning for simplicity. */ - throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedType_1, type)); + 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 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)); + 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); } - 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 fixed to BIG_ENDIAN - * (the default) regardless the byte order of the original buffer. - */ - 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); - stream.reset(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 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)); - 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); + } 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); } - return result; + throw new CanNotProbeException(this, connector, e); } + return result; } /** @@ -461,6 +513,64 @@ public abstract class DataStoreProvider { * @throws Exception if an error occurred during the execution of the probe action. */ ProbeResult test(S input) throws Exception; + + /** + * Returns a composed probe that attempts, in sequence, this probe followed by the alternative probe + * if the first probe can not be executed. The alternative probe is tried if and only if one of the + * following conditions is true: + * + * <ul> + * <li>The storage connector can not provide an input of the type requested by this probe.</li> + * <li>This probe {@link #test(S)} method returned {@link ProbeResult#UNDETERMINED}.</li> + * </ul> + * + * If any probe throws an exception, the exception is propagated + * (the alternative probe is not a fallback executed if this probe threw an exception). + * + * @param <A> 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}, <i>etc</i>. + * @param alternative the test to apply on the source of the given type. + * @return a composed probe that attempts the given probe if this probe can not be executed. + */ + default <A> Prober<S> orElse(final Class<A> type, final Prober<? super A> alternative) { + return new ProberList<>(this, type, alternative); + } + } + + /** + * Implementation of the composed probe returned by {@link Prober#orElse(Class, Prober)}. + * Instances of this class a nodes in a linked list. + * + * @param <S> the source type of the original probe. + * @param <N> the source type of the next probe to try as an alternative. + */ + private static final class ProberList<S,N> implements Prober<S> { + /** The main probe to try first. */ + private final Prober<S> first; + + /** The probe to try next if the {@linkplain #first} probe can not be executed. */ + Prober<? super N> next; + + /** Type of input expected by the {@linkplain #next} probe. */ + final Class<N> type; + + /** Creates a new composed probe as a root node of a linked list. */ + ProberList(final Prober<S> first, final Class<N> type, final Prober<? super N> next) { + this.first = first; + this.type = type; + this.next = next; + } + + /** Forward to the primary probe. */ + @Override public ProbeResult test(final S input) throws Exception { + return first.test(input); + } + + /** Appends a new probe alternative at the end of this linked list. */ + @Override public <A> Prober<S> orElse(final Class<A> type, final Prober<? super A> prober) { + next = next.orElse(type, prober); + return this; + } } /** 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 c423504..539f3e5 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 @@ -65,20 +65,20 @@ public final strictfp class DataStoreProviderTest extends TestCase { * 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 -> { + assertEquals(ProbeResult.SUPPORTED, 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 -> { + assertEquals(ProbeResult.SUPPORTED, provider.probeContent(connector, Reader.class, stream -> { StorageConnectorTest.assertExpectedChars(stream); return ProbeResult.SUPPORTED; - }), ProbeResult.SUPPORTED); + })); } /** @@ -99,13 +99,13 @@ public final strictfp class DataStoreProviderTest extends TestCase { * Verify that the byte buffer given to the prober always have the big endian order, * regardless the byte order of the original buffer. This is part of method contract. */ - assertEquals(provider.probeContent(connector, ByteBuffer.class, buffer -> { + assertEquals(ProbeResult.UNDETERMINED, provider.probeContent(connector, ByteBuffer.class, buffer -> { assertEquals(ByteOrder.BIG_ENDIAN, buffer.order()); assertEquals(3, buffer.position()); assertEquals(8, buffer.limit()); buffer.position(5).mark(); return ProbeResult.UNDETERMINED; - }), ProbeResult.UNDETERMINED); + })); /* * Verifies that the origial buffer has its byte order and position unchanged. */ @@ -126,23 +126,23 @@ public final strictfp class DataStoreProviderTest extends TestCase { * without resetting the buffer position. */ final StorageConnector connector = StorageConnectorTest.create(false); - assertEquals(provider.probeContent(connector, ByteBuffer.class, buffer -> { + assertEquals(ProbeResult.UNDETERMINED, 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(ProbeResult.SUPPORTED, 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); + })); } /** @@ -174,7 +174,7 @@ public final strictfp class DataStoreProviderTest extends TestCase { * 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(ProbeResult.SUPPORTED, provider.probeContent(connector, InputStream.class, stream -> { assertEquals(!asStream, stream.markSupported()); stream.skip(5); stream.mark(10); @@ -188,7 +188,7 @@ public final strictfp class DataStoreProviderTest extends TestCase { 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. @@ -253,7 +253,7 @@ public final strictfp class DataStoreProviderTest extends TestCase { /* * Read a few bytes and verify that user can not overwrite the mark. */ - assertEquals(provider.probeContent(connector, Reader.class, stream -> { + assertEquals(ProbeResult.SUPPORTED, provider.probeContent(connector, Reader.class, stream -> { assertEquals(buffered, stream instanceof BufferedReader); assertFalse(stream.markSupported()); stream.skip(5); @@ -264,7 +264,7 @@ public final strictfp class DataStoreProviderTest extends TestCase { 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.