This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 8bdb0c9430847594b355f0d2dac42ad76f19adf9 Author: Martin Desruisseaux <[email protected]> AuthorDate: Wed Sep 30 16:53:27 2026 +0900 When opening a folder as a DataStore, exclude auxiliary files from the list of resources. --- .../apache/sis/storage/esri/RawRasterStore.java | 2 + .../main/org/apache/sis/storage/folder/Store.java | 139 +++++++++++++-------- 2 files changed, 91 insertions(+), 50 deletions(-) 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 a165413074..6bc1e08763 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 @@ -550,9 +550,11 @@ final class RawRasterStore extends RasterStore { * @throws DataStoreException if an error occurred while closing this data store. */ @Override + @SuppressWarnings("ConvertToTryWithResources") public void close() throws DataStoreException { try { listeners.close(); // Should never fail. + @SuppressWarnings("LocalVariableHidesMemberVariable") final ChannelDataInput input = this.input; if (input != null) try { input.channel.close(); 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 6d377432cd..d30871166d 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 @@ -18,7 +18,7 @@ package org.apache.sis.storage.folder; import java.util.Map; import java.util.List; -import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.Collection; import java.util.Optional; import java.util.logging.Level; @@ -109,14 +109,14 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate * * @see #getMetadata() */ - private transient Metadata metadata; + private Metadata metadata; /** * Resources in the folder given at construction time, created when first needed. * * @see #components() */ - transient Collection<Resource> components; + Collection<Resource> components; /** * The provider to use for probing the directory content, opening files and creating new files. @@ -126,10 +126,10 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate protected final DataStoreProvider componentProvider; /** - * {@code true} if {@link #sharedRepository(Path)} has already been invoked for {@link #locationAsPath}. + * {@code true} if {@link #sharedDirectoryReported()} has already been invoked for {@link #locationAsPath}. * This is used for avoiding to report the same message many times. */ - private transient boolean sharedRepositoryReported; + private boolean sharedDirectoryReported; /** * A structured view of this aggregate, or {@code null} if not net computed. @@ -137,7 +137,7 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate * * @see #getStructuredView() */ - private transient Resource structuredView; + private Resource structuredView; /** * Creates a new folder store from the given file, path or URI. @@ -283,51 +283,50 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate @SuppressWarnings("ReturnOfCollectionOrArrayField") public synchronized Collection<Resource> components() throws DataStoreException { if (components == null) { - final var resources = new ArrayList<DataStore>(); + final var resources = new LinkedHashMap<Path, DataStore>(); // May contain null values for auxiliary files. final var nameFactory = DefaultNameFactory.provider(); + DataStoreException closeErrors = null; try (DirectoryStream<Path> stream = Files.newDirectoryStream(locationAsPath, this)) { - for (final Path candidate : stream) { + for (final Path childPath : stream) { + final Path realPath = childPath.toRealPath(); + if (resources.containsKey(realPath)) { + // Resource already added as an auxiliary file of another resource (for example, a ".prj" file). + continue; + } /* - * The candidate path may be a symbolic link to a file that we have previously read. - * In such case, use the existing data store. A use case is a directory containing - * hundred of GeoTIFF files all accompanied by ".prj" files having identical content. - * (Note: those ".prj" files should be invisible since they should be identified as - * GeoTIFF auxiliary files, but current Store implementation does not know that). + * The path of the child may be a symbolic link to a file that we have already read + * in another directory. In such case, use (actually share) the existing data store. */ - final Path real = candidate.toRealPath(); - DataStore next = children.get(real); - if (next instanceof Store) { - ((Store) next).sharedRepository(real); // Warn about directories only. - } - if (next == null) { + DataStore child = children.get(realPath); + if (child == null) { /* * The candidate file has never been read before. Try to read it now. - * If the file format is unknown (UnsupportedStorageException), we will - * check if we can open it as a child folder store before to skip it. + * If the file format is unknown (UnsupportedStorageException), + * try to open the path as a child folder store. */ - final StorageConnector connector = new StorageConnector(configuration, candidate); + final var connector = new StorageConnector(configuration, childPath); connector.setOption(OptionKey.PARENT_LISTENERS, listeners); connector.setOption(OptionKey.OPEN_OPTIONS, new StandardOpenOption[] { StandardOpenOption.READ // Restrict to read-only mode. }); try { if (componentProvider == null) { - next = DataStores.open(connector); // May throw UnsupportedStorageException. + child = DataStores.open(connector); // May throw UnsupportedStorageException. } else if (componentProvider.probeContent(connector).isSupported()) { - next = componentProvider.open(connector); // Open a file of specified format. - } else if (Files.isDirectory(candidate)) { - next = new Store(this, connector, nameFactory); // Open a sub-directory. + child = componentProvider.open(connector); // Open a file of specified format. + } else if (Files.isDirectory(childPath)) { + child = new Store(this, connector, nameFactory); // Open a sub-directory. } else { connector.closeAllExcept(null); // Not the format specified at construction time. continue; } } catch (UnsupportedStorageException ex) { - if (!Files.isDirectory(candidate)) { + if (!Files.isDirectory(childPath)) { connector.closeAllExcept(null); listeners.warning(Level.FINE, null, ex); continue; } - next = new Store(this, connector, nameFactory); + child = new Store(this, connector, nameFactory); } catch (DataStoreException ex) { try { connector.closeAllExcept(null); @@ -336,20 +335,26 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate } throw ex; } - /* - * At this point we got the data store. It could happen that a store for - * the same file has been added concurrently, so we need to check again. - */ - final DataStore existing = children.putIfAbsent(real, next); - if (existing != null) { - next.close(); - next = existing; - if (next instanceof Store) { - ((Store) next).sharedRepository(real); // Warn about directories only. + } + /* + * If the resource has auxiliary files and if we opened some of them as standalone resources, + * close and exclude the latter. The null value is for remembering to not open that file again. + */ + resources.put(realPath, child); + for (final Path auxiliary : child.getFileSet().map(FileSet::getPaths).orElse(List.of())) { + if (!realPath.equals(auxiliary)) { + final DataStore existing = resources.put(auxiliary.toRealPath(), null); + if (existing != null && !children.containsKey(realPath)) try { + existing.close(); + } catch (DataStoreException ex) { + if (closeErrors == null) { + closeErrors = ex; + } else { + closeErrors.addSuppressed(ex); + } } } } - resources.add(next); } } catch (DirectoryIteratorException | UncheckedIOException ex) { // The cause is an IOException (no other type allowed). @@ -359,7 +364,45 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate } catch (BackingStoreException ex) { throw ex.unwrapOrRethrow(DataStoreException.class); } - components = Containers.copyToImmutableList(resources, Resource.class); + /* + * Finished the iteration over all directory entries. Remove auxiliary files (null values). + * For non-null values, check if the store has already been opened in another directory. + * It may happen if the directory tree contains symbolic links. + */ + for (var it = resources.entrySet().iterator(); it.hasNext();) { + final Map.Entry<Path, DataStore> entry = it.next(); + final DataStore next = entry.getValue(); + if (next == null) { + it.remove(); + continue; + } + final Path realPath = entry.getKey(); + final DataStore existing = children.putIfAbsent(realPath, next); + if (existing != null) { + entry.setValue(existing); + /* + * Logs a warning if the path is a directory which has already been read. + * It may happen if the directory tree contains symbolic links. + * The warning will be reported one time per directory. + */ + if (existing instanceof Store && ((Store) existing).sharedDirectoryReported()) { + listeners.warning(message(Resources.Keys.SharedDirectory_1, realPath)); + } + try { + next.close(); + } catch (DataStoreException ex) { + if (closeErrors == null) { + closeErrors = ex; + } else { + closeErrors.addSuppressed(ex); + } + } + } + } + components = Containers.copyToImmutableList(resources.values(), Resource.class); + if (closeErrors != null) { + listeners.warning(Level.FINE, null, closeErrors); + } } return components; // Safe because unmodifiable list. } @@ -372,17 +415,13 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate } /** - * Logs a warning about a file that could be read, but happen to be a directory that we have read previously. - * We could add the existing {@link Aggregate} instance in the parent {@code Aggregate} that we are building, - * but doing so may create a cycle. Current version logs a warning instead because users may not be prepared - * to handle cycles. Note that we have no guarantee that a cycle really exists at this stage, only that it may - * exist. + * Whether to logs a warning about a file that could be read, + * but happens to be a directory that we have read before. */ - private void sharedRepository(final Path candidate) { - if (!sharedRepositoryReported) { - sharedRepositoryReported = true; - listeners.warning(message(Resources.Keys.SharedDirectory_1, candidate)); - } + private synchronized boolean sharedDirectoryReported() { + final boolean r = sharedDirectoryReported; + sharedDirectoryReported = true; + return r; } /**
