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 66809d27ae Handle empty files as non-existent files when opening a 
`DataStore` in write mode. This is needed because when requesting a temporary 
file, an empty file is created.
66809d27ae is described below

commit 66809d27ae2cb743c3a5fc3fb872fc162649ae4b
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Thu Dec 5 23:38:27 2024 +0100

    Handle empty files as non-existent files when opening a `DataStore` in 
write mode.
    This is needed because when requesting a temporary file, an empty file is 
created.
---
 .../org/apache/sis/io/stream/ChannelFactory.java   | 46 ++++++++++++++++---
 .../main/org/apache/sis/io/stream/IOUtilities.java | 16 +++++++
 .../org/apache/sis/storage/ProbeProviderPair.java  |  6 +--
 .../main/org/apache/sis/storage/ProbeResult.java   | 10 ++++-
 .../org/apache/sis/storage/StorageConnector.java   | 15 ++++---
 .../sis/storage/base/URIDataStoreProvider.java     |  8 ++--
 .../main/org/apache/sis/pending/jdk/JDK20.java     | 51 ++++++++++++++++++++++
 .../apache/sis/gui/internal/io/FileAccessView.java |  2 +-
 8 files changed, 135 insertions(+), 19 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 c24fe66a5c..3f9854f97c 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
@@ -43,6 +43,7 @@ import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.WritableByteChannel;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.system.Modules;
@@ -241,7 +242,7 @@ public abstract class ChannelFactory {
                      * way less surprising for the user (closer to the object 
he has specified).
                      */
                     if (file.isFile()) {
-                        return new Fallback(file, e);
+                        return new Fallback(file, options, e);
                     }
                 }
             }
@@ -287,9 +288,13 @@ 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);
+                    @Override public boolean isCreateNew() throws IOException {
+                        if (optionSet.contains(StandardOpenOption.CREATE_NEW)) 
{
+                            return true;
+                        }
+                        if (optionSet.contains(StandardOpenOption.CREATE)) {
+                            return IOUtilities.isAbsentOrEmpty(path);
+                        }
                         return false;
                     }
                 };
