You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/01/10 00:48:08 UTC
[sis] branch geoapi-4.0 updated: Add a `Prober.orElse(…)` method for testing probers with different types.
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 <ma...@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.