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
commit bad81f42f15b104bf2c9650601a5d29e18e49ae2 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Sep 20 17:44:22 2024 +0200 Replace `ResourceOnFileSystem` internal interface by a public `Resource.getFileSet()` method. Provides default methods for copying and deleting the resource, with overrides in GDALStore. --- .../org/apache/sis/storage/geotiff/DataCube.java | 9 +- .../sis/storage/geotiff/MultiResolutionImage.java | 11 +- .../sis/storage/netcdf/base/RasterResource.java | 11 +- .../main/org/apache/sis/storage/Resource.java | 206 ++++++++++++++++++++- .../org/apache/sis/storage/base/PRJDataStore.java | 35 ++-- .../sis/storage/base/ResourceOnFileSystem.java | 71 ------- .../org/apache/sis/storage/base/URIDataStore.java | 37 ++-- .../sis/storage/base/URIDataStoreProvider.java | 9 +- .../org/apache/sis/storage/esri/RasterStore.java | 7 +- .../apache/sis/storage/esri/RawRasterStore.java | 5 +- .../main/org/apache/sis/storage/folder/Store.java | 6 +- .../apache/sis/storage/folder/WritableStore.java | 39 ++-- .../apache/sis/storage/image/WorldFileStore.java | 5 +- .../sis/storage/esri/AsciiGridStoreTest.java | 59 +++++- .../main/org/apache/sis/storage/gdal/Driver.java | 99 ++++++++++ .../main/org/apache/sis/storage/gdal/GDAL.java | 23 +++ .../org/apache/sis/storage/gdal/GDALStore.java | 36 ++-- .../main/org/apache/sis/storage/gdal/Opener.java | 9 +- .../org/apache/sis/storage/gdal/GDALStoreTest.java | 59 +++++- .../apache/sis/storage/geopackage/GpkgStore.java | 26 +-- .../apache/sis/storage/gsf/GSFRecordReader.java | 2 +- .../main/org/apache/sis/storage/gsf/GSFStore.java | 9 +- .../sis/storage/shapefile/ShapefileStore.java | 15 +- .../sis/storage/shapefile/ShapefileStoreTest.java | 31 ++-- .../org/apache/sis/gui/dataset/PathAction.java | 27 +-- 25 files changed, 605 insertions(+), 241 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java index a0f32761ab..e6f123f9f7 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java @@ -34,7 +34,6 @@ import org.apache.sis.storage.geotiff.base.Resources; import org.apache.sis.storage.geotiff.base.Predictor; import org.apache.sis.storage.geotiff.base.Compression; import org.apache.sis.storage.base.TiledGridResource; -import org.apache.sis.storage.base.ResourceOnFileSystem; import org.apache.sis.storage.base.StoreResource; import org.apache.sis.math.Vector; @@ -50,7 +49,7 @@ import org.apache.sis.math.Vector; * * @author Martin Desruisseaux (Geomatys) */ -abstract class DataCube extends TiledGridResource implements ResourceOnFileSystem, StoreResource { +abstract class DataCube extends TiledGridResource implements StoreResource { /** * The GeoTIFF reader which contain this {@code DataCube}. * Used for fetching information like the input channel and where to report warnings. @@ -111,12 +110,12 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste public abstract Optional<GenericName> getIdentifier(); /** - * Gets the paths to files used by this resource, or an empty array if unknown. + * Gets the paths to files used by this resource, or an empty value if unknown. */ @Override - public final Path[] getComponentFiles() { + public final Optional<FileSet> getFileSet() { final Path location = reader.store.path; - return (location != null) ? new Path[] {location} : new Path[0]; + return (location != null) ? Optional.of(new FileSet(location)) : Optional.empty(); } /** diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java index 8e928eb840..7188a27f74 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java @@ -18,8 +18,8 @@ package org.apache.sis.storage.geotiff; import java.util.List; import java.util.Arrays; +import java.util.Optional; import java.io.IOException; -import java.nio.file.Path; import org.opengis.util.NameSpace; import org.opengis.util.FactoryException; import org.opengis.geometry.DirectPosition; @@ -37,7 +37,6 @@ import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.DataStoreReferencingException; import org.apache.sis.storage.base.StoreResource; import org.apache.sis.storage.base.GridResourceWrapper; -import org.apache.sis.storage.base.ResourceOnFileSystem; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.privy.DirectPositionView; import org.apache.sis.referencing.operation.matrix.MatrixSIS; @@ -49,7 +48,7 @@ import org.apache.sis.referencing.operation.matrix.MatrixSIS; * * @author Martin Desruisseaux (Geomatys) */ -final class MultiResolutionImage extends GridResourceWrapper implements ResourceOnFileSystem, StoreResource { +final class MultiResolutionImage extends GridResourceWrapper implements StoreResource { /** * Name of the image at finest resolution. * This is used as the namespace for overviews. @@ -95,11 +94,11 @@ final class MultiResolutionImage extends GridResourceWrapper implements Resource } /** - * Gets the paths to files used by this resource, or an empty array if unknown. + * Gets the paths to files used by this resource, or an empty value if unknown. */ @Override - public Path[] getComponentFiles() { - return levels[0].getComponentFiles(); + public final Optional<FileSet> getFileSet() { + return levels[0].getFileSet(); } /** diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java index ddfb51efb7..af58a891be 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java @@ -35,7 +35,6 @@ import org.apache.sis.storage.DataStore; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.storage.Resource; -import org.apache.sis.storage.base.ResourceOnFileSystem; import org.apache.sis.storage.base.MetadataBuilder; import org.apache.sis.storage.base.StoreResource; import org.apache.sis.util.Numbers; @@ -68,7 +67,7 @@ import org.apache.sis.storage.netcdf.internal.Resources; * @author Johann Sorel (Geomatys) * @author Alexis Manin (Geomatys) */ -public final class RasterResource extends AbstractGridCoverageResource implements StoreResource, ResourceOnFileSystem { +public final class RasterResource extends AbstractGridCoverageResource implements StoreResource { /** * Words used in standard (preferred) or long (if no standard) variable names which suggest * that the variable is a component of a vector. Those words are used in heuristic rules @@ -159,7 +158,7 @@ public final class RasterResource extends AbstractGridCoverageResource implement /** * Path to the netCDF file for information purpose, or {@code null} if unknown. * - * @see #getComponentFiles() + * @see #getFileSet() */ private final Path location; @@ -739,11 +738,11 @@ public final class RasterResource extends AbstractGridCoverageResource implement } /** - * Gets the paths to files used by this resource, or an empty array if unknown. + * Gets the paths to files used by this resource, or an empty value if unknown. */ @Override - public Path[] getComponentFiles() { - return (location != null) ? new Path[] {location} : new Path[0]; + public final Optional<FileSet> getFileSet() { + return (location != null) ? Optional.of(new FileSet(location)) : Optional.empty(); } /** diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java index ab15572895..61022718dc 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java @@ -16,7 +16,16 @@ */ package org.apache.sis.storage; +import java.util.List; +import java.util.Set; +import java.util.HashSet; import java.util.Optional; +import java.util.Collection; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Files; +import java.nio.file.CopyOption; +import java.nio.file.FileAlreadyExistsException; import org.opengis.util.GenericName; import org.opengis.metadata.Metadata; import org.apache.sis.storage.event.StoreEvent; @@ -44,7 +53,8 @@ import org.apache.sis.storage.event.StoreListener; * The sub-types performing the actual data extraction – for example {@link FeatureSet} – are specific to Apache SIS. * * @author Johann Sorel (Geomatys) - * @version 1.0 + * @author Martin Desruisseaux (Geomatys) + * @version 1.5 * * @see Aggregate#components() * @@ -138,6 +148,200 @@ public interface Resource { */ Metadata getMetadata() throws DataStoreException; + /** + * Gets the paths to the files potentially used by this resource. + * They are the files that would need to be linked, copied, moved or deleted if this resource was + * to be transferred efficiently to another location or file system (e.g. a <abbr>ZIP</abbr> file). + * Such set of files usually exists for {@link DataStore} instances, but not for their + * {@linkplain DataStore#findResource(String) components} because it is often impractical + * to copy a single resource component without copying the full data set that contains it. + * + * <p>There are some exceptions to above scenario. A {@link DataStore} may have no declared set of files + * if the store is generated by an in-memory calculation, or if the store uses a connection to a database. + * Conversely, a component (child resource) may have a set of files if the parent {@code DataStore} stores + * each component separately.</p> + * + * @return files used by this resource. + * @throws DataStoreException if an error occurred while preparing the set of files. + * + * @since 1.5 + */ + default Optional<FileSet> getFileSet() throws DataStoreException { + return Optional.empty(); + } + + /** + * Paths to the files potentially used by the enclosing resource. + * They are typically the files given to the {@link DataStore} constructor, + * sometime with auxiliary files (metadata, index, <i>etc.</i>). + * {@code FileSet} allows efficient copies of data set from one location to another. + * + * <p>Note that some {@code FileSet} items of a given resource may be shared by the {@code FileSet} + * of another resource. Therefore, there is no guarantee that modifying or deleting a file will not + * impact other resources.</p> + * + * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) + * @version 1.5 + * @since 1.5 + */ + public static class FileSet { + /** + * The files to be returned by {@link #getPaths()}. + */ + private final Collection<Path> paths; + + /** + * Creates a new instance with the given path. + * This is a convenience constructor for the common case where the data store uses exactly one file. + * + * @param paths the single file to be returned by {@link #getPaths()}. + */ + public FileSet(final Path path) { + this.paths = List.of(path); + } + + /** + * Creates a new instance with the given paths. + * + * @param paths the files to be returned by {@link #getPaths()}. + */ + public FileSet(final Path[] paths) { + this.paths = List.of(paths); + } + + /** + * Creates a new instance with the given paths. + * The content of the given collection is copied. + * + * @param paths the files to be returned by {@link #getPaths()}. + */ + public FileSet(final Collection<Path> paths) { + this.paths = List.copyOf(paths); + } + + /** + * Returns the paths to the files potentially used by the enclosing resource. + * The first file in the returned collection should be the main file given to the {@link DataStore} + * constructor or opened by other kinds of resource. All other files, if any, may be auxiliary files + * such as metadata or index. The files are not necessarily in the same directory, + * and the same files may be used by more than one resource. + * + * <p>The returned paths should be regular files instead of directories, unless the enclosing + * resource is designed for using the content of the whole directory. The collection should not + * contain non-existent files.</p> + * + * @return paths to the files potentially used by the enclosing resource. + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + public Collection<Path> getPaths() { + return paths; + } + + /** + * Returns the directory which contains all files, or {@code null} if none. + * This is either the directory of the main file, or a parent directory if + * an auxiliary file is found there. The latter case happens when the same + * file (e.g. a global {@code metadata.xml}) is shared by many resources. + */ + private Path getBaseDirectory() { + Path directory = null; + for (Path path : getPaths()) { + path = path.getParent(); + if (directory == null || directory.startsWith(path)) { + directory = path; + } + } + return directory; + } + + /** + * Copies all resource files to the given directory. The copied files are usually the files returned + * by {@link #getPaths()}, but details may very depending on the {@link DataStore} implementation. + * It is recommended to invoke this method only when the {@link DataStore} is closed. + * + * <p><b>Limitations:</b></p> + * <ul> + * <li>This method does not overwrite existing files or sub-directories. + * If a destination already exists, an exception should be thrown.</li> + * <li>This copy operation is not atomic. If an exception is thrown, + * some files may be only partially copied or not created at all.</li> + * </ul> + * + * This method can be used for exporting the resource to a <abbr>ZIP</abbr> file if the given + * destination directory is associated to the file system of the {@code jdk.zipfs} module. + * + * <h4>Default implementation</h4> + * The default implementation performs a {@linkplain Files#copy(Path, Path, CopyOption...) copy operation} + * for each item returned by {@link #getPath()}. If those source files are in different directories, + * the default implementation reproduces the directory structure which is below the common parent of + * all source files. + * + * @param destDir the directory where to copy the resource files. + * @return the copied main file. May be in a sub-directory of {@code destDir}. + * @throws FileAlreadyExistsException if a destination file or directory already exists. + * @throws IOException if another error occurred while copying the files. + * + * @see Files#copy(Path, Path, CopyOption...) + */ + public Path copy(final Path destDir) throws IOException { + final var subdirs = new HashSet<Path>(); + final Path base = getBaseDirectory(); + Path main = null; + for (Path source : getPaths()) { + Path target = source; + if (base != null) { + target = base.relativize(target); + mkdirs(destDir, target, subdirs); + } + target = Files.copy(source, destDir.resolve(target)); + if (main == null) main = target; + } + return main; + } + + /** + * Creates the parent directories if they are needed. + * We do not use {@code Files.mkdirs(…)} because we want this method to fail if a directory exists. + */ + private static void mkdirs(final Path destDir, Path file, final Set<Path> done) throws IOException { + file = file.getParent(); + if (file != null && done.add(file)) { + mkdirs(destDir, file, done); // Create parents first. + Files.createDirectory(destDir.resolve(file)); + } + } + + /** + * Deletes the files used by the enclosing resource. + * The {@link DataStore} that contains the resource should be closed before to invoke this method. + * This is not an atomic operation. If an exception is thrown, some files may still remain. + * + * <h4>Default implementation</h4> + * The default implementation {@linkplain Files#delete(Path) deletes} the files returned by + * {@link #getPaths()} that are in the same directory or a sub-directory of the main file. + * Files in parent directory are not deleted, because they are often files shared by many + * resources (e.g. a global {@code metadata.xml} file). + * + * @throws IOException if another error occurred while deleting a file. + * + * @see Files#delete(Path) + */ + public void delete() throws IOException { + Path base = null; + boolean first = true; + for (Path path : getPaths()) { + if (first) { + first = false; + base = path.getParent(); // May still null. + } else if (base != null && !path.startsWith(base)) { + continue; + } + Files.delete(path); + } + } + } + /** * Registers a listener to notify when the specified kind of event occurs in this resource or in children. * The resource will call the {@link StoreListener#eventOccured(StoreEvent)} method when new events matching diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java index 99e5a9864f..dcecea92b8 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java @@ -25,9 +25,9 @@ import java.nio.file.Path; import java.nio.file.NoSuchFileException; import java.text.ParseException; import java.text.ParsePosition; -import java.util.Arrays; import java.util.Objects; import java.util.Optional; +import java.util.ArrayList; import jakarta.xml.bind.JAXBException; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterDescriptorGroup; @@ -44,7 +44,6 @@ import org.apache.sis.storage.wkt.StoreFormat; import org.apache.sis.io.wkt.Convention; import org.apache.sis.parameter.ParameterBuilder; import org.apache.sis.parameter.Parameters; -import org.apache.sis.util.ArraysExt; import org.apache.sis.util.Classes; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.xml.privy.ExceptionSimplifier; @@ -67,7 +66,7 @@ public abstract class PRJDataStore extends URIDataStore { /** * The filename extension of {@code "*.prj"} files. * - * @see #getComponentFiles() + * @see #getFileSet() */ protected static final String PRJ = "prj"; @@ -218,46 +217,42 @@ public abstract class PRJDataStore extends URIDataStore { * The default implementation does the same computation as the super-class, then adds the sibling * file with {@code ".prj"} extension if it exists. * - * @return the main file and auxiliary files as paths, or an empty array if unknown. + * @return the main file and auxiliary files as paths, or an empty value if unknown. * @throws DataStoreException if the URI cannot be converted to a {@link Path}. */ @Override - public Path[] getComponentFiles() throws DataStoreException { + public Optional<FileSet> getFileSet() throws DataStoreException { return listComponentFiles(PRJ); } /** * Returns the {@linkplain #location} as a {@code Path} component together with auxiliary files. - * This method computes the path to the main file as {@link URIDataStore#getComponentFiles()}, - * then add the sibling files with all extensions specified in the {@code auxiliaries} argument. + * This method computes the path to the main file as {@link URIDataStore#getFileSet()}, + * then adds the sibling files with all extensions specified in the {@code auxiliaries} argument. * Each auxiliary file is tested for existence. Paths that are not regular files are omitted. - * This is a helper method for {@link #getComponentFiles()} implementation. + * This is a helper method for {@link #getFileSet()} implementations. * * @param auxiliaries filename extension (without leading dot) of all auxiliary files. * Null elements are silently ignored. * @return the URI as a path, followed by all auxiliary files that exist. * @throws DataStoreException if the URI cannot be converted to a {@link Path}. */ - protected final Path[] listComponentFiles(final String... auxiliaries) throws DataStoreException { - Path[] paths = super.getComponentFiles(); - int count = paths.length; - if (count != 0) { - final Path path = paths[0]; + protected final Optional<FileSet> listComponentFiles(final String... auxiliaries) throws DataStoreException { + return super.getFileSet().map((fileset) -> { + final var paths = new ArrayList<Path>(fileset.getPaths()); + final Path path = paths.get(0); // This list is ever empty. final String base = getBaseFilename(path); + boolean modified = false; for (final String extension : auxiliaries) { if (extension != null) { final Path p = path.resolveSibling(base.concat(extension)); if (Files.isRegularFile(p)) { - if (count >= paths.length) { - paths = Arrays.copyOf(paths, count + auxiliaries.length); - } - paths[count++] = p; + modified |= paths.add(p); } } } - paths = ArraysExt.resize(paths, count); - } - return paths; + return modified ? new FileSet(paths) : fileset; + }); } /** diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/ResourceOnFileSystem.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/ResourceOnFileSystem.java deleted file mode 100644 index f24c155bff..0000000000 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/ResourceOnFileSystem.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.base; - -import java.nio.file.Path; -import org.apache.sis.storage.DataStoreException; -import org.apache.sis.storage.Resource; - - -/** - * A resource which is loaded from one or many files on an arbitrary file system. This interface - * allows a resource (typically a {@linkplain org.apache.sis.storage.DataStore data store}) to - * list the files that it uses. It may be used for copying or deleting resources if the caller - * is certain that those files are not in use. - * - * <h2>Alternatives</h2> - * <p>For copying data from one location to another, consider using - * {@link org.apache.sis.storage.WritableAggregate#add(Resource)} instead. - * The data store implementations may detect that some {@code add(…)} operations - * can be performed by verbatim copy of files.</p> - * - * <p>For deleting data, consider using - * {@link org.apache.sis.storage.WritableAggregate#remove(Resource)} instead.</p> - * - * @author Johann Sorel (Geomatys) - */ -public interface ResourceOnFileSystem extends Resource { - /** - * Gets the paths to files potentially used by this resource. - * This is typically the files opened by a {@linkplain org.apache.sis.storage.DataStore data store}. - * There is no guarantee that all files are in the same directory or that each file is used exclusively - * by this data source (e.g. no guarantee that modifying or deleting a file will not impact other resources). - * - * <p>This method should return paths to files only. - * It should not return paths to directories. - * The caller should verify that all paths are regular files; - * non-existent paths should be omitted.</p> - * - * <h4>Example</h4> - * A resources created for a GRIB file may use the following component files: - * <ul> - * <li>The main GRIB file.</li> - * <li>If managed by the UCAR library, two auxiliary files next to the main GRIB file: - * the index file ({@code ".gbx9"}) and the collection file ({@code ".ncx"}). - * Those two files are owned exclusively by the resource.</li> - * <li>Eventually a GRIB table file. This table may be located in a path unrelated to - * to the path of the main file and may be shared by many resources.</li> - * </ul> - * - * @return files used by this resource. Should never be {@code null}. - * @throws DataStoreException if an error on the file system prevent the creation of the list. - * - * @todo Should be renamed to something else, because current name creates a confusion with - * {@link org.apache.sis.storage.Aggregate#components()}. - */ - Path[] getComponentFiles() throws DataStoreException; -} diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java index 2eb90a5382..0be020891b 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java @@ -67,7 +67,7 @@ import org.apache.sis.xml.privy.ExceptionSimplifier; * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) */ -public abstract class URIDataStore extends DataStore implements StoreResource, ResourceOnFileSystem { +public abstract class URIDataStore extends DataStore implements StoreResource { /** * The {@link DataStoreProvider#LOCATION} parameter value, or {@code null} if none. */ @@ -76,7 +76,7 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R /** * The {@link #location} as a path, or {@code null} if none or if the URI cannot be converted to a path. * - * @see #getComponentFiles() + * @see #getFileSet() */ protected final Path locationAsPath; @@ -177,22 +177,28 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R } /** - * Returns the main and metadata locations as {@code Path} components, or an empty array if none. + * Returns the main and metadata locations as {@code Path} components, or an empty value if none. * The default implementation returns the storage specified at construction time converted to a - * {@link Path} if such conversion was possible, or an empty array otherwise. The array may also + * {@link Path} if such conversion was possible, or an empty value otherwise. The set may also * contains the path to the {@linkplain DataOptionKey#METADATA_PATH auxiliary metadata file}. * - * @return the URI to component files as paths, or an empty array if unknown. + * @return the URI to component files as paths, or an empty value if unknown. * @throws DataStoreException if an error occurred while getting the paths. */ @Override - public Path[] getComponentFiles() throws DataStoreException { + public Optional<FileSet> getFileSet() throws DataStoreException { + final Path[] paths; try { - final var paths = new Path[] {locationAsPath, getMetadataPath()}; - return ArraysExt.resize(paths, ArraysExt.removeDuplicated(paths, ArraysExt.removeNulls(paths))); + paths = new Path[] {locationAsPath, getMetadataPath()}; } catch (IOException e) { throw new DataStoreException(e); } + final int count = ArraysExt.removeDuplicated(paths, ArraysExt.removeNulls(paths)); + if (count != 0) { + return Optional.of(new FileSet(ArraysExt.resize(paths, count))); + } else { + return Optional.empty(); + } } /** @@ -510,12 +516,15 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R */ protected final void deleteAuxiliaryFile(final String extension) throws DataStoreException, IOException { String previous = null; - for (Path path : getComponentFiles()) { - final String base = getBaseFilename(path); - if (!base.equals(previous)) { - previous = base; - path = path.resolveSibling(base.concat(extension)); - Files.deleteIfExists(path); + final Optional<FileSet> files = getFileSet(); + if (files.isPresent()) { + for (Path path : files.get().getPaths()) { + final String base = getBaseFilename(path); + if (!base.equals(previous)) { + previous = base; + path = path.resolveSibling(base.concat(extension)); + Files.deleteIfExists(path); + } } } } 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 f70df8e68e..911ab593fc 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 @@ -164,14 +164,9 @@ public abstract class URIDataStoreProvider extends DataStoreProvider { /* * This fallback should not happen with `URIDataStore` implementation because the "location" parameter * is always present even if null. This fallback is for resources implementated by different classes. + * The first path is presumed the main file. */ - if (resource instanceof ResourceOnFileSystem) { - final Path[] paths = ((ResourceOnFileSystem) resource).getComponentFiles(); - if (paths != null && paths.length != 0) { - return paths[0]; // First path is presumed the main file. - } - } - return null; + return resource.getFileSet().flatMap((files) -> files.getPaths().stream().findFirst()).orElse(null); } /** diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java index 1d35e79ad3..3b5f993518 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Arrays; import java.util.Hashtable; import java.util.Locale; +import java.util.Optional; import java.io.IOException; import java.io.FileNotFoundException; import java.nio.file.NoSuchFileException; @@ -86,7 +87,7 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource /** * The filename extension of {@code "*.stx"} and {@code "*.clr"} files. * - * @see #getComponentFiles() + * @see #getFileSet() */ static final String STX = "stx", CLR = "clr"; @@ -132,11 +133,11 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource /** * Returns the {@linkplain #location} as a {@code Path} component together with auxiliary files. * - * @return the main file and auxiliary files as paths, or an empty array if unknown. + * @return the main file and auxiliary files as paths, or an empty value if unknown. * @throws DataStoreException if the URI cannot be converted to a {@link Path}. */ @Override - public Path[] getComponentFiles() throws DataStoreException { + public Optional<FileSet> getFileSet() throws DataStoreException { return listComponentFiles(PRJ, STX, CLR); } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java index 0da1ed09f6..99d2d8e0d3 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java @@ -18,6 +18,7 @@ package org.apache.sis.storage.esri; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.io.IOException; import java.nio.ByteOrder; import java.nio.file.Path; @@ -193,11 +194,11 @@ final class RawRasterStore extends RasterStore { /** * Returns the {@linkplain #location} as a {@code Path} component together with auxiliary files. * - * @return the main file and auxiliary files as paths, or an empty array if unknown. + * @return the main file and auxiliary files as paths, or an empty value if unknown. * @throws DataStoreException if the URI cannot be converted to a {@link Path}. */ @Override - public Path[] getComponentFiles() throws DataStoreException { + public Optional<FileSet> getFileSet() throws DataStoreException { return listComponentFiles(RawRasterStoreProvider.HDR, PRJ, STX, CLR); } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java index f59400c987..62fbf4df1c 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java @@ -258,7 +258,7 @@ class Store extends DataStore implements StoreResource, UnstructuredAggregate, D @Override public synchronized Metadata getMetadata() { if (metadata == null) { - final MetadataBuilder mb = new MetadataBuilder(); + final var mb = new MetadataBuilder(); mb.addResourceScope(ScopeCode.COLLECTION, Resources.formatInternational(Resources.Keys.DirectoryContent_1, getDisplayName())); mb.addLanguage(configuration.getOption(OptionKey.LOCALE), configuration.getOption(OptionKey.ENCODING), @@ -292,8 +292,8 @@ class Store extends DataStore implements StoreResource, UnstructuredAggregate, D @SuppressWarnings("ReturnOfCollectionOrArrayField") public synchronized Collection<Resource> components() throws DataStoreException { if (components == null) { - final List<DataStore> resources = new ArrayList<>(); - final NameFactory nameFactory = DefaultNameFactory.provider(); + final var resources = new ArrayList<DataStore>(); + final var nameFactory = DefaultNameFactory.provider(); try (DirectoryStream<Path> stream = Files.newDirectoryStream(location, this)) { for (final Path candidate : stream) { /* diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/WritableStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/WritableStore.java index f553e6549a..bc55ac81cc 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/WritableStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/WritableStore.java @@ -36,7 +36,6 @@ import org.apache.sis.storage.WritableAggregate; import org.apache.sis.storage.WritableFeatureSet; import org.apache.sis.storage.ReadOnlyStorageException; import org.apache.sis.storage.base.StoreUtilities; -import org.apache.sis.storage.base.ResourceOnFileSystem; import org.apache.sis.storage.internal.Resources; import org.apache.sis.io.stream.IOUtilities; @@ -142,7 +141,7 @@ final class WritableStore extends Store implements WritableAggregate { /** * Removes a {@code Resource} from this store. The resource must be a part of this {@code Aggregate}. - * For a folder store, this means that the resource must be a direct children of the directory managed + * For a folder store, this means that the resource must be a direct child of the directory managed * by this store. * * This operation is destructive: the {@link Resource} and it's related files will be deleted. @@ -158,25 +157,25 @@ final class WritableStore extends Store implements WritableAggregate { children.remove(path); return; } - } else if (resource instanceof ResourceOnFileSystem) { - final Path[] componentPaths = ((ResourceOnFileSystem) resource).getComponentFiles().clone(); - for (Path root : componentPaths) { - root = root.getParent(); - if (Files.isSameFile(root, location)) { - /* - * If we enter in this block, we have determined that at least one file is located in the - * directory managed by this store - NOT in a subdirectory since they could be managed by - * different folder stores. We assume that this root file is the "main" file. Other files - * could be in subdirectories, but we need to verify - we do not delete files outside. - */ - for (final Path path : componentPaths) { - if (path.startsWith(root)) { - Files.delete(path); - } + } else { + final FileSet fileset = resource.getFileSet().orElse(null); + if (fileset != null) { + for (Path root : fileset.getPaths()) { + root = root.getParent(); + if (root != null && Files.isSameFile(root, location)) { + /* + * If we enter in this block, we have determined that the main file is located in the + * directory managed by this store - NOT in a subdirectory because they could be managed + * by different folder stores. Note that the default implementation of `FileSet.delete()` + * will not delete any file in the parent directory. This is the desired behavior because + * such file could be shared by many resources (e.g. a global `metadata.xml` file). + */ + fileset.delete(); + children.values().removeIf((e) -> e == resource); + components = null; // Clear cache. TODO: we should do something more efficient. + return; } - children.values().removeIf((e) -> e == resource); - components = null; // Clear cache. TODO: we should do something more efficient. - return; + break; // Check only the first file, which should be the main file. } } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java index 6b041d655e..36b095adf6 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.HashMap; +import java.util.Optional; import java.util.logging.Level; import java.io.Closeable; import java.io.IOException; @@ -445,11 +446,11 @@ loop: for (int convention=0;; convention++) { /** * Returns paths to the main file together with auxiliary files. * - * @return paths to the main file and auxiliary files, or an empty array if unknown. + * @return paths to the main file and auxiliary files, or an empty value if unknown. * @throws DataStoreException if the URI cannot be converted to a {@link Path}. */ @Override - public synchronized Path[] getComponentFiles() throws DataStoreException { + public Optional<FileSet> getFileSet() throws DataStoreException { if (suffixWLD == null) try { getGridGeometry(MAIN_IMAGE); // Will compute `suffixWLD` as a side effect. } catch (URISyntaxException | IOException e) { diff --git a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/esri/AsciiGridStoreTest.java b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/esri/AsciiGridStoreTest.java index 67a74ab967..e4c06e0d50 100644 --- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/esri/AsciiGridStoreTest.java +++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/esri/AsciiGridStoreTest.java @@ -19,6 +19,9 @@ package org.apache.sis.storage.esri; import java.util.List; import java.awt.image.Raster; import java.awt.image.RenderedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.opengis.metadata.Metadata; import org.opengis.metadata.extent.GeographicBoundingBox; import org.apache.sis.coverage.Category; @@ -31,6 +34,7 @@ import org.apache.sis.storage.ProbeResult; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; import org.apache.sis.test.TestCase; +import static org.apache.sis.test.Assertions.assertMultilinesEquals; import static org.apache.sis.test.TestUtilities.getSingleton; // Specific to the geoapi-3.1 and geoapi-4.0 branches: @@ -43,6 +47,11 @@ import org.opengis.metadata.identification.Identification; * @author Martin Desruisseaux (Geomatys) */ public final class AsciiGridStoreTest extends TestCase { + /** + * Filename of the test file. + */ + private static final String FILENAME = "grid.asc"; + /** * Creates a new test case. */ @@ -53,7 +62,7 @@ public final class AsciiGridStoreTest extends TestCase { * Returns a storage connector with the URL to the test data. */ private static StorageConnector testData() { - return new StorageConnector(AsciiGridStoreTest.class.getResource("grid.asc")); + return new StorageConnector(AsciiGridStoreTest.class.getResource(FILENAME)); } /** @@ -70,7 +79,7 @@ public final class AsciiGridStoreTest extends TestCase { } /** - * Tests the metadata of the {@code "grid.asc"} file. This test reads only the header. + * Tests the metadata of the {@value #FILENAME} file. This test reads only the header. * It should not test sample dimensions or pixel values, because doing so read the full * image and is the purpose of {@link #testRead()}. * @@ -105,7 +114,7 @@ public final class AsciiGridStoreTest extends TestCase { } /** - * Tests reading a few values from the {@code "grid.asc"} file. + * Tests reading a few values from the {@value #FILENAME} file. * * @throws DataStoreException if an error occurred while reading the file. */ @@ -136,4 +145,48 @@ public final class AsciiGridStoreTest extends TestCase { assertSame(coverage, store.read(null, null)); } } + + /** + * Tests {@link AsciiGridStore#getFileSet()}. Since {@link AsciiGridStore} inherits + * the default implementation, this is actually a test of {@code Resource.FileSet}. + * + * @throws DataStoreException if an error occurred while fetching the list of files. + * @throws IOException if an error occurred while copying the file. + */ + @Test + public void testFileSet() throws DataStoreException, IOException { + AsciiGridStore.FileSet fileset; + try (AsciiGridStore store = new AsciiGridStore(null, testData(), true)) { + fileset = store.getFileSet().orElseThrow(); + } + final Path source = fileset.getPaths().iterator().next(); + assertEquals(FILENAME, source.getFileName().toString()); + /* + * Tests the copy operation in a temporary directory. + * This is using the default implementation of `FileSet.copy(…)`, + */ + final Path dir = Files.createTempDirectory("sis-"); + Path target = null; + try { + target = fileset.copy(dir); + assertEquals(dir, target.getParent()); + assertEquals(FILENAME, target.getFileName().toString()); + assertMultilinesEquals(Files.readString(source), Files.readString(target)); + /* + * In order to test the delete operation, we need to open a new data store on the file + * that we just copied. Otherwise, `fileset.delete()` would delete the original file. + */ + try (AsciiGridStore store = new AsciiGridStore(null, new StorageConnector(target), true)) { + fileset = store.getFileSet().orElseThrow(); + } + fileset.delete(); + assertTrue(Files.notExists(target)); + target = null; + } finally { + if (target != null) { + Files.deleteIfExists(target); + } + Files.deleteIfExists(dir); + } + } } diff --git a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Driver.java b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Driver.java index 3b0842a556..84cbda2c3b 100644 --- a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Driver.java +++ b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Driver.java @@ -19,6 +19,9 @@ package org.apache.sis.storage.gdal; import java.util.List; import java.util.AbstractList; import java.util.Objects; +import java.net.URI; +import java.nio.file.Path; +import java.io.IOException; import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.Linker; @@ -32,6 +35,7 @@ import org.apache.sis.util.collection.TreeTable; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.privy.Strings; +import org.apache.sis.storage.Resource; import org.apache.sis.storage.DataStoreException; @@ -165,6 +169,101 @@ public final class Driver { return GDAL.toString(result); } + /** + * List of files of a <abbr>GDAL</abbr> data set, together with methods for copying or deleting them. + */ + final class FileList extends Resource.FileSet { + /** Location of the data store as a path. */ + private final Path path; + + /** Location of the data store as a URL. */ + private final URI location; + + /** Creates a new file set for the given files. */ + FileList(final Path[] paths, final Path path, final URI location) { + super(paths); + this.path = path; + this.location = location; + } + + /** + * Returns the URL (<abbr>GDAL</abbr> syntax) to the data set. + */ + private String getURL() { + return Opener.toURL(location, path, true); + } + + /** + * Copies the files to the given directory. The source should be an <abbr>URL</abbr> recognized + * by <abbr>GDAL</abbr> because {@code FileList} can be returned only by {@link GDALStore}. + * However, the destination could be a pure-Java file system. + */ + @Override + public Path copy(final Path destDir) throws IOException { + final Path dest = destDir.resolve(path.getFileName()); + final String target = Opener.toURL(dest.toUri(), dest, false); + if (target != null) try { + copyDataSet(getURL(), target); + return dest; + } catch (DataStoreException e) { + throw new IOException(e); + } else { + return super.copy(destDir); + } + } + + /** + * Deletes the files of the data set. + */ + @Override + public void delete() throws IOException { + try { + deleteDataSet(getURL()); + } catch (DataStoreException e) { + throw new IOException(e); + } + } + } + + /** + * Copies the files of a data set managed by this driver. + * It is caller responsibility to ensure that this driver is the correct one for the data set to copy. + * No {@link GDALStore} should be opened on the source at the time that this method is invoked. + * + * @param source path or <abbr>URL</abbr> (<abbr>GDAL</abbr> syntax) of the main file of the data set to copy. + * @param target path or <abbr>URL</abbr> (<abbr>GDAL</abbr> syntax) of the desired target file. + * @throws DataStoreException if an error occurred while invoking a <abbr>GDAL</abbr> function. + */ + public void copyDataSet(final String source, final String target) throws DataStoreException { + final int err; + final var gdal = owner.GDAL().copyDataset; + try (var arena = Arena.ofConfined()) { + err = (int) gdal.invokeExact(handle, arena.allocateFrom(target), arena.allocateFrom(source)); + } catch (Throwable e) { + throw GDAL.propagate(e); + } + ErrorHandler.checkCPLErr(err); + } + + /** + * Deletes the files of a data set managed by this driver. + * It is caller responsibility to ensure that this driver is the correct one for the data set to delete. + * No {@link GDALStore} should be opened on the data set when this method is invoked. + * + * @param dataset path or <abbr>URL</abbr> (<abbr>GDAL</abbr> syntax) of the main file of the data set to delete. + * @throws DataStoreException if an error occurred while invoking a <abbr>GDAL</abbr> function. + */ + public void deleteDataSet(final String dataset) throws DataStoreException { + final int err; + final var gdal = owner.GDAL().deleteDataset; + try (var arena = Arena.ofConfined()) { + err = (int) gdal.invokeExact(handle, arena.allocateFrom(dataset)); + } catch (Throwable e) { + throw GDAL.propagate(e); + } + ErrorHandler.checkCPLErr(err); + } + /** * Returns the <abbr>GDAL</abbr> version number together with the list of drivers. * See {@link GDALStoreProvider#configuration()} for the public documentation. diff --git a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java index 246aeb96b9..a8a6d2ee6e 100644 --- a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java +++ b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java @@ -93,6 +93,18 @@ final class GDAL extends NativeFunctions { */ final MethodHandle identifyDriver; + /** + * <abbr>GDAL</abbr> {@code CPLErr GDALCopyDatasetFiles(GDALDriverH, const char *pszNewName, const char *pszOldName)}. + * Copy the files of a dataset. + */ + final MethodHandle copyDataset; + + /** + * <abbr>GDAL</abbr> {@code CPLErr GDALDeleteDataset(GDALDriverH, const char*)}. + * Delete named dataset. + */ + final MethodHandle deleteDataset; + /** * <abbr>GDAL</abbr> {@code GDALDatasetH GDALOpenEx(const char *pszFilename, …)}. * Opens a raster or vector file by invoking the open method of each driver in turn. @@ -304,6 +316,17 @@ final class GDAL extends NativeFunctions { ValueLayout.ADDRESS, // const char* name ValueLayout.ADDRESS)); // const char* domain + copyDataset = lookup(linker, "GDALCopyDatasetFiles", FunctionDescriptor.of( + ValueLayout.JAVA_INT, // CPLErr (return type) + ValueLayout.ADDRESS, // GDALDriverH + ValueLayout.ADDRESS, // const char* pszNewName + ValueLayout.ADDRESS)); // const char* pszOldName + + deleteDataset = lookup(linker, "GDALDeleteDataset", FunctionDescriptor.of( + ValueLayout.JAVA_INT, // CPLErr (return type) + ValueLayout.ADDRESS, // GDALDriverH + ValueLayout.ADDRESS)); // const char* pszName + // For Opener close = lookup(linker, "GDALClose", acceptPointerReturnInt); open = lookup(linker, "GDALOpenEx", FunctionDescriptor.of(ValueLayout.ADDRESS, diff --git a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java index f5272fc1a7..57d60b202c 100644 --- a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java +++ b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java @@ -44,7 +44,6 @@ import org.apache.sis.storage.DataStoreClosedException; import org.apache.sis.storage.InternalDataStoreException; import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.base.MetadataBuilder; -import org.apache.sis.storage.base.ResourceOnFileSystem; import org.apache.sis.storage.base.URIDataStore; import org.apache.sis.util.privy.UnmodifiableArrayList; import org.apache.sis.util.iso.DefaultNameFactory; @@ -60,7 +59,7 @@ import org.apache.sis.system.Cleaners; * @version 1.5 * @since 1.5 */ -public class GDALStore extends DataStore implements Aggregate, ResourceOnFileSystem { +public class GDALStore extends DataStore implements Aggregate { /** * The {@link GDALStoreProvider#LOCATION} parameter value, or {@code null} if none. * @@ -71,7 +70,7 @@ public class GDALStore extends DataStore implements Aggregate, ResourceOnFileSys /** * Path of the file opened by this data store, or {@code null} if none. * - * @see #getComponentFiles() + * @see #getFileSet() */ private final Path path; @@ -154,7 +153,7 @@ public class GDALStore extends DataStore implements Aggregate, ResourceOnFileSys path = connector.getStorageAs(Path.class); String url = connector.commit(String.class, GDALStoreProvider.NAME); if (location != null) { - url = Opener.toURL(location, path); + url = Opener.toURL(location, path, true); } Opener opener; opener = new Opener(provider, url, drivers); @@ -275,11 +274,11 @@ public class GDALStore extends DataStore implements Aggregate, ResourceOnFileSys * <abbr>GDAL</abbr> provides a {@code GDALCopyDatasetFiles} function for this purpose. * That function is not yet used by Apache <abbr>SIS</abbr>. * - * @return files used by this resource, or an empty array if unknown. + * @return files used by this resource, or an empty value if unknown. * @throws DataStoreException if the list of files cannot be obtained. */ @Override - public synchronized Path[] getComponentFiles() throws DataStoreException { + public synchronized Optional<FileSet> getFileSet() throws DataStoreException { final GDAL gdal = getProvider().GDAL(); final List<String> files; try { @@ -292,18 +291,23 @@ public class GDALStore extends DataStore implements Aggregate, ResourceOnFileSys } catch (Throwable e) { throw GDAL.propagate(e); } - if (files == null || files.isEmpty()) { - return (path != null) ? new Path[] {path} : new Path[0]; - } - final var paths = new Path[files.size()]; - for (int i=0; i < paths.length; i++) { - var item = Path.of(files.get(i)); - if (path != null) { - item = path.resolveSibling(item); + final FileSet fs; + if (files != null && !files.isEmpty()) { + final var paths = new Path[files.size()]; + for (int i=0; i < paths.length; i++) { + var item = Path.of(files.get(i)); + if (path != null) { + item = path.resolveSibling(item); + } + paths[i] = item; } - paths[i] = item; + fs = getDriver().new FileList(paths, path, location); + } else if (path != null) { + fs = new FileSet(path); + } else { + return Optional.empty(); } - return paths; + return Optional.of(fs); } /** diff --git a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java index 4a2539745f..642e30d89d 100644 --- a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java +++ b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java @@ -117,9 +117,10 @@ final class Opener implements Runnable { * * @param location URL to the file to open (mandatory). * @param path URL as a path on the file system, or {@code null} if none. - * @return <abbr>URL</abbr> for <var>GDAL</var>. + * @param fallback whether to use a fallback value if the URI is not recognized. + * @return <abbr>URL</abbr> for <var>GDAL</var>. May be {@code null} if unrecognized and no fallback is used. */ - static String toURL(final URI location, final Path path) { + static String toURL(final URI location, final Path path, final boolean fallback) { String url; final String scheme = location.getScheme(); if (path != null && "file".equalsIgnoreCase(scheme)) { @@ -128,6 +129,8 @@ final class Opener implements Runnable { url = location.toString(); if (scheme != null && VSICURL.contains(scheme.toLowerCase(Locale.US))) { url = "/vsicurl/".concat(url); + } else if (!fallback) { + return null; } } return url; @@ -147,7 +150,7 @@ final class Opener implements Runnable { String url; final URI location = connector.getStorageAs(URI.class); if (location != null) { - url = toURL(location, connector.getStorageAs(Path.class)); + url = toURL(location, connector.getStorageAs(Path.class), true); } else { url = connector.getStorageAs(String.class); } diff --git a/incubator/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java b/incubator/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java index d316abd1e4..78755043bd 100644 --- a/incubator/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java +++ b/incubator/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java @@ -16,7 +16,10 @@ */ package org.apache.sis.storage.gdal; +import java.util.Collection; import java.nio.file.Path; +import java.nio.file.Files; +import java.io.IOException; import java.awt.image.DataBuffer; import java.awt.image.RenderedImage; import org.opengis.util.GenericName; @@ -49,6 +52,11 @@ import static org.junit.jupiter.api.Assertions.*; * @author Quentin BIALOTA (Geomatys) */ public final class GDALStoreTest { + /** + * Name of the test file. + */ + private static final String FILENAME = "test.tiff"; + /** * Creates a new test case. */ @@ -72,7 +80,7 @@ public final class GDALStoreTest { * Returns the storage connector to the test file to use as input. */ private static StorageConnector input() { - return new StorageConnector(GDALStoreTest.class.getResource("test.tiff")); + return new StorageConnector(GDALStoreTest.class.getResource(FILENAME)); } /** @@ -159,11 +167,54 @@ public final class GDALStoreTest { } // Check the file components - final Path[] paths = store.getComponentFiles(); - assertEquals(1, paths.length); - assertEquals("test.tiff", paths[0].getFileName().toString()); + final Collection<Path> paths = store.getFileSet().orElseThrow().getPaths(); + assertEquals(1, paths.size()); + assertEquals(FILENAME, paths.iterator().next().getFileName().toString()); } assertTrue(foundGrid); assertTrue(foundBand); } + + /** + * Tests {@link GDALStore#getFileSet()}. + * + * @throws DataStoreException if an error occurred while fetching the list of files. + * @throws IOException if an error occurred while copying the file. + */ + @Test + public void testFileSet() throws DataStoreException, IOException { + final var provider = new GDALStoreProvider(); + GDALStore.FileSet fileset; + try (GDALStore store = new GDALStore(provider, input())) { + fileset = store.getFileSet().orElseThrow(); + } + final Path source = fileset.getPaths().iterator().next(); + assertEquals(FILENAME, source.getFileName().toString()); + /* + * Tests the copy operation in a temporary directory. + */ + final Path dir = Files.createTempDirectory("sis-"); + Path target = null; + try { + target = fileset.copy(dir); + assertEquals(dir, target.getParent()); + assertEquals(FILENAME, target.getFileName().toString()); + assertArrayEquals(Files.readAllBytes(source), Files.readAllBytes(target)); + /* + * In order to test the delete operation, we need to open a new data store on the file + * that we just copied. Otherwise, `fileset.delete()` would delete the original file. + */ + try (GDALStore store = new GDALStore(provider, new StorageConnector(target))) { + fileset = store.getFileSet().orElseThrow(); + } + fileset.delete(); + assertTrue(Files.notExists(target)); + target = null; + } finally { + if (target != null) { + Files.deleteIfExists(target); + } + Files.deleteIfExists(dir); + } + } } diff --git a/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java b/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java index 47167d894e..ea4db47c69 100644 --- a/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java +++ b/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java @@ -18,6 +18,7 @@ package org.apache.sis.storage.geopackage; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.List; @@ -40,7 +41,6 @@ import org.apache.sis.storage.WritableAggregate; import org.apache.sis.storage.sql.DataAccess; import org.apache.sis.storage.sql.SQLStore; import org.apache.sis.storage.event.StoreListeners; -import org.apache.sis.storage.base.ResourceOnFileSystem; import org.apache.sis.metadata.sql.privy.ScriptRunner; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.ArraysExt; @@ -76,7 +76,7 @@ import org.apache.sis.util.Workaround; * * @see <a href="https://www.geopackage.org/spec140/index.html">OGC® GeoPackage Encoding Standard version 1.4.0</a> */ -public class GpkgStore extends SQLStore implements WritableAggregate, ResourceOnFileSystem { +public class GpkgStore extends SQLStore implements WritableAggregate { /** * Type of data which for which {@code GpkgStore} will delegate the handling to the {@code SQLStore} parent. * A data type is a values of the {@value Content#DATA_TYPE} column of the {@value Content#TABLE_NAME} table. @@ -87,7 +87,7 @@ public class GpkgStore extends SQLStore implements WritableAggregate, ResourceOn * Path to the SQLite file, or {@code null} if the store was opened with a {@link DataSource}. * This is for information purpose only. * - * @see #getComponentFiles() + * @see #getFileSet() */ private final Path path; @@ -154,21 +154,23 @@ public class GpkgStore extends SQLStore implements WritableAggregate, ResourceOn } /** - * Returns the path to the main file and its auxiliary files, or an empty array if unknown. + * Returns the path to the main file and its auxiliary files, or an empty value if unknown. * - * @return the Geopackage file and auxiliary files, or an empty array if unknown. + * @return the Geopackage file and auxiliary files, or an empty value if unknown. */ @Override - public Path[] getComponentFiles() { + public Optional<FileSet> getFileSet() { if (path == null) { - return new Path[0]; + return Optional.empty(); } + final var paths = new ArrayList<Path>(3); + paths.add(path); final String filename = path.getFileName().toString(); - return new Path[] { - path, - path.resolveSibling(filename.concat(Initializer.WAL_SUFFIX)), - path.resolveSibling(filename.concat(Initializer.SHM_SUFFIX)) - }; + for (String suffix : new String[] {Initializer.WAL_SUFFIX, Initializer.SHM_SUFFIX}) { + Path aux = path.resolveSibling(filename.concat(suffix)); + if (Files.exists(aux)) paths.add(aux); + } + return Optional.of(new FileSet(paths)); } /** diff --git a/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFRecordReader.java b/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFRecordReader.java index 246c3fdd8d..45ed42207a 100644 --- a/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFRecordReader.java +++ b/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFRecordReader.java @@ -45,7 +45,7 @@ public final class GSFRecordReader implements AutoCloseable { public GSFRecordReader(GSFStore store) throws DataStoreException { this.store = store; - this.file = store.getComponentFiles()[0]; + this.file = store.getFileSet().orElseThrow().getPaths().iterator().next(); this.gsf = store.getProvider().GSF(); this.arena = Arena.ofShared(); diff --git a/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFStore.java b/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFStore.java index bb70c1158d..644c74445c 100644 --- a/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFStore.java +++ b/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFStore.java @@ -24,7 +24,6 @@ import org.apache.sis.storage.DataStore; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.base.MetadataBuilder; -import org.apache.sis.storage.base.ResourceOnFileSystem; import org.apache.sis.storage.base.URIDataStore; import org.opengis.metadata.Metadata; import org.opengis.parameter.ParameterValueGroup; @@ -33,7 +32,7 @@ import org.opengis.parameter.ParameterValueGroup; * * @author Johann Sorel (Geomatys) */ -public final class GSFStore extends DataStore implements ResourceOnFileSystem { +public final class GSFStore extends DataStore { /** * The {@link GSFStoreProvider#LOCATION} parameter value, or {@code null} if none. * @@ -43,7 +42,7 @@ public final class GSFStore extends DataStore implements ResourceOnFileSystem { /** * Path of the file opened by this data store, or {@code null} if none. * - * @see #getComponentFiles() + * @see #getFileSet() */ private final Path path; /** @@ -92,8 +91,8 @@ public final class GSFStore extends DataStore implements ResourceOnFileSystem { } @Override - public Path[] getComponentFiles() throws DataStoreException { - return new Path[]{path}; + public Optional<FileSet> getFileSet() throws DataStoreException { + return Optional.of(new FileSet(path)); } @Override diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java index 2457bb271c..a3e36941cd 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java @@ -90,7 +90,6 @@ import org.apache.sis.storage.Query; import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.UnsupportedQueryException; import org.apache.sis.storage.WritableFeatureSet; -import org.apache.sis.storage.base.ResourceOnFileSystem; import org.apache.sis.storage.shapefile.cpg.CpgFiles; import org.apache.sis.storage.shapefile.dbf.DBFField; import org.apache.sis.storage.shapefile.dbf.DBFHeader; @@ -127,7 +126,7 @@ import org.opengis.filter.ValueReference; * * @author Johann Sorel (Geomatys) */ -public final class ShapefileStore extends DataStore implements WritableFeatureSet, ResourceOnFileSystem { +public final class ShapefileStore extends DataStore implements WritableFeatureSet { private static final String GEOMETRY_NAME = "geometry"; private static final Logger LOGGER = Logger.getLogger("org.apache.sis.storage.shapefile"); @@ -270,11 +269,11 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe * {@inheritDoc } */ @Override - public Path[] getComponentFiles() throws DataStoreException { - return featureSetView.getComponentFiles(); + public Optional<FileSet> getFileSet() throws DataStoreException { + return featureSetView.getFileSet(); } - private class AsFeatureSet extends AbstractFeatureSet implements WritableFeatureSet, ResourceOnFileSystem { + private class AsFeatureSet extends AbstractFeatureSet implements WritableFeatureSet { private final Rectangle2D.Double filter; private final Set<String> dbfProperties; @@ -853,8 +852,8 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe } @Override - public Path[] getComponentFiles() throws DataStoreException { - final List<Path> paths = new ArrayList<>(); + public Optional<FileSet> getFileSet() throws DataStoreException { + final var paths = new ArrayList<Path>(5); final Path shp = files.shpFile; final Path shx = files.getShx(false); final Path dbf = files.getDbf(false); @@ -865,7 +864,7 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe if (dbf != null && Files.exists(dbf)) paths.add(dbf); if (prj != null && Files.exists(prj)) paths.add(prj); if (cpg != null && Files.exists(cpg)) paths.add(cpg); - return paths.toArray(Path[]::new); + return Optional.of(new FileSet(paths)); } } diff --git a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java index ec293c91fb..b5f642304f 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java +++ b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java @@ -180,13 +180,13 @@ public class ShapefileStoreTest { public void testFiles() throws URISyntaxException, DataStoreException { final URL url = ShapefileStoreTest.class.getResource("/org/apache/sis/storage/shapefile/point.shp"); try (final ShapefileStore store = new ShapefileStore(Paths.get(url.toURI()))) { - Path[] componentFiles = store.getComponentFiles(); - assertEquals(5, componentFiles.length); - assertTrue(componentFiles[0].toString().endsWith("point.shp")); - assertTrue(componentFiles[1].toString().endsWith("point.shx")); - assertTrue(componentFiles[2].toString().endsWith("point.dbf")); - assertTrue(componentFiles[3].toString().endsWith("point.prj")); - assertTrue(componentFiles[4].toString().endsWith("point.cpg")); + Iterator<Path> componentFiles = store.getFileSet().orElseThrow().getPaths().iterator(); + assertTrue(componentFiles.next().toString().endsWith("point.shp")); + assertTrue(componentFiles.next().toString().endsWith("point.shx")); + assertTrue(componentFiles.next().toString().endsWith("point.dbf")); + assertTrue(componentFiles.next().toString().endsWith("point.prj")); + assertTrue(componentFiles.next().toString().endsWith("point.cpg")); + assertFalse(componentFiles.hasNext()); } } @@ -198,8 +198,7 @@ public class ShapefileStoreTest { final Path temp = folder.resolve("test.shp"); final String name = temp.getFileName().toString().split("\\.")[0]; try (final ShapefileStore store = new ShapefileStore(temp)) { - Path[] componentFiles = store.getComponentFiles(); - assertEquals(0, componentFiles.length); + assertTrue(store.getFileSet().orElseThrow().getPaths().isEmpty()); {//create type final FeatureType type = createType(); @@ -207,13 +206,13 @@ public class ShapefileStoreTest { } {//check files have been created - componentFiles = store.getComponentFiles(); - assertEquals(5, componentFiles.length); - assertTrue(componentFiles[0].toString().endsWith(name+".shp")); - assertTrue(componentFiles[1].toString().endsWith(name+".shx")); - assertTrue(componentFiles[2].toString().endsWith(name+".dbf")); - assertTrue( componentFiles[3].toString().endsWith(name+".prj")); - assertTrue(componentFiles[4].toString().endsWith(name+".cpg")); + Iterator<Path> componentFiles = store.getFileSet().orElseThrow().getPaths().iterator(); + assertTrue(componentFiles.next().toString().endsWith(name+".shp")); + assertTrue(componentFiles.next().toString().endsWith(name+".shx")); + assertTrue(componentFiles.next().toString().endsWith(name+".dbf")); + assertTrue(componentFiles.next().toString().endsWith(name+".prj")); + assertTrue(componentFiles.next().toString().endsWith(name+".cpg")); + assertFalse(componentFiles.hasNext()); } {// check created type diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java index 61a36be73e..fa26f7162d 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java @@ -19,6 +19,7 @@ package org.apache.sis.gui.dataset; import java.awt.Desktop; import java.util.List; import java.util.ArrayList; +import java.util.Collection; import java.io.File; import java.nio.file.Path; import java.net.URL; @@ -32,7 +33,6 @@ import javafx.scene.input.ClipboardContent; import org.apache.sis.gui.internal.ExceptionReporter; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.Resource; -import org.apache.sis.storage.base.ResourceOnFileSystem; import org.apache.sis.storage.base.URIDataStoreProvider; import org.apache.sis.io.stream.IOUtilities; @@ -141,26 +141,27 @@ final class PathAction implements EventHandler<ActionEvent> { * This list of files will usually be ignored and only the `file` text will be pasted, * but it depends on the application where files will be pasted. */ - List<File> files = null; - if (resource instanceof ResourceOnFileSystem) try { - final Path[] components = ((ResourceOnFileSystem) resource).getComponentFiles(); - if (components != null) { - files = new ArrayList<>(components.length); - for (final Path p : components) try { - if (p != null) files.add(p.toFile()); - } catch (UnsupportedOperationException e) { - // Ignore and try to add other components. - } - } + Collection<Path> components = null; + try { + components = resource.getFileSet().map(Resource.FileSet::getPaths).orElse(null); } catch (DataStoreException e) { ResourceTree.unexpectedException("copy", e); + } + List<File> files = null; + if (components != null) { + files = new ArrayList<>(components.size()); + for (final Path p : components) try { + if (p != null) files.add(p.toFile()); + } catch (UnsupportedOperationException e) { + // Ignore and try to add other components. + } } else if (file instanceof File) { files = List.of((File) file); } /* * Put in the clipboard all information that we could get. */ - final ClipboardContent content = new ClipboardContent(); + final var content = new ClipboardContent(); content.putString(file.toString()); if (files != null) content.putFiles(files); if (uri != null) content.putUrl(uri.toString());