@@ -327,8 +332,9 @@ public abstract class ChannelFactory {
      * this factory has been created with an option that allows file creation.
      *
      * @return whether opening a channel will create a new file.
+     * @throws if an error occurred while checking file existence of 
attributes.
      */
-    public boolean isCreateNew() {
+    public boolean isCreateNew() throws IOException {
         return false;
     }
 
@@ -532,6 +538,11 @@ public abstract class ChannelFactory {
          */
         private final File file;
 
+        /**
+         * The {@code READ}, {@code CREATE} or {@code CREATE_NEW} option.
+         */
+        private final StandardOpenOption option;
+
         /**
          * The reason why we are using this fallback instead of a {@link Path}.
          * Will be reported at most once, then set to {@code null}.
@@ -541,10 +552,19 @@ public abstract class ChannelFactory {
         /**
          * Creates a new fallback to use if the given file cannot be converted 
to a {@link Path}.
          */
-        Fallback(final File file, final InvalidPathException cause) {
+        Fallback(final File file, final OpenOption[] options, final 
InvalidPathException cause) {
             super(true);
             this.file  = file;
             this.cause = cause;
+            @SuppressWarnings("LocalVariableHidesMemberVariable")
+            StandardOpenOption option = StandardOpenOption.CREATE_NEW;
+            if (!ArraysExt.contains(options, option)) {
+                option = StandardOpenOption.CREATE;
+                if (!ArraysExt.contains(options, option)) {
+                    option = StandardOpenOption.READ;   // Could actually be 
WRITE, but we don't need to distinguish.
+                }
+            }
+            this.option = option;
         }
 
         /**
@@ -638,6 +658,20 @@ public abstract class ChannelFactory {
         public WritableByteChannel writable(String filename, StoreListeners 
listeners) throws IOException {
             return outputStream(filename, listeners).getChannel();
         }
+
+        /**
+         * Returns {@code true} if opening the channel will create a new, 
initially empty, file.
+         */
+        @Override
+        public boolean isCreateNew() throws IOException {
+            switch (option) {
+                default: return false;
+                case CREATE_NEW: return true;
+                case CREATE: {
+                    return !file.exists() || (file.isFile() && file.length() 
== 0);
+                }
+            }
+        }
     }
 
     /**
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 3dd75cd7ed..02a71bcfe1 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
@@ -41,10 +41,12 @@ import java.nio.file.Path;
 import java.nio.file.OpenOption;
 import java.nio.file.StandardOpenOption;
 import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.charset.StandardCharsets;
 import javax.imageio.stream.ImageInputStream;
 import javax.xml.stream.Location;
 import javax.xml.stream.XMLStreamReader;
+import org.apache.sis.pending.jdk.JDK20;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.Static;
 import org.apache.sis.util.resources.Errors;
@@ -558,6 +560,20 @@ check:  if (stream instanceof ChannelData) {
         return false;
     }
 
+    /**
+     * Returns {@code true} if the file at the specified path is absent or an 
empty file.
+     * If the file exists but is not a regular file, then this method returns 
{@code false}.
+     *
+     * @param  path  the path to test.
+     * @return whether the file is absent or empty.
+     * @throws IOException if an error occurred while fetching the attributes.
+     */
+    public static boolean isAbsentOrEmpty(final Path path) throws IOException {
+        final BasicFileAttributes attributes = JDK20.readAttributesIfExists(
+                path.getFileSystem().provider(), path, 
BasicFileAttributes.class);
+        return (attributes == null) || (attributes.isRegularFile() && 
attributes.size() == 0);
+    }
+
     /**
      * Returns {@code true} if the given object is an output stream with no 
read capability.
      *
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 acc9f44941..129854dd85 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
@@ -54,14 +54,14 @@ final class ProbeProviderPair {
 
     /**
      * Sets the {@linkplain #probe} result for a file that does not exist yet.
-     * The result will be {@link ProbeResult#SUPPORTED} or {@code 
UNSUPPORTED_STORAGE},
+     * The result will be {@link ProbeResult#CREATE_NEW} 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.
+     * {@code CREATE_NEW} 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()
@@ -69,7 +69,7 @@ final class ProbeProviderPair {
     final void setProbingAbsentFile() {
         final StoreMetadata md = 
provider.getClass().getAnnotation(StoreMetadata.class);
         if (md == null || ArraysExt.contains(md.capabilities(), 
Capability.CREATE)) {
-            probe = ProbeResult.SUPPORTED;
+            probe = ProbeResult.CREATE_NEW;
         } 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 df18b15c6b..9151e91059 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.privy.Strings;
  * In such cases, SIS will revisit those providers only if no better suited 
provider is found.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see DataStoreProvider#probeContent(StorageConnector)
  *
@@ -57,6 +57,14 @@ public class ProbeResult implements Serializable {
      */
     private static final long serialVersionUID = -4977853847503500550L;
 
+    /**
+     * The {@code DataStoreProvider} will create a new file.
+     * The file does not exist yet or is empty.
+     *
+     * @since 1.5
+     */
+    public static final ProbeResult CREATE_NEW = new Constant(true, 
"CREATE_NEW");
+
     /**
      * The {@code DataStoreProvider} recognizes the given storage, but has no 
additional information.
      * The {@link #isSupported()} method returns {@code true}, but the 
{@linkplain #getMimeType() MIME type}
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 1b64ee68cf..b851d8b821 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
@@ -1038,14 +1038,19 @@ public class StorageConnector implements Serializable {
      * 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
+     *   <li>The {@linkplain #getOption(OptionKey) options} 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>
+     *   <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>One of the following conditions is true:
+     *     <ul>
+     *       <li>The input stream is empty.</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>
+     *     </ul>
+     *   </li>
      * </ul>
      *
      * If all above conditions are true, then {@link #getStorageAs(Class)} 
returns {@code null} instead of creating
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
index 911ab593fc..565670359c 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
@@ -19,11 +19,11 @@ package org.apache.sis.storage.base;
 import java.util.Optional;
 import java.io.DataInput;
 import java.io.DataOutput;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
 import java.nio.file.Path;
-import java.nio.file.Files;
 import java.nio.file.OpenOption;
 import java.nio.file.StandardOpenOption;
 import java.nio.charset.Charset;
@@ -221,9 +221,11 @@ public abstract class URIDataStoreProvider extends 
DataStoreProvider {
             if (ArraysExt.contains(options, StandardOpenOption.CREATE_NEW)) {
                 return IOUtilities.isKindOfPath(storage);
             }
-            if (ArraysExt.contains(options, StandardOpenOption.CREATE)) {
+            if (ArraysExt.contains(options, StandardOpenOption.CREATE)) try {
                 final Path path = connector.getStorageAs(Path.class);
-                return (path != null) && Files.notExists(path);
+                return (path != null) && IOUtilities.isAbsentOrEmpty(path);
+            } catch (IOException e) {
+                throw new DataStoreException(e);
             }
         }
         return false;
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK20.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK20.java
new file mode 100644
index 0000000000..2142cc573a
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK20.java
@@ -0,0 +1,51 @@
+/*
+ * 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.pending.jdk;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.spi.FileSystemProvider;
+import java.nio.file.attribute.BasicFileAttributes;
+
+
+/**
+ * Place holder for some functionalities defined in a JDK more recent than 
Java 11.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class JDK20 {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private JDK20() {
+    }
+
+    /**
+     * Reads a file's attributes as a bulk operation if it exists.
+     */
+    public static <A extends BasicFileAttributes> A 
readAttributesIfExists(FileSystemProvider provider,
+            Path path, Class<A> type, LinkOption... options) throws IOException
+    {
+        try {
+            return provider.readAttributes(path, type, options);
+        } catch (NoSuchFileException ignore) {
+            return null;
+        }
+    }
+}
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 6e105a1105..f1a9571a8a 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
@@ -118,7 +118,7 @@ public final class FileAccessView extends Widget implements 
UnaryOperator<Channe
              * Returns {@code true} if opening the channel will create a new, 
initially empty, file.
              */
             @Override
-            public boolean isCreateNew() {
+            public boolean isCreateNew() throws IOException {
                 return factory.isCreateNew();
             }
 

Reply via email to