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 <martin.desruisse...@geomatys.com> 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(); } /**