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();
             }
 
             /**

Reply via email to