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 172d3c1866dd638f8ae9fd550385dfe134d80924 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Dec 9 14:44:34 2023 +0100 Allow to specify a metadata auxiliary file using wildcard. For example if the main file is "city-center.tiff" and metadata path is "*.xml", then the actual metadata file will be "city-center.xml". --- .../main/org/apache/sis/io/stream/IOUtilities.java | 61 +++++++---- .../main/org/apache/sis/storage/DataOptionKey.java | 5 + .../org/apache/sis/storage/base/PRJDataStore.java | 6 +- .../org/apache/sis/storage/base/URIDataStore.java | 115 ++++++++++++++++++--- .../sis/storage/esri/RawRasterStoreProvider.java | 5 +- .../apache/sis/storage/folder/WritableStore.java | 3 +- .../apache/sis/storage/image/WorldFileStore.java | 5 +- .../org/apache/sis/io/stream/IOUtilitiesTest.java | 15 +-- 8 files changed, 168 insertions(+), 47 deletions(-) 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 a4232f2849..9c3d270eb4 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 @@ -72,6 +72,18 @@ public final class IOUtilities extends Static { */ public static final String CURRENT_DIRECTORY_SYMBOL = "."; + /** + * The separator between a filename components in an URI. + * Declared for making easier to identify the code doing such separation. + */ + public static final char URI_PATH_SEPARATOR = '/'; + + /** + * The separator between a filename and its suffix. + * Declared for making easier to identify the code doing such separation. + */ + public static final char EXTENSION_SEPARATOR = '.'; + /** * Do not allow instantiation of this class. */ @@ -113,7 +125,7 @@ public final class IOUtilities extends Static { * {@link URI} or {@link CharSequence} instance. If no extension is found, returns an empty string. * If the given object is of unknown type, return {@code null}. * - * @param path the filename extension (may be an empty string), or {@code null} if unknown. + * @param path the path as an instance of one of the above-cited types, or {@code null}. * @return the extension in the given path, or an empty string if none, or {@code null} * if the given object is null or of unknown type. */ @@ -123,6 +135,11 @@ public final class IOUtilities extends Static { /** * Implementation of {@link #filename(Object)} and {@link #extension(Object)} methods. + * + * @param path the path as an instance of the types listed in public methods, or {@code null}. + * @param exension {@code true} for requesting the extension instead of the file name. + * @return the filename or extension in the given path, or an empty string if none, or {@code null} + * if the given object is null or of unknown type. */ private static String part(final Object path, final boolean extension) { int fromIndex = 0; @@ -132,10 +149,12 @@ public final class IOUtilities extends Static { name = ((File) path).getName(); end = name.length(); } else if (path instanceof Path) { - name = ((Path) path).getFileName().toString(); + final Path tip = ((Path) path).getFileName(); + if (tip == null) return null; + name = tip.toString(); end = name.length(); } else { - char separator = '/'; + char separator = URI_PATH_SEPARATOR; if (path instanceof URL) { name = ((URL) path).getPath(); } else if (path instanceof URI) { @@ -155,8 +174,8 @@ public final class IOUtilities extends Static { end = name.length(); do { if (--end < 0) return ""; // `end` is temporarily inclusive in this loop. - fromIndex = name.lastIndexOf('/', end); - if (separator != '/') { + fromIndex = name.lastIndexOf(URI_PATH_SEPARATOR, end); + if (separator != URI_PATH_SEPARATOR) { // Search for platform-specific character only if the object is neither a URL or a URI. fromIndex = Math.max(fromIndex, name.lastIndexOf(separator, end)); } @@ -165,7 +184,7 @@ public final class IOUtilities extends Static { end++; // Make exclusive. } if (extension) { - fromIndex = CharSequences.lastIndexOf(name, '.', fromIndex, end) + 1; + fromIndex = CharSequences.lastIndexOf(name, EXTENSION_SEPARATOR, fromIndex, end) + 1; if (fromIndex <= 1) { // If the dot is the first character, do not consider as a filename extension. return ""; @@ -221,16 +240,19 @@ public final class IOUtilities extends Static { } /** - * Converts the given URI to a new URI with the same path except for the file extension, - * which is replaced by the given extension. This method is used for opening auxiliary files - * such as {@code "*.prj"} and {@code "*.tfw"} files that come with e.g. TIFF files. + * Converts the given URI to a new URI with the same path except for the filename or extension. + * This method is used for opening auxiliary files such as {@code "*.prj"} and {@code "*.tfw"} + * files that come with e.g. TIFF files. * - * @param location the URI to convert to a URL with a different extension, or {@code null}. - * @param extension the file extension (without {@code '.'}) of the auxiliary file. + * @param location the URI to convert to a URL with a different extension, or {@code null}. + * @param replacement the filename (including extension) or file extension (without {@code '.'}) of the auxiliary file. + * @param extension whether the replacement is the filename or only the file extension. * @return URI for the auxiliary file with the given extension, or {@code null} if none. * @throws URISyntaxException if the URI cannot be reconstructed. */ - public static URI toAuxiliaryURI(final URI location, final String extension) throws URISyntaxException { + public static URI toAuxiliaryURI(final URI location, final String replacement, final boolean extension) + throws URISyntaxException + { if (location == null || !location.isAbsolute() || location.isOpaque()) { return null; } @@ -242,11 +264,12 @@ public final class IOUtilities extends Static { s = path.length(); } } - s = path.lastIndexOf('.', s); - if (s >= 0) { - path = path.substring(0, s+1) + extension; + final int base = path.lastIndexOf(URI_PATH_SEPARATOR, s); + s = extension ? path.lastIndexOf(EXTENSION_SEPARATOR, s) : base; + if (!extension || s > base) { + path = path.substring(0, s+1) + replacement; } else { - path = path + '.' + extension; + path = path + EXTENSION_SEPARATOR + replacement; } return new URI(location.getScheme(), // http, https, file or jar. location.getRawAuthority(), // Host name or literal IP address. @@ -264,10 +287,10 @@ public final class IOUtilities extends Static { public static String filenameWithoutExtension(String path) { if (path != null) { int s = path.lastIndexOf(File.separatorChar); - if (s < 0 && File.separatorChar != '/') { - s = path.lastIndexOf('/'); + if (s < 0 && File.separatorChar != URI_PATH_SEPARATOR) { + s = path.lastIndexOf(URI_PATH_SEPARATOR); } - int e = path.lastIndexOf('.'); + int e = path.lastIndexOf(EXTENSION_SEPARATOR); if (e <= ++s) { e = path.length(); } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java index 44d069e4e1..116ff542cf 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java @@ -50,6 +50,11 @@ public final class DataOptionKey<T> extends OptionKey<T> { * If the file exists, it is parsed and its content is merged or appended after the * metadata read by the storage. If the file does not exist, then it is ignored. * + * <h4>Wildcard</h4> + * It the {@code '*'} character is present in the path, then it is replaced by the name of the + * main file without its extension. For example if the main file is {@code "city-center.tiff"}, + * then {@code "*.xml"} will become {@code "city-center.xml"}. + * * @since 1.5 */ public static final OptionKey<Path> METADATA_PATH = 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 5bd4edcebd..3249a57314 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 @@ -204,7 +204,7 @@ public abstract class PRJDataStore extends URIDataStore { stream = Files.newInputStream(path); source = path; } else try { - final URI uri = IOUtilities.toAuxiliaryURI(location, extension); + final URI uri = IOUtilities.toAuxiliaryURI(location, extension, true); if (uri == null) { return null; } @@ -437,8 +437,8 @@ public abstract class PRJDataStore extends URIDataStore { */ private static String getBaseFilename(final Path path) { final String base = path.getFileName().toString(); - final int s = base.lastIndexOf('.'); - return (s >= 0) ? base.substring(0, s+1) : base + '.'; + final int s = base.lastIndexOf(IOUtilities.EXTENSION_SEPARATOR); + return (s >= 0) ? base.substring(0, s+1) : base + IOUtilities.EXTENSION_SEPARATOR; } /** 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 df7f1141ae..856b34f6f8 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 @@ -16,6 +16,7 @@ */ package org.apache.sis.storage.base; +import java.util.Arrays; import java.util.Optional; import java.io.DataInput; import java.io.DataOutput; @@ -28,6 +29,7 @@ import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; import java.nio.charset.Charset; +import java.net.URISyntaxException; import jakarta.xml.bind.JAXBException; import org.opengis.util.GenericName; import org.opengis.parameter.ParameterValueGroup; @@ -75,6 +77,8 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R /** * Path to an auxiliary file providing metadata as path, or {@code null} if none or not applicable. * Unless absolute, this path is relative to the {@link #location} or to the {@link #locationAsPath}. + * The path may contain the {@code '*'} character, which need to be replaced by the main file name + * without suffix at reading time. */ private final Path metadataPath; @@ -134,31 +138,100 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R } /** - * Returns the path to the auxiliary metadata file, or {@code null} if none. - * This is a path build from the "metadata path" option if present. + * {@return the path to the auxiliary metadata file, or {@code null} if none}. + * This is a path built from the {@link DataOptionKey#METADATA_PATH} value if present. + * Note that the metadata may be unavailable as a {@link Path} but available as an {@link URI}. */ - private Path getMetadataPath() { - if (metadataPath != null && locationAsPath != null) { - Path path = locationAsPath.getParent(); + private Path getMetadataPath() throws IOException { + Path path = replaceWildcard(metadataPath); + if (path != null) { + Path parent = locationAsPath; + if (parent != null) { + parent = parent.getParent(); + if (parent != null) { + path = parent.resolve(path); + } + } + if (Files.isSameFile(path, locationAsPath)) { + return null; + } + } + return path; + } + + /** + * {@return the URI to the auxiliary metadata file, or {@code null} if none}. + * This is a path built from the {@link DataOptionKey#METADATA_PATH} value if present. + * Note that the metadata may be unavailable as an {@link URI} but available as a {@link Path}. + */ + private URI getMetadataURI() throws URISyntaxException { + URI uri = location; + if (uri != null) { + final Path path = replaceWildcard(metadataPath); if (path != null) { - return path.resolve(metadataPath); + uri = IOUtilities.toAuxiliaryURI(uri, path.toString(), false); + if (!uri.equals(location)) { + return uri; + } } } return null; } + /** + * Returns the given path with the wildcard character replaced by the name of the main file. + * + * @param path path in which to replace wildcard character, or {@code null}. + * @return path with wildcard character replaced, or {@code path} if no replacement was done, + * or {@code null} if a replacement was required but couldn't be done. + */ + private Path replaceWildcard(Path path) { + if (path != null) { + boolean changed = false; + String filename = null; // Determined when first needed. + final var names = new String[path.getNameCount()]; + int count = 0; + for (final Path p : path) { + String name = p.toString(); + if (name.indexOf('*') >= 0) { + if (filename == null) { + filename = IOUtilities.filename(locationAsPath != null ? locationAsPath : location); + if (filename == null) { + return null; + } + final int s = filename.lastIndexOf(IOUtilities.EXTENSION_SEPARATOR); + if (s >= 0) { + filename = filename.substring(0, s); + } + } + name = name.replace("*", filename); + changed = true; + } + names[count++] = name; + } + if (changed) { + path = path.getFileSystem().getPath(names[0], Arrays.copyOfRange(names, 1, count)); + } + } + return path; + } + /** * Returns the main and metadata locations as {@code Path} components, or an empty array if none. * The default implementation returns the storage specified at construction time converted to a {@link Path} * if such conversion was possible, or {@code null} otherwise. * * @return the URI as a path, or an empty array if unknown. - * @throws DataStoreException if the URI cannot be converted to a {@link Path}. + * @throws DataStoreException if an error occurred while getting the paths. */ @Override public Path[] getComponentFiles() throws DataStoreException { - final var paths = new Path[] {locationAsPath, getMetadataPath()}; - return ArraysExt.resize(paths, ArraysExt.removeDuplicated(paths, ArraysExt.removeNulls(paths))); + try { + final var paths = new Path[] {locationAsPath, getMetadataPath()}; + return ArraysExt.resize(paths, ArraysExt.removeDuplicated(paths, ArraysExt.removeNulls(paths))); + } catch (IOException e) { + throw new DataStoreException(e); + } } /** @@ -404,12 +477,28 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R * @param builder where to merge the metadata. */ protected final void mergeAuxiliaryMetadata(final MetadataBuilder builder) { - final Path path = getMetadataPath(); - if (path != null) try { - builder.mergeMetadata(XML.unmarshal(path), getLocale()); + Object metadata = null; + Exception error = null; + try { + final Path path = getMetadataPath(); + if (path != null) { + metadata = XML.unmarshal(path); + } else { + final URI uri = getMetadataURI(); + if (uri != null) { + metadata = XML.unmarshal(uri.toURL()); + } + } + } catch (URISyntaxException | IOException e) { + error = e; } catch (JAXBException e) { final Throwable cause = e.getCause(); - listeners.warning(cannotReadAuxiliaryFile("xml"), (cause instanceof IOException) ? (Exception) cause : e); + error = (cause instanceof IOException) ? (Exception) cause : e; + } + if (metadata != null) { + builder.mergeMetadata(metadata, getLocale()); + } else if (error != null) { + listeners.warning(cannotReadAuxiliaryFile("xml"), error); } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStoreProvider.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStoreProvider.java index f5b2a05a14..f46423d2dc 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStoreProvider.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStoreProvider.java @@ -28,6 +28,7 @@ import org.apache.sis.storage.DataStore; import org.apache.sis.storage.base.Capability; import org.apache.sis.storage.base.StoreMetadata; import org.apache.sis.storage.base.PRJDataStore; +import org.apache.sis.io.stream.IOUtilities; /** @@ -92,8 +93,8 @@ public final class RawRasterStoreProvider extends PRJDataStore.Provider { Path path = connector.getStorageAs(Path.class); if (path != null) { String filename = path.getFileName().toString(); - final int s = filename.lastIndexOf('.'); - filename = ((s >= 0) ? filename.substring(0, s+1) : filename.concat(".")).concat(HDR); + final int s = filename.lastIndexOf(IOUtilities.EXTENSION_SEPARATOR); + filename = ((s >= 0) ? filename.substring(0, s+1) : filename + IOUtilities.EXTENSION_SEPARATOR) + HDR; path = path.resolveSibling(filename); if (Files.isRegularFile(path)) { // TODO: maybe we should do more tests here (open the file?) 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 1f9f65e780..d89e0b6641 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 @@ -37,6 +37,7 @@ 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; import org.apache.sis.util.ArgumentChecks; @@ -99,7 +100,7 @@ final class WritableStore extends Store implements WritableAggregate { String filename = identifier.toString(); final String[] suffixes = StoreUtilities.getFileSuffixes(componentProvider.getClass()); if (suffixes.length != 0) { - filename += '.' + suffixes[0]; + filename += IOUtilities.EXTENSION_SEPARATOR + suffixes[0]; } /* * Create new store/resource for write access, provided that no store already exist for the path. 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 5293f2c541..d6014cee80 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 @@ -38,6 +38,7 @@ import org.opengis.metadata.maintenance.ScopeCode; import org.opengis.referencing.datum.PixelInCell; import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.io.stream.IOUtilities; import org.apache.sis.storage.Resource; import org.apache.sis.storage.Aggregate; import org.apache.sis.storage.StorageConnector; @@ -50,10 +51,10 @@ import org.apache.sis.storage.UnsupportedStorageException; import org.apache.sis.storage.base.PRJDataStore; import org.apache.sis.storage.base.MetadataBuilder; import org.apache.sis.referencing.util.j2d.AffineTransform2D; +import org.apache.sis.metadata.sql.MetadataStoreException; import org.apache.sis.util.CharSequences; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.internal.ListOfUnknownSize; -import org.apache.sis.metadata.sql.MetadataStoreException; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.resources.Errors; import org.apache.sis.setup.OptionKey; @@ -397,7 +398,7 @@ loop: for (int convention=0;; convention++) { throw new EOFException(errors().getString(Errors.Keys.UnexpectedEndOfFile_1, filename)); } if (filename != null) { - final int s = filename.lastIndexOf('.'); + final int s = filename.lastIndexOf(IOUtilities.EXTENSION_SEPARATOR); if (s >= 0) { suffixWLD = filename.substring(s+1); } diff --git a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/IOUtilitiesTest.java b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/IOUtilitiesTest.java index 44b759565a..e1ebfc728b 100644 --- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/IOUtilitiesTest.java +++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/IOUtilitiesTest.java @@ -26,7 +26,7 @@ import org.apache.sis.util.CharSequences; // Test dependencies import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; import org.apache.sis.test.DependsOnMethod; import org.apache.sis.test.TestCase; @@ -122,15 +122,16 @@ public final class IOUtilitiesTest extends TestCase { } /** - * Tests {@link IOUtilities#toAuxiliaryURI(URI, int)}. + * Tests {@link IOUtilities#toAuxiliaryURI(URI, String, boolean)}. * * @throws URISyntaxException if a URI cannot be parsed. * @throws MalformedURLException if a URL cannot be parsed. */ @Test public void testAuxiliaryURI() throws URISyntaxException, MalformedURLException { - assertEquals(new URI("http://localhost/directory/image.tfw"), - IOUtilities.toAuxiliaryURI(new URI("http://localhost/directory/image.tiff"), "tfw")); + final var src = new URI("http://localhost/directory/image.tiff?request=ignore.me"); + assertEquals(new URI("http://localhost/directory/image.tfw"), IOUtilities.toAuxiliaryURI(src, "tfw", true)); + assertEquals(new URI("http://localhost/directory/metadata.xml"), IOUtilities.toAuxiliaryURI(src, "metadata.xml", false)); } /** @@ -210,11 +211,11 @@ public final class IOUtilitiesTest extends TestCase { * @throws IOException if a URL cannot be parsed. */ private void testToFile(final String encoding, final String plus) throws IOException { - assertEquals("Unix absolute path.", new File("/Users/name/Map.png"), + assertEquals(new File("/Users/name/Map.png"), // Unix absolute path. IOUtilities.toFile(IOUtilities.toURL("file:/Users/name/Map.png", encoding))); - assertEquals("Path with space.", new File("/Users/name/Map with spaces.png"), + assertEquals(new File("/Users/name/Map with spaces.png"), // Path with space. IOUtilities.toFile(IOUtilities.toURL("file:/Users/name/Map with spaces.png", encoding))); - assertEquals("Path with + sign.", new File("/Users/name/++t--++est.shp"), + assertEquals(new File("/Users/name/++t--++est.shp"), // Path with + sign. IOUtilities.toFile(IOUtilities.toURL( CharSequences.replace("file:/Users/name/++t--++est.shp", "+", plus).toString(), encoding))); }