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