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 b03b235825 Add a `DataStores.openWritable(…)` method.
b03b235825 is described below
commit b03b2358250885ea88fd64cb451c2543b619e01d
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Sep 25 17:45:10 2023 +0200
Add a `DataStores.openWritable(…)` method.
https://issues.apache.org/jira/browse/SIS-571
---
.../org/apache/sis/io/stream/ChannelFactory.java | 22 ++-
.../main/org/apache/sis/io/stream/IOUtilities.java | 18 +++
.../apache/sis/io/stream/InternalOptionKey.java | 12 +-
.../org/apache/sis/storage/DataStoreProvider.java | 15 +-
.../org/apache/sis/storage/DataStoreRegistry.java | 152 +++++++++++++++------
.../main/org/apache/sis/storage/DataStores.java | 49 ++++++-
.../org/apache/sis/storage/ProbeProviderPair.java | 37 ++++-
.../main/org/apache/sis/storage/ProbeResult.java | 2 +-
.../org/apache/sis/storage/StorageConnector.java | 96 ++++++++++++-
.../apache/sis/storage/base/StoreUtilities.java | 10 +-
.../apache/sis/storage/image/DataStoreFilter.java | 83 +++++++++++
.../org/apache/sis/storage/image/FormatFilter.java | 14 +-
.../org/apache/sis/storage/image/FormatFinder.java | 34 +++--
.../sis/storage/image/WorldFileStoreProvider.java | 4 +-
.../storage/image/WritableSingleImageStore.java | 4 +-
.../apache/sis/storage/image/WritableStore.java | 6 +-
.../apache/sis/gui/internal/io/FileAccessView.java | 12 +-
17 files changed, 476 insertions(+), 94 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelFactory.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelFactory.java
index 567d862b9d..4153019de3 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelFactory.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelFactory.java
@@ -133,6 +133,8 @@ public abstract class ChannelFactory {
* @param options the options to use for creating a new byte
channel. Can be null or empty for read-only.
* @return the channel factory for the given input, or {@code null} if the
given input is of unknown type.
* @throws IOException if an error occurred while processing the given
input.
+ *
+ * @see IOUtilities#isWriteOnly(Object)
*/
public static ChannelFactory prepare(Object storage, final boolean
allowWriteOnly,
final String encoding, final OpenOption[] options) throws
IOException
@@ -286,6 +288,11 @@ public abstract class ChannelFactory {
@Override public WritableByteChannel writable(String
filename, StoreListeners listeners) throws IOException {
return Files.newByteChannel(path, optionSet);
}
+ @Override public boolean isCreateNew() {
+ if (optionSet.contains(StandardOpenOption.CREATE_NEW))
return true;
+ if (optionSet.contains(StandardOpenOption.CREATE))
return Files.notExists(path);
+ return false;
+ }
};
}
}
@@ -311,10 +318,21 @@ public abstract class ChannelFactory {
*
* @return whether {@link #readable readable(…)} or {@link #writable
writable(…)} can be invoked.
*/
- public boolean canOpen() {
+ public boolean canReopen() {
return true;
}
+ /**
+ * Returns {@code true} if opening the channel will create a new,
initially empty, file.
+ * This is {@code true} only if the storage is some supported kind of file
or URL and
+ * this factory has been created with an option that allows file creation.
+ *
+ * @return whether opening a channel will create a new file.
+ */
+ public boolean isCreateNew() {
+ return false;
+ }
+
/**
* Returns the readable channel as an input stream. The returned stream is
<strong>not</strong> buffered;
* it is caller's responsibility to wrap the stream in a {@link
java.io.BufferedInputStream} if desired.
@@ -418,7 +436,7 @@ public abstract class ChannelFactory {
* Returns whether {@link #readable readable(…)} or {@link #writable
writable(…)} can be invoked.
*/
@Override
- public boolean canOpen() {
+ public boolean canReopen() {
return storage != null;
}
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java
index d47ad79c7c..7db901775e 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java
@@ -21,6 +21,8 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.LineNumberReader;
import java.io.Reader;
+import java.io.DataInput;
+import java.io.DataOutput;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
@@ -640,6 +642,22 @@ public final class IOUtilities extends Static {
return false;
}
+ /**
+ * Returns {@code true} if the given object is an output stream with no
read capability.
+ *
+ * @param output the object to test, or {@code null}.
+ * @return whether the given object is write-only.
+ */
+ public static boolean isWriteOnly(final Object output) {
+ if (output instanceof DataInput || output instanceof
ReadableByteChannel) {
+ return false;
+ }
+ return (output instanceof OutputStream) ||
+ (output instanceof DataOutput) ||
+ (output instanceof ChannelDataOutput) ||
+ (output instanceof WritableByteChannel);
+ }
+
/**
* Returns {@code true} if the given options would open a file mostly for
writing.
* This method returns {@code true} if the following conditions are true:
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InternalOptionKey.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InternalOptionKey.java
index 9701231888..83e5685904 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InternalOptionKey.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InternalOptionKey.java
@@ -16,9 +16,11 @@
*/
package org.apache.sis.io.stream;
+import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import org.apache.sis.setup.OptionKey;
import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreProvider;
/**
@@ -26,7 +28,7 @@ import org.apache.sis.storage.StorageConnector;
* Some of those options may move to public API in the future if useful.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
*
* @param <T> the type of option values.
*
@@ -38,6 +40,14 @@ public final class InternalOptionKey<T> extends OptionKey<T>
{
*/
private static final long serialVersionUID = 1786137598411493790L;
+ /**
+ * A filter for trying preferred data stores first. A typical usage is for
selecting data
+ * store providers based on their {@linkplain
DataStoreProvider#getShortName() format name}.
+ */
+ @SuppressWarnings("unchecked")
+ public static final InternalOptionKey<Predicate<DataStoreProvider>>
PREFERRED_PROVIDERS =
+ (InternalOptionKey) new InternalOptionKey<>("PREFERRED_PROVIDERS",
Predicate.class);
+
/**
* Wraps readable or writable channels on creation. Wrappers can be used
for example
* in order to listen to read events or for transforming bytes on the fly.
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreProvider.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreProvider.java
index 303ec7f00b..37d7c52903 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreProvider.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreProvider.java
@@ -119,8 +119,7 @@ public abstract class DataStoreProvider {
/**
* Returns a short name or abbreviation for the data format.
* This name is used in some warnings or exception messages.
- * It may contain any characters, including white spaces
- * (i.e. this short name is <strong>not</strong> a format identifier).
+ * It may contain any characters, including white spaces, and is not
guaranteed to be unique.
* For a more comprehensive format name, see {@link #getFormat()}.
*
* <h4>Examples</h4>
@@ -338,7 +337,7 @@ public abstract class DataStoreProvider {
*/
Prober<?> next = prober;
while (next instanceof ProberList<?,?>) {
- final ProberList<?,?> list = (ProberList<?,?>) next;
+ final var list = (ProberList<?,?>) next;
result = tryNextProber(connector, list);
if (result != null && result != ProbeResult.UNDETERMINED) {
return result;
@@ -380,7 +379,15 @@ public abstract class DataStoreProvider {
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.
+ if (input == null) {
+ /*
+ * Means one of the following:
+ * - The storage is a file that do not exist yet but can be
created by this provider.
+ * - The given type is valid but not applicable with the
`StorageConnector` content.
+ */
+ if (connector.probing != null) {
+ return connector.probing.probe;
+ }
return null;
}
if (input == connector.storage &&
!StorageConnector.isSupportedType(type)) {
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreRegistry.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreRegistry.java
index de0d35bcaf..91f0c46788 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreRegistry.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreRegistry.java
@@ -16,14 +16,19 @@
*/
package org.apache.sis.storage;
-import java.util.List;
import java.util.LinkedList;
import java.util.Iterator;
import java.util.ServiceLoader;
+import java.util.function.Predicate;
+import java.nio.file.StandardOpenOption;
+import org.apache.sis.io.stream.IOUtilities;
+import org.apache.sis.io.stream.InternalOptionKey;
+import org.apache.sis.setup.OptionKey;
import org.apache.sis.system.Reflect;
import org.apache.sis.system.Modules;
import org.apache.sis.system.SystemListener;
import org.apache.sis.storage.internal.Resources;
+import org.apache.sis.storage.base.Capability;
import org.apache.sis.storage.base.StoreMetadata;
import org.apache.sis.referencing.util.LazySet;
import org.apache.sis.util.ArgumentChecks;
@@ -104,7 +109,7 @@ final class DataStoreRegistry extends
LazySet<DataStoreProvider> {
*/
public String probeContentType(final Object storage) throws
DataStoreException {
ArgumentChecks.ensureNonNull("storage", storage);
- final ProbeProviderPair p = lookup(storage, false);
+ final ProbeProviderPair p = lookup(storage, Capability.READ, null,
false);
return (p != null) ? p.probe.getMimeType() : null;
}
@@ -122,85 +127,114 @@ final class DataStoreRegistry extends
LazySet<DataStoreProvider> {
* <li>An existing {@link StorageConnector} instance.</li>
* </ul>
*
- * @param storage the input/output object as a URL, file, image input
stream, <i>etc.</i>.
+ * @param storage the input/output object as a URL, file, image input
stream, <i>etc.</i>.
+ * @param capability the capability that the data store must have (read,
write, create).
+ * @param preferred a filter for selecting the providers to try first,
or {@code null}.
* @return the object to use for reading geospatial data from the given
storage.
* @throws UnsupportedStorageException if no {@link DataStoreProvider} is
found for a given storage object.
* @throws DataStoreException if an error occurred while opening the
storage.
*/
- public DataStore open(final Object storage) throws
UnsupportedStorageException, DataStoreException {
+ public DataStore open(Object storage, Capability capability,
Predicate<DataStoreProvider> preferred)
+ throws UnsupportedStorageException, DataStoreException
+ {
ArgumentChecks.ensureNonNull("storage", storage);
- return lookup(storage, true).store;
+ return lookup(storage, capability, preferred, true).store;
}
/**
- * The kind of providers to test. The provider are divided in 4 categories
depending on whether
+ * The kind of providers to test. The provider are divided in 5 categories
depending on whether
* the file suffix matches the suffix expected by the provider, and
whether the provider should
* be tested last for giving a chance to specialized providers to open the
file.
*/
private enum Category {
- /** The provider can be tested now and the file suffix matches. */
- SUFFIX_MATCH(true, false),
+ /** Providers selected using preference filter and file suffix. */
+ PREFERRED(true, true, false),
+
+ /** Providers selected using the preference filter only. */
+ PREFERRED_IGNORE_SUFFIX(true, false, false),
+
+ /** Non-deferred providers selected using file suffix. */
+ SUFFIX_MATCH(false, true, false),
- /** The provider could be tested now but the file suffix does not
match. */
- IGNORE_SUFFIX(false, false),
+ /** All others non-deferred providers. */
+ IGNORE_SUFFIX(false, false, false),
- /** The provider should be tested last, but the file suffix matches. */
- DEFERRED(true, true),
+ /** Providers to be tested last, filtered by file suffix. */
+ DEFERRED(false, true, true),
- /** The provider should be tested last because it is too generic. */
- DEFERRED_IGNORE_SUFFIX(false, true);
+ /** Providers tested last because too generic. */
+ DEFERRED_IGNORE_SUFFIX(false, false, true);
- /** Whether this category checks if suffix matches. */
+ /** Whether this category uses the preference filter. */
+ final boolean preferred;
+
+ /** Whether this category checks if the suffix matches. */
final boolean useSuffix;
- /** Whether this category is for providers to test last. */
+ /** Whether this category is for providers to test in last resort. */
final boolean yieldPriority;
/** Creates a new enumeration value. */
- private Category(final boolean useSuffix, final boolean yieldPriority)
{
+ private Category(final boolean preferred, final boolean useSuffix,
final boolean yieldPriority) {
+ this.preferred = preferred;
this.useSuffix = useSuffix;
this.yieldPriority = yieldPriority;
}
-
- /** Returns the categories, optionally ignoring file suffix. */
- static Category[] values(final boolean useSuffix) {
- return useSuffix ? values() : new Category[] {IGNORE_SUFFIX,
DEFERRED_IGNORE_SUFFIX};
- }
}
/**
- * Implementation of {@link #probeContentType(Object)} and {@link
#open(Object)}.
+ * Implementation of {@link #probeContentType(Object)} and {@link
#open(Object, Capability, String)}.
*
- * @param storage the input/output object as a URL, file, image input
stream, <i>etc.</i>.
- * @param open {@code true} for creating a {@link DataStore}, or
{@code false} if not needed.
+ * @param storage the input/output object as a URL, file, image input
stream, <i>etc.</i>.
+ * @param capability the capability that the data store must have (read,
write, create).
+ * @param preferred a filter for selecting the providers to try first,
or {@code null}.
+ * @param open {@code true} for creating a {@link DataStore}, or
{@code false} if not needed.
* @return the result, or {@code null} if the format is not recognized and
{@code open} is {@code false}.
* @throws UnsupportedStorageException if no {@link DataStoreProvider} is
found for a given storage object.
* @throws DataStoreException if an error occurred while opening the
storage.
*
* @todo Iterate on {@code ServiceLoader.Provider.type()} on JDK9.
+ * However the use of {@code Stream} is not convenience because of
the need to synchronize.
+ * Ideally, we would want the {@code Iterator} that {@code
ServiceLoader} is creating anyway.
*/
- private ProbeProviderPair lookup(final Object storage, final boolean open)
throws DataStoreException {
+ private ProbeProviderPair lookup(final Object storage, final Capability
capability,
+ Predicate<DataStoreProvider> preferred, final boolean open)
+ throws DataStoreException
+ {
StorageConnector connector; // Will be reset to `null`
if it shall not be closed.
if (storage instanceof StorageConnector) {
connector = (StorageConnector) storage;
+ if (preferred == null) {
+ preferred =
connector.getOption(InternalOptionKey.PREFERRED_PROVIDERS);
+ }
} else {
connector = new StorageConnector(storage);
+ if (capability == Capability.WRITE) {
+ connector.setOption(OptionKey.OPEN_OPTIONS, new
StandardOpenOption[] {
+ StandardOpenOption.CREATE, StandardOpenOption.WRITE
+ });
+ }
+ if (preferred != null) {
+ connector.setOption(InternalOptionKey.PREFERRED_PROVIDERS,
preferred);
+ }
}
/*
- * If we can get a filename extension from the given storage (file,
URL, etc.), then we may perform two iterations
- * on the provider list. The first iteration will use only the
providers which declare capability to read files of
- * that suffix (Category.SUFFIX_MATCH). Only if no provider has been
able to read that file, we will do a second
- * iteration on other providers (Category.IGNORE_SUFFIX). The intent
is to avoid DataStoreProvider.probeContent(…)
- * invocations loading large dependencies.
+ * If we can get a filename extension from the given storage (file,
URL, etc.), we may perform two times
+ * more iterations on the provider list. One serie of iterations will
use only the providers that declare
+ * capability to read or write files of that suffix
(Category.SUFFIX_MATCH). Only if no provider has been
+ * able to read or write that file, we will do another iteration on
other providers (Category.IGNORE_SUFFIX).
+ * The intent is to avoid DataStoreProvider.probeContent(…)
invocations loading large dependencies.
*/
- final String extension = connector.getFileExtension();
- final boolean useSuffix = !(extension == null ||
extension.isEmpty());
- final Category[] categories = Category.values(useSuffix);
- ProbeProviderPair selected = null;
- final List<ProbeProviderPair> needMoreBytes = new LinkedList<>();
+ final String extension = connector.getFileExtension();
+ final boolean useSuffix = !(extension == null ||
extension.isEmpty());
+ final boolean isWriteOnly = (capability == Capability.WRITE) &&
IOUtilities.isWriteOnly(connector.getStorage());
+ ProbeProviderPair selected = null;
+ final var needMoreBytes = new LinkedList<ProbeProviderPair>();
try {
-search: for (int ci=0; ci < categories.length; ci++) {
- final Category category = categories[ci];
+ boolean isFirstIteration = true;
+search: for (final Category category : Category.values()) {
+ if (category.preferred && (preferred == null)) continue;
+ if (category.useSuffix && !useSuffix) continue;
/*
* All usages of `loader` and its `providers` iterator must be
protected in a synchronized block,
* because ServiceLoader is not thread-safe. We try to keep
the synhronization block as small as
@@ -222,21 +256,48 @@ search: for (int ci=0; ci < categories.length; ci++) {
boolean accept;
final StoreMetadata md =
provider.getClass().getAnnotation(StoreMetadata.class);
if (md == null) {
- accept = (ci == 0); // If no metadata, test
only in first iteration.
+ accept = isFirstIteration; // If no metadata,
test only during one iteration.
} else {
- accept = (md.yieldPriority() ==
category.yieldPriority);
+ accept = (md.yieldPriority() ==
category.yieldPriority) &&
+ ArraysExt.contains(md.capabilities(),
capability);
if (accept & useSuffix) {
accept =
ArraysExt.containsIgnoreCase(md.fileSuffixes(), extension) ==
category.useSuffix;
}
}
+ if (accept & (preferred != null)) {
+ accept = (preferred.test(provider) ==
category.preferred);
+ }
+ /*
+ * At this point, it has been determined whether the
provider should be tested in current iteration.
+ * If accepted, perform now the probing operation for
checking if the current provider is suitable.
+ * The `connector.probing` field is set to a non-null
value for telling `StorageConnector` to not
+ * create empty file if the file does not exist (it has no
effect in read-only mode).
+ */
if (accept) {
- final ProbeResult probe =
provider.probeContent(connector);
+ final var candidate = new ProbeProviderPair(provider);
+ if (isWriteOnly) {
+ /*
+ * We cannot probe a write-only storage. Rely on
the filtering done before this block,
+ * which was based on format name and file suffix,
and use the first filtered provider.
+ */
+ selected = candidate;
+ break search;
+ }
+ final ProbeProviderPair old = connector.probing;
+ final ProbeResult probe;
+ try {
+ connector.probing = candidate;
+ probe = provider.probeContent(connector);
+ } finally {
+ connector.probing = old;
+ }
+ candidate.probe = probe;
if (probe.isSupported()) {
/*
* Stop at the first provider claiming to be able
to read the storage.
* Do not iterate over the list of deferred
providers (if any).
*/
- selected = new ProbeProviderPair(provider, probe);
+ selected = candidate;
break search;
}
if (ProbeResult.INSUFFICIENT_BYTES.equals(probe)) {
@@ -245,7 +306,7 @@ search: for (int ci=0; ci < categories.length; ci++) {
* try again after this loop with more bytes in
the buffer, unless we
* found another provider.
*/
- needMoreBytes.add(new ProbeProviderPair(provider,
probe));
+ needMoreBytes.add(candidate);
} else if (ProbeResult.UNDETERMINED.equals(probe)) {
/*
* If a provider doesn't know whether it can open
the given storage,
@@ -254,7 +315,7 @@ search: for (int ci=0; ci < categories.length; ci++) {
* one for the file extension of the given storage.
*/
if (selected == null) {
- selected = new ProbeProviderPair(provider,
probe);
+ selected = candidate;
}
}
}
@@ -286,8 +347,9 @@ search: for (int ci=0; ci < categories.length; ci++) {
/*
* If we filtered providers by the file extension without
finding a suitable provider,
* try again with all other providers (even if they are for
another file extension).
- * We do that by changing moving to the next `Category`.
+ * We do that by moving to the next `Category`.
*/
+ isFirstIteration = false;
}
/*
* If a provider has been found, or if a provider returned
UNDETERMINED, use that one
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStores.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStores.java
index df01c0e5a6..7a6c56edd1 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStores.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStores.java
@@ -17,7 +17,10 @@
package org.apache.sis.storage;
import java.util.Collection;
+import java.util.function.Predicate;
import org.apache.sis.util.Static;
+import org.apache.sis.storage.base.Capability;
+import org.apache.sis.storage.image.DataStoreFilter;
/**
@@ -62,7 +65,7 @@ public final class DataStores extends Static {
}
/**
- * Creates a {@link DataStore} for the given storage.
+ * Creates a {@link DataStore} capable to read the given storage.
* The {@code storage} argument can be any of the following types:
*
* <ul>
@@ -75,12 +78,48 @@ public final class DataStores extends Static {
* <li>An existing {@link StorageConnector} instance.</li>
* </ul>
*
- * @param storage the input/output object as a URL, file, image input
stream, <i>etc.</i>.
+ * @param storage the input object as a URL, file, image input stream,
<i>etc.</i>.
* @return the object to use for reading geospatial data from the given
storage.
- * @throws UnsupportedStorageException if no {@link DataStoreProvider} is
found for a given storage object.
- * @throws DataStoreException if an error occurred while opening the
storage.
+ * @throws UnsupportedStorageException if no {@link DataStoreProvider} is
found for the given storage object.
+ * @throws DataStoreException if an error occurred while opening the
storage in read mode.
*/
public static DataStore open(final Object storage) throws
UnsupportedStorageException, DataStoreException {
- return DataStoreRegistry.INSTANCE.open(storage);
+ return DataStoreRegistry.INSTANCE.open(storage, Capability.READ, null);
+ }
+
+ /**
+ * Creates a {@link DataStore} capable to write or update the given
storage.
+ * The {@code storage} argument can be any of the types documented in
{@link #open(Object)}.
+ * If the storage is a file and that file does not exist, then a new file
will be created.
+ * If the storage exists, then it will be opened in read/write mode for
updates.
+ * The returned data store should implement the {@link
WritableGridCoverageResource},
+ * {@link WritableFeatureSet} or {@link WritableAggregate} interface.
+ *
+ * <h4>Format selection</h4>
+ * The {@code preferredFormat} argument can be a {@linkplain
DataStoreProvider#getShortName() data store name}
+ * (examples: {@code "CSV"}, {@code "GPX"}) or an {@linkplain
javax.imageio.ImageIO Image I/O} name
+ * (examples: {@code "TIFF"}, {@code "PNG"}). In the latter case, the
WorldFile convention is used.
+ *
+ * <p>If the given storage exists (for example, an existing file), then
the {@link DataStoreProvider} is determined
+ * by probing the existing content and the {@code preferredFormat}
argument may be ignored (it can be {@code null}).
+ * Otherwise the {@link DataStoreProvider} is selected by a combination of
{@code preferredFormat} (if non-null) and
+ * file suffix (if the storage is a file path or URI).</p>
+ *
+ * @param storage the input/output object as a URL, file, image
input stream, <i>etc.</i>.
+ * @param preferredFormat the format to use if not determined by the
existing content, or {@code null}.
+ * @return the object to use for writing geospatial data in the given
storage.
+ * @throws UnsupportedStorageException if no {@link DataStoreProvider} is
found for the given storage object.
+ * @throws DataStoreException if an error occurred while opening the
storage in write mode.
+ *
+ * @since 1.4
+ */
+ public static DataStore openWritable(final Object storage, final String
preferredFormat)
+ throws UnsupportedStorageException, DataStoreException
+ {
+ Predicate<DataStoreProvider> preferred = null;
+ if (preferredFormat != null) {
+ preferred = new DataStoreFilter(preferredFormat, true);
+ }
+ return DataStoreRegistry.INSTANCE.open(storage, Capability.WRITE,
preferred);
}
}
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/ProbeProviderPair.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/ProbeProviderPair.java
index aba01c412b..4aaf89eba6 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/ProbeProviderPair.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/ProbeProviderPair.java
@@ -16,13 +16,18 @@
*/
package org.apache.sis.storage;
+import java.nio.file.StandardOpenOption;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.storage.base.Capability;
+import org.apache.sis.storage.base.StoreMetadata;
+
/**
* A pair of {@link ProbeResult} and {@link DataStoreProvider}, for internal
usage by {@link DataStoreRegistry} only.
- * Provides also a {@link DataStore} created by the provider if this class is
used for an {@code open} operation.
+ * Provides also a {@link DataStore} created by the provider if this class is
used for an {@code open(…)} operation.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.4
+ * @version 1.4
* @since 0.4
*/
final class ProbeProviderPair {
@@ -43,10 +48,32 @@ final class ProbeProviderPair {
DataStore store;
/**
- * Creates a new pair.
+ * Creates a new pair with a result not yet known.
*/
- ProbeProviderPair(final DataStoreProvider provider, final ProbeResult
probe) {
+ ProbeProviderPair(final DataStoreProvider provider) {
this.provider = provider;
- this.probe = probe;
+ }
+
+ /**
+ * Sets the {@linkplain #probe} result for a file that does not exist yet.
+ * The result will be {@link ProbeResult#SUPPORTED} or {@code
UNSUPPORTED_STORAGE},
+ * depending on whether the {@linkplain #provider} supports the creation
of new storage.
+ * In both cases, {@link StorageConnector#wasProbingAbsentFile()} will
return {@code true}.
+ *
+ * <p>This method is invoked for example if the storage is a file, the
file does not exist
+ * but {@link StandardOpenOption#CREATE} or {@link
StandardOpenOption#CREATE_NEW CREATE_NEW}
+ * option was provided and the data store has write capability. Note
however that declaring
+ * {@code SUPPORTED} is not a guarantee that the data store will
successfully create the resource.
+ * For example we do not verify if the file system grants write permission
to the application.</p>
+ *
+ * @see StorageConnector#wasProbingAbsentFile()
+ */
+ final void setProbingAbsentFile() {
+ final StoreMetadata md =
provider.getClass().getAnnotation(StoreMetadata.class);
+ if (md == null || ArraysExt.contains(md.capabilities(),
Capability.CREATE)) {
+ probe = ProbeResult.SUPPORTED;
+ } else {
+ probe = ProbeResult.UNSUPPORTED_STORAGE;
+ }
}
}
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/ProbeResult.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/ProbeResult.java
index 72cf0c7c1b..4978fe7681 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/ProbeResult.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/ProbeResult.java
@@ -45,7 +45,7 @@ import org.apache.sis.util.internal.Strings;
* In such cases, SIS will revisit those providers only if no better suited
provider is found.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.4
+ * @version 1.4
*
* @see DataStoreProvider#probeContent(StorageConnector)
*
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java
index 326bfbfceb..a64c444152 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java
@@ -263,6 +263,18 @@ public class StorageConnector implements Serializable {
@SuppressWarnings("serial") // Not statically typed as
Serializable.
private Map<OptionKey<?>, Object> options;
+ /**
+ * If a probing operation is ongoing, the provider doing the operation.
Otherwise {@code null}.
+ * This information is needed because if the storage is a file and that
file does not exist,
+ * then the {@code StorageConnector} behavior depends on whether the
caller is probing or not.
+ * If probing, {@link ProbeResult#SUPPORTED} should be returned without
creating the file,
+ * because attempt to probe that file would cause an {@link
java.io.EOFException}.
+ * If not probing, then an empty file should be created.
+ *
+ * @see #wasProbingAbsentFile()
+ */
+ transient ProbeProviderPair probing;
+
/**
* Views of {@link #storage} as instances of types different than the type
of the object given to the constructor.
* The {@code null} reference can appear in various places:
@@ -815,6 +827,8 @@ public class StorageConnector implements Serializable {
* 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.
+ * In the latter case, {@link #wasProbingAbsentFile()} can be
invoked for determining whether the
+ * reason is that the file does not exist but could be created.
* @throws IllegalArgumentException if the given {@code type} argument is
not one of the supported types
* listed in this javadoc or in subclass javadoc.
* @throws IllegalStateException if this {@code StorageConnector} has been
{@linkplain #closeAllExcept closed}.
@@ -960,6 +974,41 @@ public class StorageConnector implements Serializable {
}
}
+ /**
+ * Returns whether returning the storage would have required the creation
of a new file.
+ * This method may return {@code true} if all the following conditions are
true:
+ *
+ * <ul>
+ * <li>A previous {@link #getStorageAs(Class)} call requested some kind
of input stream
+ * (e.g. {@link InputStream}, {@link ImageInputStream}, {@link
DataInput}, {@link Reader}).</li>
+ * <li>The {@linkplain #getStorage() storage} is an object convertible
to a {@link Path} and the
+ * file identified by that path {@linkplain
java.nio.file.Files#notExists does not exist}.</li>
+ * <li>The {@linkplain #getOption(OptionKey) optons} given to this
{@code StorageConnector} include
+ * {@link java.nio.file.StandardOpenOption#CREATE} or {@code
CREATE_NEW}.</li>
+ * <li>The {@code getStorageAs(…)} and {@code wasProbingAbsentFile()}
calls happened in the context of
+ * {@link DataStores} probing the storage content in order to choose
a {@link DataStoreProvider}.</li>
+ * </ul>
+ *
+ * If all above conditions are true, then {@link #getStorageAs(Class)}
returns {@code null} instead of creating
+ * a new empty file. In such case, {@link DataStoreProvider} may use this
{@code wasProbingAbsentFile()} method
+ * for deciding whether to report {@link ProbeResult#SUPPORTED} or {@link
ProbeResult#UNSUPPORTED_STORAGE}.
+ *
+ * <h4>Rational</h4>
+ * When the file does not exist and the {@code CREATE} or {@code
CREATE_NEW} option is provided,
+ * {@code getStorageAs(…)} would normally create a new empty file. However
this behavior is modified during probing
+ * (the first condition in above list) because newly created files are
empty and probing empty files may result in
+ * {@link java.io.EOFException} to be thrown or in providers declaring
that they do not support the storage.
+ *
+ * <p>IF the {@code CREATE} or {@code CREATE_NEW} options were not
provided, then probing the storage content of an
+ * absent file will rather throw {@link java.nio.file.NoSuchFileException}
or {@link java.io.FileNotFoundException}.
+ * So this method is useful only for {@link DataStore} having write
capabilities.</p>
+ *
+ * @since 1.4
+ */
+ public boolean wasProbingAbsentFile() {
+ return probing != null && probing.probe != null;
+ }
+
/**
* Creates a view for the input as a {@link ChannelDataInput} if possible.
* This is also a starting point for {@link #createDataInput()} and {@link
#createByteBuffer()}.
@@ -967,6 +1016,7 @@ public class StorageConnector implements Serializable {
* {@code StorageConnector} instance.
*
* @param asImageInputStream whether the {@code ChannelDataInput} needs
to be {@link ChannelImageInputStream} subclass.
+ * @return input channel, or {@code null} if none or if {@linkplain
#probing} result has been determined offline.
* @throws IOException if an error occurred while opening a channel for
the input.
*
* @see #createChannelDataOutput()
@@ -995,6 +1045,16 @@ public class StorageConnector implements Serializable {
if (factory == null) {
return null;
}
+ /*
+ * If the storage is a file, that file does not exist, the open
options include `CREATE` or `CREATE_NEW`
+ * and this method is invoked for probing the file content (not yet
for creating the data store), then
+ * set the probe result without opening the file. We do that because
attempts to probe a newly created
+ * file would probably cause an EOFException to be thrown.
+ */
+ if (probing != null && factory.isCreateNew()) {
+ probing.setProbingAbsentFile();
+ return null;
+ }
/*
* ChannelDataInput depends on ReadableByteChannel, which itself
depends on storage
* (potentially an InputStream). We need to remember this chain in
`Coupled` objects.
@@ -1014,7 +1074,7 @@ public class StorageConnector implements Serializable {
* Following is an undocumented mechanism for allowing some Apache SIS
implementations of DataStore
* to re-open the same channel or input stream another time, typically
for re-reading the same data.
*/
- if (factory.canOpen()) {
+ if (factory.canReopen()) {
addView(ChannelFactory.class, factory);
}
return asDataInput;
@@ -1029,6 +1089,7 @@ public class StorageConnector implements Serializable {
* <p>This method is one of the {@link #OPENERS} methods and should be
invoked at most once per
* {@code StorageConnector} instance.</p>
*
+ * @return input stream, or {@code null} if none or if {@linkplain
#probing} result has been determined offline.
* @throws IOException if an error occurred while opening a stream for the
input.
*
* @see #createDataOutput()
@@ -1056,6 +1117,8 @@ public class StorageConnector implements Serializable {
c.view = asDataInput;
}
views.put(DataInput.class, c); // Share
the same Coupled instance.
+ } else if (wasProbingAbsentFile()) {
+ return null; // Do not
cache, for allowing file creation later.
} else {
reset();
try {
@@ -1110,6 +1173,7 @@ public class StorageConnector implements Serializable {
* <p>This method is one of the {@link #OPENERS} methods and should be
invoked at most once per
* {@code StorageConnector} instance.</p>
*
+ * @return buffer containing at least the first bytes of storage content,
or {@code null} if none.
* @throws IOException if an error occurred while opening a stream for the
input.
*/
private ByteBuffer createByteBuffer() throws IOException,
DataStoreException {
@@ -1123,6 +1187,8 @@ public class StorageConnector implements Serializable {
ByteBuffer asByteBuffer = null;
if (c != null) {
asByteBuffer = c.buffer.asReadOnlyBuffer();
+ } else if (wasProbingAbsentFile()) {
+ return null; // Do not cache, for allowing file
creation when opening the data store.
} else {
/*
* If no ChannelDataInput has been created by the above code, get
the input as an ImageInputStream and
@@ -1209,6 +1275,8 @@ public class StorageConnector implements Serializable {
*
* <p>This method is one of the {@link #OPENERS} methods and should be
invoked at most once per
* {@code StorageConnector} instance.</p>
+ *
+ * @return input stream, or {@code null} if none or if {@linkplain
#probing} result has been determined offline.
*/
private ImageInputStream createImageInputStream() throws
DataStoreException {
final Class<DataInput> source = DataInput.class;
@@ -1216,7 +1284,7 @@ public class StorageConnector implements Serializable {
if (input instanceof ImageInputStream) {
views.put(ImageInputStream.class, views.get(source));
// Share the same Coupled instance.
return (ImageInputStream) input;
- } else {
+ } else if (!wasProbingAbsentFile()) {
/*
* We do not invoke `ImageIO.createImageInputStream(Object)`
because we do not know
* how the stream will use the `storage` object. It may read in
advance some bytes,
@@ -1224,8 +1292,8 @@ public class StorageConnector implements Serializable {
* creating image input/output streams is left to caller's
responsibility.
*/
addView(ImageInputStream.class, null);
// Remember that there is no view.
- return null;
}
+ return null;
}
/**
@@ -1235,6 +1303,8 @@ public class StorageConnector implements Serializable {
* <p>This method is one of the {@link #OPENERS} methods and should be
invoked at most once per
* {@code StorageConnector} instance.</p>
*
+ * @return input stream, or {@code null} if none or if {@linkplain
#probing} result has been determined offline.
+ *
* @see #createOutputStream()
*/
private InputStream createInputStream() throws IOException,
DataStoreException {
@@ -1252,10 +1322,10 @@ public class StorageConnector implements Serializable {
final InputStream in = new InputStreamAdapter((ImageInputStream)
input);
addView(InputStream.class, in, source, (byte)
(getView(source).cascade & CASCADE_ON_RESET));
return in;
- } else {
+ } else if (!wasProbingAbsentFile()) {
addView(InputStream.class, null);
// Remember that there is no view.
- return null;
}
+ return null;
}
/**
@@ -1263,11 +1333,15 @@ public class StorageConnector implements Serializable {
*
* <p>This method is one of the {@link #OPENERS} methods and should be
invoked at most once per
* {@code StorageConnector} instance.</p>
+ *
+ * @return input characters, or {@code null} if none or if {@linkplain
#probing} result has been determined offline.
*/
private Reader createReader() throws IOException, DataStoreException {
final InputStream input = getStorageAs(InputStream.class);
if (input == null) {
- addView(Reader.class, null);
// Remember that there is no view.
+ if (!wasProbingAbsentFile()) {
+ addView(Reader.class, null);
// Remember that there is no view.
+ }
return null;
}
input.mark(READ_AHEAD_LIMIT);
@@ -1281,6 +1355,8 @@ public class StorageConnector implements Serializable {
*
* <p>This method is one of the {@link #OPENERS} methods and should be
invoked at most once per
* {@code StorageConnector} instance.</p>
+ *
+ * @return input, or {@code null} if none.
*/
private Connection createConnection() throws SQLException {
if (storage instanceof DataSource) {
@@ -1296,6 +1372,8 @@ public class StorageConnector implements Serializable {
*
* <p>This method is one of the {@link #OPENERS} methods and should be
invoked at most once per
* {@code StorageConnector} instance.</p>
+ *
+ * @return string representation of the storage path, or {@code null} if
none.
*/
private String createString() {
return IOUtilities.toString(storage);
@@ -1335,6 +1413,7 @@ public class StorageConnector implements Serializable {
* Creates a view for the storage as a {@link ChannelDataOutput} if
possible.
* This code is a partial copy of {@link #createDataInput()} adapted for
output.
*
+ * @return output channel, or {@code null} if none.
* @throws IOException if an error occurred while opening a channel for
the output.
*
* @see #createChannelDataInput(boolean)
@@ -1368,7 +1447,7 @@ public class StorageConnector implements Serializable {
* Following is an undocumented mechanism for allowing some Apache SIS
implementations of DataStore
* to re-open the same channel or output stream another time,
typically for re-writing the same data.
*/
- if (factory.canOpen()) {
+ if (factory.canReopen()) {
addView(ChannelFactory.class, factory);
}
return asDataOutput;
@@ -1378,6 +1457,7 @@ public class StorageConnector implements Serializable {
* Creates a view for the output as a {@link DataOutput} if possible.
* This code is a copy of {@link #createDataInput()} adapted for output.
*
+ * @return output stream, or {@code null} if none.
* @throws IOException if an error occurred while opening a stream for the
output.
*
* @see #createDataInput()
@@ -1427,6 +1507,8 @@ public class StorageConnector implements Serializable {
* or from {@link ImageOutputStream} otherwise.
* This code is a partial copy of {@link #createInputStream()} adapted for
output.
*
+ * @return output stream, or {@code null} if none.
+ *
* @see #createInputStream()
*/
private OutputStream createOutputStream() throws IOException,
DataStoreException {
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/StoreUtilities.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/StoreUtilities.java
index 11121d01ff..9e7947f4c9 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/StoreUtilities.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/StoreUtilities.java
@@ -51,6 +51,7 @@ import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.metadata.internal.Identifiers;
import org.apache.sis.system.Configuration;
import org.apache.sis.system.Modules;
+import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.resources.Errors;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -63,7 +64,7 @@ import org.opengis.feature.Feature;
* Some methods may also move in public API if we feel confident enough.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
* @since 1.0
*/
public final class StoreUtilities extends Static {
@@ -262,12 +263,7 @@ public final class StoreUtilities extends Static {
if (provider != null) {
StoreMetadata md = provider.getAnnotation(StoreMetadata.class);
if (md != null) {
- for (Capability c : md.capabilities()) {
- if (Capability.WRITE.equals(c)) {
- return Boolean.TRUE;
- }
- }
- return Boolean.FALSE;
+ return ArraysExt.contains(md.capabilities(), Capability.WRITE);
}
}
return null;
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/DataStoreFilter.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/DataStoreFilter.java
new file mode 100644
index 0000000000..c2d9929e89
--- /dev/null
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/DataStoreFilter.java
@@ -0,0 +1,83 @@
+/*
+ * 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.image;
+
+import java.util.function.Predicate;
+import javax.imageio.ImageIO;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Characters;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.storage.DataStoreProvider;
+import org.apache.sis.storage.base.StoreUtilities;
+
+
+/**
+ * A filter for data store providers with special handling for world files.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ *
+ * @see org.apache.sis.io.stream.InternalOptionKey#PREFERRED_PROVIDERS
+ *
+ * @since 1.4
+ */
+public final class DataStoreFilter implements Predicate<DataStoreProvider> {
+ /**
+ * Short name of the data store to search.
+ * May also be an Image I/O format name.
+ *
+ * @see DataStoreProvider#getShortName()
+ */
+ final String preferred;
+
+ /**
+ * Whether to search among writers instead of readers.
+ */
+ private final boolean writer;
+
+ /**
+ * Creates a new filter for the given data store name.
+ *
+ * @param preferred name of the data store to search, or Image I/O
format name.
+ * @param writer whether to search among writers intead of readers.
+ */
+ public DataStoreFilter(final String preferred, final boolean writer) {
+ this.preferred = preferred;
+ this.writer = writer;
+ }
+
+ /**
+ * Returns {@code true} if the specified store has the name that this
filter is looking for.
+ * Name comparison is case-insensitive and ignores characters that are not
part of Unicode
+ * identifier (e.g. white spaces).
+ *
+ * @param candidate the provider to test.
+ * @return whether the given provider has the desired name.
+ */
+ @Override
+ public boolean test(final DataStoreProvider candidate) {
+ final String formatName = StoreUtilities.getFormatName(candidate);
+ if (CharSequences.equalsFiltered(formatName, preferred,
Characters.Filter.UNICODE_IDENTIFIER, true)) {
+ return true;
+ }
+ if (WorldFileStoreProvider.NAME.equals(formatName)) {
+ String[] formats = writer ? ImageIO.getWriterFormatNames() :
ImageIO.getReaderFormatNames();
+ return ArraysExt.containsIgnoreCase(formats, preferred);
+ }
+ return false;
+ }
+}
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFilter.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFilter.java
index 9986729f95..9f04ada38c 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFilter.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFilter.java
@@ -48,7 +48,7 @@ import org.apache.sis.util.ArraysExt;
* This is used for providing utility methods about image formats.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
* @since 1.2
*/
enum FormatFilter {
@@ -135,7 +135,7 @@ enum FormatFilter {
final ImageReaderSpi findProvider(final String identifier, final
StorageConnector connector, final Set<ImageReaderSpi> done)
throws IOException, DataStoreException
{
- final Iterator<ImageReaderSpi> it =
FormatFilter.SUFFIX.getServiceProviders(ImageReaderSpi.class, identifier);
+ final Iterator<ImageReaderSpi> it =
getServiceProviders(ImageReaderSpi.class, identifier);
while (it.hasNext()) {
final ImageReaderSpi provider = it.next();
if (done.add(provider)) {
@@ -156,6 +156,16 @@ enum FormatFilter {
return provider;
}
break; // Skip other input types, try the
next provider.
+ } else if (connector.wasProbingAbsentFile()) {
+ /*
+ * This method is invoked for probing a file
content (not for opening the file),
+ * the file does not exist, but a `CREATE` or
`CREATE_NEW` option has been provided.
+ * Accept this provider if it as a writer
counterpart.
+ */
+ if (provider.getImageWriterSpiNames() != null) {
+ return provider;
+ }
+ break;
}
}
}
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFinder.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFinder.java
index 22545e154f..5d40d6c862 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFinder.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFinder.java
@@ -32,8 +32,10 @@ import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.FileImageOutputStream;
+import java.awt.image.RenderedImage;
import org.apache.sis.storage.StorageConnector;
import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.io.stream.InternalOptionKey;
import org.apache.sis.io.stream.IOUtilities;
import org.apache.sis.util.Workaround;
@@ -44,7 +46,7 @@ import org.apache.sis.util.Workaround;
* It also helps to choose which {@link WorldFileStore} subclass to
instantiate.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
* @since 1.2
*/
final class FormatFinder implements AutoCloseable {
@@ -112,6 +114,11 @@ final class FormatFinder implements AutoCloseable {
*/
final String suffix;
+ /**
+ * Name of the preferred format, or {@code null} if none.
+ */
+ private final String preferredFormat;
+
/**
* Creates a new format finder.
*
@@ -135,6 +142,8 @@ final class FormatFinder implements AutoCloseable {
}
this.storage = storage;
this.suffix = IOUtilities.extension(storage);
+ final var filter =
connector.getOption(InternalOptionKey.PREFERRED_PROVIDERS);
+ preferredFormat = filter instanceof DataStoreFilter ?
((DataStoreFilter) filter).preferred : null;
/*
* Detect if the image can be opened in read/write mode.
* If not, it will be opened in read-only mode.
@@ -157,8 +166,8 @@ final class FormatFinder implements AutoCloseable {
return;
}
}
- openAsWriter = false;
- fileIsEmpty = false;
+ openAsWriter = IOUtilities.isWriteOnly(storage);
+ fileIsEmpty = openAsWriter;
}
}
@@ -169,7 +178,7 @@ final class FormatFinder implements AutoCloseable {
*/
final String[] getFormatName() throws DataStoreException, IOException {
if (openAsWriter) {
- final ImageWriter writer = getOrCreateWriter();
+ final ImageWriter writer = getOrCreateWriter(null);
if (writer != null) {
final ImageWriterSpi spi = writer.getOriginatingProvider();
if (spi != null) {
@@ -199,7 +208,10 @@ final class FormatFinder implements AutoCloseable {
if (!readerLookupDone) {
readerLookupDone = true;
final Map<ImageReaderSpi,Boolean> deferred = new LinkedHashMap<>();
- if (suffix != null) {
+ if (preferredFormat != null) {
+ reader = FormatFilter.NAME.createReader(preferredFormat, this,
deferred);
+ }
+ if (reader == null && suffix != null) {
reader = FormatFilter.SUFFIX.createReader(suffix, this,
deferred);
}
if (reader == null) {
@@ -243,17 +255,21 @@ final class FormatFinder implements AutoCloseable {
/**
* Returns the user-specified writer or searches for a writer for the file
suffix.
*
+ * @param image the image to write, or {@code null} if unknown.
* @return the writer, or {@code null} if none could be found.
*/
- final ImageWriter getOrCreateWriter() throws DataStoreException,
IOException {
+ final ImageWriter getOrCreateWriter(final RenderedImage image) throws
DataStoreException, IOException {
if (!writerLookupDone) {
writerLookupDone = true;
final Map<ImageWriterSpi,Boolean> deferred = new LinkedHashMap<>();
- if (suffix != null) {
- writer = FormatFilter.SUFFIX.createWriter(suffix, this, null,
deferred);
+ if (preferredFormat != null) {
+ writer = FormatFilter.NAME.createWriter(preferredFormat, this,
image, deferred);
+ }
+ if (writer == null && suffix != null) {
+ writer = FormatFilter.SUFFIX.createWriter(suffix, this, image,
deferred);
}
if (writer == null) {
- writer = FormatFilter.SUFFIX.createWriter(null, this, null,
deferred);
+ writer = FormatFilter.SUFFIX.createWriter(null, this, image,
deferred);
if (writer == null) {
ImageOutputStream stream = null;
for (final Map.Entry<ImageWriterSpi,Boolean> entry :
deferred.entrySet()) {
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStoreProvider.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStoreProvider.java
index 48bfbc9cf9..09b768a594 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStoreProvider.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStoreProvider.java
@@ -159,7 +159,9 @@ public final class WorldFileStoreProvider extends
PRJDataStore.Provider {
try {
provider = FormatFilter.SUFFIX.findProvider(suffix, connector,
deferred);
if (provider == null) {
- provider = FormatFilter.SUFFIX.findProvider(null, connector,
deferred);
+ if (suffix != null) {
+ provider = FormatFilter.SUFFIX.findProvider(null,
connector, deferred);
+ }
if (provider == null) {
return ProbeResult.UNSUPPORTED_STORAGE;
}
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableSingleImageStore.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableSingleImageStore.java
index 3b8acd3db5..b6d0d146c2 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableSingleImageStore.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableSingleImageStore.java
@@ -160,8 +160,8 @@ final class WritableSingleImageStore extends WritableStore
implements WritableGr
}
/**
- * Writes a new coverage in the data store for this resource. If a
coverage already exists for this resource,
- * then it will be overwritten only if the {@code TRUNCATE} or {@code
UPDATE} option is specified.
+ * Writes a new coverage in the data store containing this resource. If a
coverage already exists for this
+ * resource, then it will be overwritten only if the {@code TRUNCATE} or
{@code UPDATE} option is specified.
*
* @param coverage new data to write in the data store for this resource.
* @param options configuration of the write operation.
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableStore.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableStore.java
index 32cf6fc3f5..6228a5862c 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableStore.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableStore.java
@@ -113,7 +113,7 @@ class WritableStore extends WorldFileStore {
if (getCurrentReader() != null) {
numImages = -1;
} else {
- writer = format.getOrCreateWriter();
+ writer = format.getOrCreateWriter(null);
if (writer == null) {
throw new UnsupportedStorageException(super.getLocale(),
WorldFileStoreProvider.NAME,
format.storage,
format.connector.getOption(OptionKey.OPEN_OPTIONS));
@@ -287,6 +287,8 @@ writeCoeffs: for (int i=0;; i++) {
* @param resource the resource to copy in this {@code Aggregate}.
* @return the effectively added resource.
* @throws DataStoreException if the given resource cannot be stored in
this {@code Aggregate}.
+ *
+ * @see WritableAggregate#add(Resource)
*/
public synchronized Resource add(final Resource resource) throws
DataStoreException {
Exception cause = null;
@@ -325,6 +327,8 @@ writeCoeffs: for (int i=0;; i++) {
*
* @param resource child resource to remove from this {@code Aggregate}.
* @throws DataStoreException if the given resource could not be removed.
+ *
+ * @see WritableAggregate#remove(Resource)
*/
@Override
public synchronized void remove(final Resource resource) throws
DataStoreException {
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/io/FileAccessView.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/io/FileAccessView.java
index 431ff7718b..cbeddbd0f1 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/io/FileAccessView.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/io/FileAccessView.java
@@ -111,8 +111,16 @@ public final class FileAccessView extends Widget
implements UnaryOperator<Channe
* Returns {@code true} if this factory is capable to create
another readable byte channel.
*/
@Override
- public boolean canOpen() {
- return factory.canOpen();
+ public boolean canReopen() {
+ return factory.canReopen();
+ }
+
+ /**
+ * Returns {@code true} if opening the channel will create a new,
initially empty, file.
+ */
+ @Override
+ public boolean isCreateNew() {
+ return factory.isCreateNew();
}
/**