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 1c3e7cf558a4a5734afca80b33771ccc3e90caa2 Merge: dc78d5d1ab 51c7ed2535 Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri May 29 23:26:35 2026 +0200 Merge (with refactoring) branch 'feat/CustomHostS3' into geoapi-4.0. https://github.com/apache/sis/pull/40 .../org.apache.sis.cloud.aws/main/module-info.java | 5 +- .../apache/sis/cloud/aws/s3/ClientFileSystem.java | 75 +++-- .../org/apache/sis/cloud/aws/s3/FileService.java | 311 +++++++++++++++------ .../main/org/apache/sis/cloud/aws/s3/KeyPath.java | 48 +++- .../org/apache/sis/cloud/aws/s3/Resources.java | 22 +- .../apache/sis/cloud/aws/s3/Resources.properties | 6 +- .../sis/cloud/aws/s3/Resources_fr.properties | 8 +- .../main/org/apache/sis/cloud/aws/s3/Server.java | 230 +++++++++++++++ .../org/apache/sis/cloud/aws/s3/package-info.java | 7 +- .../sis/cloud/aws/s3/ClientFileSystemTest.java | 35 ++- .../apache/sis/cloud/aws/s3/FileServiceTest.java | 159 +++++++++++ .../sis/cloud/aws/s3/KeyPathMatcherTest.java | 10 +- .../org/apache/sis/cloud/aws/s3/KeyPathTest.java | 126 +++++++-- 13 files changed, 882 insertions(+), 160 deletions(-) diff --cc endorsed/src/org.apache.sis.cloud.aws/main/module-info.java index 5ab60c1c7a,7fc1f857a0..5c152d2a12 --- a/endorsed/src/org.apache.sis.cloud.aws/main/module-info.java +++ b/endorsed/src/org.apache.sis.cloud.aws/main/module-info.java @@@ -16,10 -16,11 +16,11 @@@ */ /** -- * Java NIO wrappers for Amazon Simple Storage Service (S3). ++ * Java <abbr>NIO</abbr> wrappers for Amazon Simple Storage Service (S3). * * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @author Quentin Bialota (Geomatys) + * @version 1.7 * @since 1.2 */ module org.apache.sis.cloud.aws { diff --cc endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/ClientFileSystem.java index b8aa19d3eb,73d0468c40..ea90b1d19b --- a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/ClientFileSystem.java +++ b/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/ClientFileSystem.java @@@ -20,6 -22,6 +20,7 @@@ import java.util.Set import java.util.Collections; import java.util.regex.PatternSyntaxException; import java.io.IOException; ++import java.net.URISyntaxException; import java.nio.file.FileStore; import java.nio.file.FileSystem; import java.nio.file.Path; @@@ -54,19 -58,35 +57,18 @@@ final class ClientFileSystem extends Fi static final String DEFAULT_SEPARATOR = "/"; /** -- * The AWS S3 access key, or {@code null} if none. -- * Also used as key of this file system in the {@link FileService#fileSystems} map. - */ - final String accessKey; - - /** - * The S3 host (if not stored on Amazon Infrastructure), or {@code null} if none. - */ - final String host; - - /** - * The S3 port (if not stored on Amazon Infrastructure), or {@code -1} if none. - */ - final int port; - - /** - * Whether the S3 HTTP Protocol is secure (if this information is not stored on Amazon Infrastructure). - * Default is {@code true}. ++ * The provider of this file system. */ - final String accessKey; - final boolean isHttps; ++ private final FileService provider; /** -- * The provider of this file system. ++ * Information about the server: protocol, host, port, access key. */ -- private final FileService provider; ++ final Server server; /** * The service client for accessing Amazon S3, or {@code null} if this file system is closed. -- * Note that AWS SDK objects are thread-safe. ++ * Note that <abbr>AWS</abbr> <abbr>SDK</abbr> objects are thread-safe. */ private volatile S3Client client; @@@ -82,12 -102,15 +84,14 @@@ final String duplicatedSeparator; /** - * Creates a file system with default hostname and default separator. + * Creates a file system with default credential and default separator. ++ * This constructor assumes that the given {@code server} argument has no host and no port. ++ * That argument should have been created with {@link Server#Server(String)} constructor. */ - ClientFileSystem(final FileService provider, final S3Client client) { - ClientFileSystem(final FileService provider, final S3Client client, String accessKey) { ++ ClientFileSystem(final FileService provider, final Server server) { this.provider = provider; -- this.client = client; - this.accessKey = null; - this.accessKey = accessKey; - this.host = null; - this.port = -1; - this.isHttps = true; ++ this.server = server; ++ this.client = S3Client.create(); this.separator = DEFAULT_SEPARATOR; duplicatedSeparator = DEFAULT_SEPARATOR + DEFAULT_SEPARATOR; } @@@ -95,29 -118,44 +99,52 @@@ /** * Creates a new file system with the specified credential. * -- * @param provider the provider creating this file system. -- * @param region the AWS region, or {@code null} for default. - * @param host the host or {@code null} for AWS request. - * @param port the port or {@code -1} for AWS request. - * @param isHttps the protocol is secure or not or {@code null} for AWS request. -- * @param accessKey the AWS S3 access key for this file system. -- * @param secret the password. -- * @param separator the separator in paths, or {@code null} for the default value. ++ * @param provider the provider creating this file system. ++ * @param region the AWS region, or {@code null} for default. ++ * @param server the host, port, access key and whether the protocol is secure. ++ * @param secret the password, or {@code null} if none. ++ * @param separator the separator in paths, or {@code null} for the default value. ++ * @throws URISyntaxException if the server is self-hosted by the <abbr>URI</abbr> is invalid. */ - ClientFileSystem(final FileService provider, final Region region, final String accessKey, final String secret, - String separator) - ClientFileSystem(final FileService provider, final Region region, final String host, final int port, - final boolean isHttps, final String accessKey, final String secret, - String separator) throws URISyntaxException ++ ClientFileSystem(final FileService provider, final Region region, final Server server, ++ final String secret, String separator) throws URISyntaxException { ++ // Verify argument validity before to start building the S3 client. ++ final boolean selfHosted = (server.host != null); ++ if (selfHosted != server.port >= 0) { ++ final short msg; ++ final Object arg; ++ if (selfHosted) { ++ msg = Resources.Keys.MissingPortNumber_1; ++ arg = server.host; ++ } else { ++ msg = Resources.Keys.MissingHostName_1; ++ arg = server.port; ++ } ++ throw new IllegalStateException(Resources.format(msg, arg)); ++ } if (separator == null) { separator = DEFAULT_SEPARATOR; ++ } else { ++ ArgumentChecks.ensureNonEmpty("separator", separator); } -- ArgumentChecks.ensureNonEmpty("separator", separator); this.provider = provider; -- this.accessKey = accessKey; - S3ClientBuilder builder = S3Client.builder().credentialsProvider( - StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secret))); ++ this.server = server; ++ this.separator = separator; ++ duplicatedSeparator = separator.concat(separator); + S3ClientBuilder builder = S3Client.builder(); + if (secret != null) { - builder = builder.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secret))); ++ AwsBasicCredentials credential = AwsBasicCredentials.create(server.accessKey, secret); ++ builder = builder.credentialsProvider(StaticCredentialsProvider.create(credential)); + } if (region != null) { builder = builder.region(region); } - this.host = host; - this.port = port; - this.isHttps = isHttps; - if (host != null) { - String hostname = (port < 0 ? host + ':' + port : host); - String protocol = (this.isHttps ? "https" : "http"); - builder = builder.endpointOverride(new URI(protocol + "://" + hostname)) - .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build()); ++ if (selfHosted) { ++ S3Configuration.Builder config = S3Configuration.builder().pathStyleAccessEnabled(true); ++ builder = builder.endpointOverride(server.toURI()).serviceConfiguration(config.build()); + } client = builder.build(); -- this.separator = separator; -- duplicatedSeparator = separator.concat(separator); } /** @@@ -148,7 -186,7 +175,7 @@@ final S3Client c = client; client = null; if (c != null) try { - provider.dispose(accessKey); - provider.dispose(new ClientFileSystemKey(accessKey, host, port, isHttps)); ++ provider.dispose(server); c.close(); } catch (SdkException e) { throw new IOException(e); @@@ -265,6 -303,6 +292,6 @@@ */ @Override public String toString() { -- return Strings.toString(getClass(), "accessKey", accessKey); ++ return Strings.toString(getClass(), "server", server); } } diff --cc endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/FileService.java index 635dc54555,126abcb048..a31734f1e3 --- a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/FileService.java +++ b/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/FileService.java @@@ -22,6 -23,6 +22,8 @@@ import java.util.function.Function import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentHashMap; import java.net.URI; ++import java.net.URISyntaxException; ++import java.net.MalformedURLException; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; @@@ -47,7 -48,7 +49,6 @@@ import java.nio.channels.SeekableByteCh import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.exception.SdkException; --import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.Bucket; import software.amazon.awssdk.services.s3.model.S3Object; import software.amazon.awssdk.services.s3.model.GetObjectRequest; @@@ -57,6 -58,6 +58,7 @@@ import org.apache.sis.util.CharSequence import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.collection.Containers; ++import org.apache.sis.util.internal.shared.Strings; /** @@@ -80,48 -84,112 +85,73 @@@ */ public class FileService extends FileSystemProvider { /** -- * An arbitrary string used as key in the {@link #fileSystems} map -- * when the user did not specify explicitly an access key. -- * In such case, the default mechanism documented in AWS SDK is used. -- * In preference order: -- * -- * <ul> -- * <li>{@code aws.accessKeyId} and {@code aws.secretAccessKey} Java system properties.</li> -- * <li>{@code AWS_ACCESS_KEY_ID} and {@code AWS_SECRET_ACCESS_KEY} environment variables.</li> -- * <li>{@code ~/.aws/credentials} or {@code ~/.aws/config} files.</li> -- * </ul> ++ * The default port number for the <abbr>HTTP</abbr> protocol. + */ - private static final String DEFAULT_ACCESS_KEY = ""; ++ static final int HTTP_PORT = 80; + + /** - * An arbitrary string used as part of the key in the {@link #fileSystems} map - * when the user did not specified explicitly a host. - * In such case, the default host is the Amazon host and is defined with the region. - * - * Host can also be defined with: - * <ul> - * <li>{@code #S3_HOST_URL} Java system properties.</li> - * </ul> ++ * The default port number for the <abbr>HTTPS</abbr> protocol. */ - private static final String DEFAULT_ACCESS_KEY = ""; - private static final String DEFAULT_HOST_KEY = null; ++ static final int HTTPS_PORT = 443; /** - * An arbitrary string used as part of the key in the {@link #fileSystems} map - * when the user did not specified explicitly a port. - * In such case, no port is assigned, the default port is used + * The property for the secret access key (password). + * Values shall be instances of {@link String}. + * If not specified, the AWS SDK default mechanism searches for the first of the following: * - * Port can also be defined with : * <ul> - * <li>{@code #S3_PORT} Java system properties.</li> + * <li>{@code AWS_SECRET_ACCESS_KEY} environment variable.</li> + * <li>{@code ~/.aws/credentials} and {@code ~/.aws/config} files.</li> * </ul> - */ - private static final int DEFAULT_PORT_KEY = -1; - - /** - * A boolean used as part of the key in the {@link #fileSystems} map - * when the user did not specified explicitly a protocol. - * In such case, the default protocol is HTTPS * - * Port can also be defined with : - * <ul> - * <li>{@code #S3_IS_HTTPS} Java system properties.</li> - * </ul> + * @see #newFileSystem(URI, Map) */ - private static final boolean DEFAULT_IS_HTTPS = true; + public static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey"; /** -- * The property for the secret access key (password). - * Values shall be instances of {@link String}. - * If not specified, the AWS SDK default mechanism searches for the first of the following: ++ * The property for the region. + * Values shall should be instances of {@link Region} or + * strings {@linkplain Region#of(String) convertible} to region. - * If not specified, the AWS SDK default mechanism searches for the first of the following: ++ * If not specified, the <abbr>AWS</abbr> <abbr>SDK</abbr> default ++ * mechanism searches for the first of the following: * * <ul> - * <li>{@code AWS_SECRET_ACCESS_KEY} environment variable.</li> + * <li>{@code AWS_REGION} environment variable.</li> * <li>{@code ~/.aws/credentials} and {@code ~/.aws/config} files.</li> * </ul> * * @see #newFileSystem(URI, Map) */ - public static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey"; + public static final String AWS_REGION = "aws.region"; + /** - * The property for the host (mandatory if not using AWS S3). ++ * The property for the host (mandatory if not using <abbr>AWS</abbr> S3). + * Values shall be instances of {@link String}. + * + * @see #newFileSystem(URI, Map) + * @since 1.7 + */ - public static final String S3_HOST_URL = "org.apache.sis.s3.hostURL"; ++ public static final String HOST_URL = "hostURL"; + + /** - * The property for the port (mandatory if not using AWS S3). ++ * The property for the port (mandatory if not using <abbr>AWS</abbr> S3). + * Values shall be instances of {@link Integer}. + * + * @see #newFileSystem(URI, Map) + * @since 1.7 + */ - public static final String S3_PORT = "org.apache.sis.s3.port"; ++ public static final String PORT = "port"; + + /** + * The property for the protocol (optional). + * Values shall be instances of {@link Boolean}. - * Default value : True (HTTPS) ++ * The default value is {@code true} (<abbr>HTTPS</abbr>). + * + * @see #newFileSystem(URI, Map) + * @since 1.7 + */ - public static final String S3_IS_HTTPS = "org.apache.sis.s3.isHttps"; - - /** - * The property for the secret access key (password). - * Values shall should be instances of {@link Region} or - * strings {@linkplain Region#of(String) convertible} to region. - * If not specified, the AWS SDK default mechanism searches for the first of the following: - * - * <ul> - * <li>{@code AWS_REGION} environment variable.</li> - * <li>{@code ~/.aws/credentials} and {@code ~/.aws/config} files.</li> - * </ul> - * - * @see #newFileSystem(URI, Map) - */ - public static final String AWS_REGION = "aws.region"; ++ public static final String IS_HTTPS = "isHttps"; + /** * The property for the name-separator characters. * The default value is "/" for simulating Unix paths. @@@ -132,9 -200,9 +162,9 @@@ public static final String SEPARATOR = "separator"; /** -- * All file systems created by this provider. Keys are AWS S3 access keys. ++ * All file systems created by this provider. Keys are <abbr>AWS</abbr> S3 access keys. */ - private final ConcurrentMap<String, ClientFileSystem> fileSystems; - private final ConcurrentMap<ClientFileSystemKey, ClientFileSystem> fileSystems; ++ private final ConcurrentMap<Server, ClientFileSystem> fileSystems; /** * Creates a new provider of file systems for Amazon S3. @@@ -144,9 -212,9 +174,9 @@@ } /** -- * Returns the URI scheme that identifies this provider, which is {@code "S3"}. ++ * Returns the <abbr>URI</abbr> scheme that identifies this provider, which is {@code "S3"}. * -- * @return the {@code "S3"} URI scheme. ++ * @return the {@code "S3"} <abbr>URI</abbr> scheme. */ @Override public String getScheme() { @@@ -191,12 -259,13 +221,19 @@@ * <li>{@value #AWS_SECRET_ACCESS_KEY} with {@link String} value.</li> * <li>{@value #AWS_REGION} with {@link Region} value or a string * {@linkplain Region#of(String) convertible} to region.</li> ++ * <li>{@value #HOST_URL} with {@link String} value.</li> ++ * <li>{@value #PORT} with {@link Integer} value.</li> ++ * <li>{@value #IS_HTTPS} with {@link Boolean} value.</li> * </ul> * - * @param uri a <abbr>URI</abbr> of the form {@code "s3://accessKey@bucket/file"}. - * @param uri a <abbr>URI</abbr> of the form {@code "s3://accessKey@bucket/file"} - * or {@code s3://accessKey@host:port/bucket/file} (in this second case, host AND port are mandatory). ++ * <p>The <abbr>URI</abbr> can be of the form {@code "s3://accessKey@bucket/file"} (<abbr>AWS</abbr> S3) ++ * or {@code "s3://accessKey@host:port/bucket/file"} (self-hosted S3). ++ * In the latter case, the host <em>and</em> the port are mandatory.</p> ++ * ++ * @param uri an <abbr>URI</abbr> of the form {@code "s3://accessKey@[host:port/]bucket/file"}. * @param properties properties to configure the file system, or {@code null} if none. * @return the new file system. -- * @throws IllegalArgumentException if the URI or the map contains invalid values. ++ * @throws IllegalArgumentException if the <abbr>URI</abbr> or the map contains invalid values. * @throws IOException if an I/O error occurs while creating the file system. * @throws FileSystemAlreadyExistsException if a file system has already been created * for the given URI and has not yet been closed. @@@ -208,72 -279,205 +245,182 @@@ if (accessKey == null || (secret = Containers.property(properties, AWS_SECRET_ACCESS_KEY, String.class)) == null) { throw new IllegalArgumentException(Resources.format(Resources.Keys.MissingAccessKey_2, (accessKey == null) ? 0 : 1, uri)); } -- final String separator = Containers.property(properties, SEPARATOR, String.class); -- final Region region; -- Object value = properties.get(AWS_REGION); -- if (value instanceof String) { -- region = Region.of((String) value); -- } else { -- region = Containers.property(properties, AWS_REGION, Region.class); -- } - final class Creator implements Function<String, ClientFileSystem> { - - final class Creator implements Function<ClientFileSystemKey, ClientFileSystem> { -- /** Identifies if a new file system is created. */ boolean created; - /** Store URI Syntax exception. */ URISyntaxException exception; ++ final String separator = Containers.property(properties, SEPARATOR, String.class); ++ final Region region = property(properties, AWS_REGION, Region.class, null, Region::of); ++ /* ++ * Host and port number specified in the property map have precedence over the URI. ++ * If these information were not found anywhere, the AWS SDK will fetch them itself. ++ */ ++ final var server = new Server(accessKey, ++ Containers.property(properties, HOST_URL, String.class), ++ property(properties, PORT, Integer.class, Server.NO_PORT, Integer::valueOf), ++ property(properties, IS_HTTPS, Boolean.class, Server.DEFAULT_IS_HTTPS, Strings::parseBoolean), ++ uri); ++ /* ++ * The following class is for checking if a `ClientFileSystem` exists before to create one, ++ * in one atomic concurrent hash map operation. The standard `Map.computeIfAbsent(…)` method ++ * does not tell us whether the returned value was the existing one or a new one. ++ * We need a flag for differentiating the two cases. ++ */ ++ final class Creator implements Function<Server, ClientFileSystem> { ++ /** Whether this function has been invoked. */ boolean invoked; ++ /** If the operation failed, the reason why. */ URISyntaxException exception; - /** Invoked if the map does not already contains the file system. */ - @Override public ClientFileSystem apply(final String key) { - created = true; - return new ClientFileSystem(FileService.this, region, key, secret, separator); + @Override - public ClientFileSystem apply(final ClientFileSystemKey key) { - created = true; ++ public ClientFileSystem apply(final Server key) { ++ invoked = true; + try { - return new ClientFileSystem(FileService.this, region, key.host, key.port, key.isHttps, key.accessKey, secret, separator); - } catch (URISyntaxException ex) { - created = false; - exception = ex; - return null; // Nothing added to the map ++ return new ClientFileSystem(FileService.this, region, key, secret, separator); ++ } catch (URISyntaxException e) { ++ exception = e; ++ return null; + } } } - - Boolean isHttps; - if ((isHttps = Containers.property(properties, S3_IS_HTTPS, Boolean.class)) == null) { - isHttps = DEFAULT_IS_HTTPS; - } - - /* - * In case of Self-Hosted S3, if host and port are not found in the URI - * We check in java properties - * Else we use Default values (=> use AWS S3) - */ - if (port < 0) { - if ((host = Containers.property(properties, S3_HOST_URL, String.class)) != null) { - Integer portProp = Containers.property(properties, S3_PORT, Integer.class); - // In case of Self-Hosted S3, if port is not found in the URI, but a host is defined - if (portProp == null || portProp < 0) { - if (isHttps) { - port = 443; // Default HTTPS port - } else { - port = 80; // Default HTTP port - } - } else { - port = portProp; - } - - } else { - host = DEFAULT_HOST_KEY; - port = DEFAULT_PORT_KEY; - } - } - -- final Creator c = new Creator(); - final ClientFileSystem fs = fileSystems.computeIfAbsent(accessKey, c); - final ClientFileSystem fs = fileSystems.computeIfAbsent(new ClientFileSystemKey(accessKey, host, port, isHttps), c); -- if (c.created) { ++ final var c = new Creator(); ++ final ClientFileSystem fs = fileSystems.computeIfAbsent(server, c); ++ if (c.exception != null) { ++ final var e = new MalformedURLException(Resources.format(Resources.Keys.CannotConnectTo_1, server)); ++ e.initCause(c.exception); ++ throw e; ++ } else if (c.invoked) { return fs; - } else if (c.exception != null) { - throw new IllegalArgumentException("Invalid URI: " + uri, c.exception); } throw new FileSystemAlreadyExistsException(Resources.format(Resources.Keys.FileSystemInitialized_2, 1, accessKey)); } /** -- * Removes the given file system from the cache. -- * This method is invoked after the file system has been closed. - */ - final void dispose(ClientFileSystemKey identifier) { - if (identifier == null) { - identifier = new ClientFileSystemKey(DEFAULT_ACCESS_KEY, DEFAULT_HOST_KEY, DEFAULT_PORT_KEY, DEFAULT_IS_HTTPS); - } - fileSystems.remove(identifier); - } - - /** - * Returns the file system associated to the {@link #DEFAULT_ACCESS_KEY}, {@link #DEFAULT_HOST_KEY}, {@link #DEFAULT_PORT_KEY} and {@link #DEFAULT_IS_HTTPS}. - * - * @throws SdkException if the file system cannot be created. - */ - private ClientFileSystem getDefaultFileSystem() { - return fileSystems.computeIfAbsent(new ClientFileSystemKey(DEFAULT_ACCESS_KEY, DEFAULT_HOST_KEY, DEFAULT_PORT_KEY, DEFAULT_IS_HTTPS), (key) -> - new ClientFileSystem(this, S3Client.create(), null)); - } - - /** - * Returns the file system associated to the {@link #DEFAULT_HOST_KEY} and {@link #DEFAULT_PORT_KEY}. - * - * @param accessKey the access key - * @throws SdkException if the file system cannot be created. - */ - private ClientFileSystem getDefaultFileSystem(String accessKey) { - return fileSystems.computeIfAbsent( - new ClientFileSystemKey(accessKey, DEFAULT_HOST_KEY, DEFAULT_PORT_KEY, DEFAULT_IS_HTTPS), - (key) -> - new ClientFileSystem(this, S3Client.create(), key.accessKey)); - } - - /** - * Returns the file system associated to the {@link #DEFAULT_ACCESS_KEY}. ++ * Returns a value from the map specified in the constructor. ++ * The value can be parsed from a string representation. + * - * @param host the host - * @param port the port - * @throws SdkException if the file system cannot be created. - * @throws URISyntaxException if the URI is not valid. ++ * @param <T> compile-time value of the {@code type} argument. ++ * @param properties map from which to get a property value. ++ * @param key key of the property to get. ++ * @param type type of the property to get. ++ * @param defaultValue default value if the key is not associated to a non-null value. ++ * @param parser function to invoke for converting a text to a value. ++ * @return the property value for the given key cast to the given type, or {@code defaultValue} if none. ++ * @throws IllegalArgumentException if the value is not of the expected type. */ - final void dispose(String identifier) { - if (identifier == null) { - identifier = DEFAULT_ACCESS_KEY; - private ClientFileSystem getDefaultFileSystem(String host, Integer port) throws URISyntaxException { - ClientFileSystemKey key = new ClientFileSystemKey(DEFAULT_ACCESS_KEY, host, port, DEFAULT_IS_HTTPS); - - synchronized (fileSystems) { - ClientFileSystem fs = fileSystems.get(key); - if (fs != null) return fs; - fs = new ClientFileSystem(this, null, key.host, key.port, key.isHttps, key.accessKey, null, null); - fileSystems.put(key, fs); - return fs; ++ private static <T> T property(final Map<String,?> properties, final String key, ++ final Class<T> type, final T defaultValue, ++ final Function<String, T> parser) ++ { ++ final Object value = properties.get(key); ++ if (value == null) { ++ return defaultValue; ++ } else if (value instanceof CharSequence) { ++ T c = parser.apply((String) value); ++ if (c != null) return c; ++ } ++ try { ++ return type.cast(value); ++ } catch (ClassCastException e) { ++ throw new IllegalArgumentException(Errors.forProperties(properties) ++ .getString(Errors.Keys.IllegalPropertyValueClass_3, key, type, value.getClass()), e); } - fileSystems.remove(identifier); } /** -- * Returns the file system associated to the {@link #DEFAULT_ACCESS_KEY}. -- * - * @param host the host - * @param port the port - * @param isHttps the protocol -- * @throws SdkException if the file system cannot be created. - * @throws URISyntaxException if the URI is not valid. ++ * Removes the given file system from the cache. ++ * This method is invoked after the file system has been closed. */ - private ClientFileSystem getDefaultFileSystem() { - return fileSystems.computeIfAbsent(DEFAULT_ACCESS_KEY, (key) -> new ClientFileSystem(this, S3Client.create())); - private ClientFileSystem getDefaultFileSystem(String host, Integer port, boolean isHttps) throws URISyntaxException { - ClientFileSystemKey key = new ClientFileSystemKey(DEFAULT_ACCESS_KEY, host, port, isHttps); - - // 1. Try to get the existing one first (no locking) - synchronized (fileSystems) { - ClientFileSystem fs = fileSystems.get(key); - if (fs != null) return fs; - fs = new ClientFileSystem(this, null, key.host, key.port, key.isHttps, key.accessKey, null, null); - fileSystems.put(key, fs); - return fs; - } ++ final void dispose(final Server server) { ++ fileSystems.remove(server); } /** * Returns a reference to a file system that was created by the {@link #newFileSystem(URI, Map)} method. * If the file system has not been created or has been closed, * then this method throws {@link FileSystemNotFoundException}. - * If the given URI contains a port number, HTTP(S) protocol is determined by the port number (port 80 means HTTP, port 443 mean HTTPS). - * If another port is used we use the default protocol (HTTPS). - * A first attempt is made with the protocol determined from the port number, then a second attempt is made with the opposite protocol. * - * @param uri a <abbr>URI</abbr> of the form {@code "s3://accessKey@bucket/file"}. - * @param uri a <abbr>URI</abbr> of the form {@code "s3://accessKey@bucket/file"} or {@code "s3://accessKey@host:port/bucket/key"}. ++ * <p>The <abbr>URI</abbr> scheme should be the value returned by {@link #getScheme()}, which is usually {@code "S3"}. ++ * The <abbr>AWS</abbr> S3 service may be implemented on top of <abbr>HTTP</abbr> or <abbr>HTTPS</abbr> protocol. ++ * This method detects automatically which protocol was specified in the call to {@link #newFileSystem(URI, Map)}. ++ * If the two protocols have been used for the same host, then this method uses the port number for disambiguation: ++ * port 80 is mapped to <abbr>HTTP</abbr> and port 443 is mapped to <abbr>HTTPS</abbr>.</p> ++ * ++ * @param uri an <abbr>URI</abbr> of the form {@code "s3://accessKey@bucket/file"} or {@code "s3://accessKey@host:port/bucket/key"}. * @return the file system previously created by {@link #newFileSystem(URI, Map)}. -- * @throws IllegalArgumentException if the URI is not supported by this provider. ++ * @throws IllegalArgumentException if the <abbr>URI</abbr> is not supported by this provider. * @throws FileSystemNotFoundException if the file system does not exist or has been closed. */ @Override public FileSystem getFileSystem(final URI uri) { - final String accessKey = getAccessKey(uri); - final String host = uri.getHost(); - final int port = uri.getPort(); ++ return getFileSystem(uri, false); ++ } + ++ /** ++ * Implementation of {@link #getFileSystem(URI)} with the option of creating the file system instead ++ * of throwing an exception. ++ * ++ * @param uri an <abbr>URI</abbr> of the form {@code "s3://accessKey@bucket/file"} or {@code "s3://accessKey@host:port/bucket/key"}. ++ * @param create {@code true} for creating the file system if it does not already exist. ++ * @return the file system previously created by {@link #newFileSystem(URI, Map)}. ++ * @throws FileSystemNotFoundException if the file system does not exist and {@code create} is {@code false}. ++ */ ++ private ClientFileSystem getFileSystem(final URI uri, final boolean create) { + final String accessKey = getAccessKey(uri); - if (accessKey == null) { - return getDefaultFileSystem(); ++ boolean isHttps = Server.DEFAULT_IS_HTTPS; ++ int port = uri.getPort(); ++ final String host = (port >= 0) ? uri.getHost() : null; ++ switch (port) { ++ case HTTP_PORT: isHttps = false; break; ++ case HTTPS_PORT: isHttps = true; break; + } - final ClientFileSystem fs = fileSystems.get(accessKey); - if (fs != null) { - return fs; + /* - * HTTPS or HTTP is not defined in the URI. By default, we use default value, - * except if the port is 80 or 443, in which case we use HTTP or HTTPS. ++ * Try the following combinations, in order ++ * (the logic is to give precedence to explicit parameters, then to security): ++ * ++ * - specified port (may be -1), isHttps ++ * - specified port (may be -1), !isHttps ++ * - default port for `isHttps`, isHttps ++ * - default port for `!isHttps`, !isHttps ++ * - default port for `!isHttps`, isHttps (unusual, possibly a user's error. ++ * - default port for `isHttps`, !isHttps (unusual, possibly a user's error. + */ - boolean isHttps = DEFAULT_IS_HTTPS; - if (port == 80) { - isHttps = false; - } else if (port == 443) { - isHttps = true; - } - - if (accessKey == null && port > -1) { - // No access key, but host and port are defined => Self Hosted - try { - return getDefaultFileSystem(host, port, isHttps); - } catch (URISyntaxException ex) { - throw new IllegalArgumentException("Invalid URI: " + uri, ex); ++ boolean tryDefaultPorts = false; ++ boolean tryOppositePorts = false; ++ Server first = null; ++ for (;;) { ++ if (tryDefaultPorts) { ++ port = (tryOppositePorts ^ isHttps) ? HTTPS_PORT : HTTP_PORT; ++ } ++ final Server server = new Server(accessKey, host, port, isHttps, uri); ++ final ClientFileSystem fs = fileSystems.get(server); ++ if (fs != null) { ++ return fs; ++ } ++ if (first == null) { ++ first = server; // Will be used for the error message if we cannot find a file system. ++ } ++ isHttps = !isHttps; ++ if (isHttps == first.isHttps) { ++ if (!tryDefaultPorts) { ++ if (port >= 0) break; // Do not try default ports if a port was explicitly specified. ++ tryDefaultPorts = true; ++ } else if (!tryOppositePorts) { ++ tryOppositePorts = true; ++ } else { ++ break; // We tried all combinations. ++ } + } - } else if (accessKey == null && port < 0) { - // No access key, no host, no port => AWS S3 - return getDefaultFileSystem(); - } else if (accessKey != null && port < 0) { - // Access key, no host, no port => AWS S3 - return getDefaultFileSystem(accessKey); + } - + /* - * Try to get the file system with the protocol determined from the port number, then try with the opposite protocol. ++ * No existing file system found. Create if we are allowed to do so. + */ - ClientFileSystem fs = fileSystems.get(new ClientFileSystemKey(accessKey, host, port, isHttps)); - if (fs != null) { - return fs; - } - fs = fileSystems.get(new ClientFileSystemKey(accessKey, host, port, !isHttps)); - if (fs != null) { - return fs; ++ if (create) { ++ return fileSystems.computeIfAbsent(first, (key) -> { ++ try { ++ return new ClientFileSystem(this, null, key, null, null); ++ } catch (URISyntaxException e) { ++ throw new IllegalArgumentException(Resources.format(Resources.Keys.CannotConnectTo_1, key), e); ++ } ++ }); ++ } else { ++ throw new FileSystemNotFoundException(Resources.format( ++ Resources.Keys.FileSystemInitialized_2, 0, first)); } -- throw new FileSystemNotFoundException(Resources.format(Resources.Keys.FileSystemInitialized_2, 0, accessKey)); } /** @@@ -281,34 -485,93 +428,42 @@@ * The resulting {@code Path} is associated with a {@link FileSystem} * that already exists or is constructed automatically. * - * @param uri a <abbr>URI</abbr> of the form {@code "s3://accessKey@bucket/file"}. - * @param uri a <abbr>URI</abbr> of the form {@code "s3://accessKey@bucket/file"} (AWS S3) - * or {@code s3://accessKey@host:port/bucket/file} (Self-hosted S3) (in this second case, host AND port are mandatory). ++ * <p>The <abbr>URI</abbr> can be of the form {@code "s3://accessKey@bucket/file"} (<abbr>AWS</abbr> S3) ++ * or {@code "s3://accessKey@host:port/bucket/file"} (self-hosted S3). ++ * In the latter case, the host <em>and</em> the port are mandatory.</p> ++ * ++ * @param uri an <abbr>URI</abbr> of the form {@code "s3://accessKey@[host:port/]bucket/file"}. * @return the resulting {@code Path}. * @throws IllegalArgumentException if the URI is not supported by this provider. * @throws FileSystemNotFoundException if the file system does not exist and cannot be created automatically. */ @Override public Path getPath(final URI uri) { -- final String accessKey = getAccessKey(uri); - final ClientFileSystem fs; - String host = uri.getHost(); - final int port = uri.getPort(); - - if (host == null) { - /* - * The host is null if the authority contains characters that are invalid for a host name. - * For example if the host contains underscore character ('_'), then it is considered invalid. - * We could use the authority instead, but that authority may contain a user name, port number, etc. - * Current version do not try to parse that string. - */ - host = uri.getAuthority(); - if (host == null) host = uri.toString(); - throw new IllegalArgumentException(Resources.format(Resources.Keys.InvalidBucketName_1, host)); - } - - ClientFileSystem fs; -- if (accessKey == null) { - fs = getDefaultFileSystem(); - if (port < 0) { - fs = getDefaultFileSystem(); - } else { - try { - fs = getDefaultFileSystem(host, port); - } catch (URISyntaxException ex) { - throw new IllegalArgumentException("Invalid URI: " + uri, ex); - } - } -- } else { -- // TODO: we may need a way to get password here. - fs = fileSystems.computeIfAbsent(accessKey, (key) -> new ClientFileSystem(FileService.this, null, key, null, null)); - // TODO: we may need a way to get SSL status here (is HTTPS or not) - ClientFileSystemKey fsKey; - - if (port < 0) { - fsKey = new ClientFileSystemKey(accessKey, DEFAULT_HOST_KEY, DEFAULT_PORT_KEY, DEFAULT_IS_HTTPS); - } else { - fsKey = new ClientFileSystemKey(accessKey, host, port, DEFAULT_IS_HTTPS); - } - - // Compute if absent - try { - synchronized (fileSystems) { - fs = fileSystems.get(fsKey); - if (fs == null) { - fs = new ClientFileSystem(this, null, fsKey.host, fsKey.port, fsKey.isHttps, fsKey.accessKey, null, null); - fileSystems.put(fsKey, fs); - } - } - } catch (URISyntaxException ex) { - throw new IllegalArgumentException("Invalid URI: " + uri, ex); - } -- } - - String path = uri.getPath(); - + /* - * In case of custom host, bucket name will be the first element of the "uri.getPath()" - * We need to get the first element of this path (this will be the bucket), and the second part will be ++ * In case of custom host, bucket name will be the first element of the "uri.getPath()". ++ * We want: ++ * ++ * - `host` as the S3 bucket name. ++ * - `path` as the path in above bucket. + */ - if (fs.host != null) { - if (!(fs.host.equalsIgnoreCase(DEFAULT_HOST_KEY))) { - if (path.startsWith("/")) { - path = path.substring(1); - } - String[] parts = path.split("/", 2); - if (parts.length >= 2) { - // Bucket + Path specified (=> /bucket/path/to/folder) - host = parts[0]; - path = "/" + parts[1]; - } else if (parts.length == 1) { - // Bucket specified (no path) (=> /bucket) - host = parts[0]; - path = null; - } ++ String path = uri.getPath(); + String host = uri.getHost(); - if (host == null) { - /* - * The host is null if the authority contains characters that are invalid for a host name. - * For example if the host contains underscore character ('_'), then it is considered invalid. - * We could use the authority instead, but that authority may contain a user name, port number, etc. - * Current version do not try to parse that string. - */ - host = uri.getAuthority(); - if (host == null) host = uri.toString(); - throw new IllegalArgumentException(Resources.format(Resources.Keys.InvalidBucketName_1, host)); ++ if (host != null) { ++ if (path.startsWith("/")) { ++ path = path.substring(1); ++ } ++ String[] parts = path.split("/", 2); ++ if (parts.length >= 2) { ++ // Bucket + Path specified. Example: "/bucket/path/to/folder" ++ host = parts[0]; ++ path = "/" + parts[1]; ++ } else if (parts.length == 1) { ++ // Bucket specified without path. Example: "/bucket" ++ host = parts[0]; ++ path = null; + } } - final String path = uri.getPath(); - /* - * - "host" in this part is the S3 bucket name - * - "path" is the path in this bucket - */ ++ final ClientFileSystem fs = getFileSystem(uri, true); return new KeyPath(fs, host, (path != null) ? new String[] {path} : CharSequences.EMPTY_ARRAY, true); } diff --cc endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/KeyPath.java index 50171eb101,4b564ac99d..5ce7070213 --- a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/KeyPath.java +++ b/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/KeyPath.java @@@ -47,6 -48,6 +47,7 @@@ import org.apache.sis.util.resources.Er * The interpretation of {@link ClientFileSystem#separator} as a path separator is done by this class.</p> * * @author Martin Desruisseaux (Geomatys) ++ * @author Quentin Bialota (Geomatys) */ final class KeyPath implements Path { /** @@@ -57,7 -58,7 +58,7 @@@ /** * Separator between {@value #SCHEME} and the bucket name. */ -- private static final String SCHEME_SEPARATOR = "://"; ++ static final String SCHEME_SEPARATOR = "://"; /** * Length of the {@code "S3://"} header. @@@ -676,7 -677,7 +677,7 @@@ search: if (key != null) @Override public Path relativize(final Path other) { if (other instanceof KeyPath) { -- final KeyPath kp = (KeyPath) other; ++ final var kp = (KeyPath) other; if (kp.startsWith(this) && key != null) { final String suffix = kp.key.substring(key.length() + fs.separator.length()); if (!suffix.isEmpty()) { @@@ -688,7 -689,7 +689,8 @@@ } /** -- * Returns an URI with the {@value #SCHEME} scheme if the path is absolute, or a relative URI otherwise. ++ * Returns an <abbr>URI</abbr> with the {@value #SCHEME} scheme if the path is absolute, ++ * or a relative <abbr>URI</abbr> otherwise. * * <p>Note: {@link Path#toUri()} specification mandate an absolute URI. * But we cannot provide an absolute URI if this path is not already absolute.</p> @@@ -699,12 -700,30 +701,39 @@@ public URI toUri() { String path = key; if (path != null) { -- final StringBuilder sb = new StringBuilder(path.length() + 2).append('/').append(path); ++ final var sb = new StringBuilder(path.length() + 2).append('/').append(path); if (isDirectory) sb.append('/'); path = sb.toString(); ++ } else { ++ path = ""; ++ } ++ if (fs == null) { ++ throw new IllegalStateException(Resources.format(Resources.Keys.MissingFileSystem_1, path)); ++ } ++ /* ++ * We can address two different URI formats, ++ * depending on whether the file system is self-hosted or not: ++ * ++ * - Self-Hosted path: s3://accessKey@host:port/bucket/key ++ * - AWS path: s3://accessKey@bucket/key ++ * ++ * The `ClientFileSytem` constructor verified that either `host` ++ * and `port` are both specified, or neither of them is specifed. ++ * We also verify bucket presence to allow relative paths URIs. ++ */ ++ final Server server = fs.server; ++ final String host; ++ final int port; ++ if (server.host != null && bucket != null) { ++ host = server.host; ++ port = server.port; ++ path = "/" + bucket + path; ++ } else { ++ host = bucket; ++ port = Server.NO_PORT; } try { - return new URI(SCHEME, fs.accessKey, bucket, -1, path, null, null); - if (fs != null) { - /* - * We can address two different URI formats, depending on whether the file system is self-hosted or not: - * - Self-Hosted path : s3://accessKey@host:port/bucket/key - * - AWS path : s3://accessKey@bucket/key - * We also verify bucket presence to allow relative paths URIs. - */ - if (fs.host != null && fs.port < 0) { - throw new IllegalStateException("Self-hosted file system shall have a port number."); - } else if (fs.host == null && fs.port >= 0) { - throw new IllegalStateException("Port number specified, but no host name. Incompatible with self-hosted and AWS file system."); - } - boolean selfHosted = fs.host != null && fs.port >= 0; - String host = (selfHosted && bucket != null) ? fs.host : bucket; - int port = (selfHosted && bucket != null) ? fs.port : -1; - String uriPath = (selfHosted && bucket != null) ? "/" + bucket + (path != null ? path : "") : (path != null ? path : ""); - return new URI(SCHEME, fs.accessKey, host, port, uriPath, null, null); - } - throw new IllegalStateException("No filesystem associated with this path."); ++ return new URI(SCHEME, server.accessKey, host, port, path, null, null); } catch (URISyntaxException e) { throw new IllegalStateException(e.getMessage(), e); } @@@ -718,14 -737,27 +747,7 @@@ if (bucket == null && !isDirectory) { return key; } - /* - * We can address two different URI formats, depending on whether the file system is self-hosted or not: - * - Self-Hosted path : s3://accessKey@host:port/bucket/key - * - AWS path : s3://accessKey@bucket/key - */ - if (fs.host != null && fs.port < 0) { - throw new IllegalStateException("Self-hosted file system shall have a port number."); - } else if (fs.host == null && fs.port >= 0) { - throw new IllegalStateException("Port number specified, but no host name. Incompatible with self-hosted and AWS file system."); - } -- final StringBuilder sb = new StringBuilder(); -- if (bucket != null) { -- sb.append(SCHEME).append(SCHEME_SEPARATOR); -- if (fs.accessKey != null) { -- sb.append(fs.accessKey).append('@'); - } - if (fs.host != null && fs.port >= 0) { - sb.append(fs.host).append(':').append(fs.port).append('/'); -- } -- sb.append(bucket); -- } ++ final var sb = fs.server.toString(bucket); if (key != null) { if (bucket != null) { sb.append(fs.separator); diff --cc endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Resources.java index cf5e2608c1,d7afb279a9..b77165c5ef --- a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Resources.java +++ b/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Resources.java @@@ -70,13 -56,13 +70,18 @@@ class Resources extends IndexedResource */ public static final short CanNotChangeToAbsolutePath = 1; ++ /** ++ * Cannot connect to “{0}”. ++ */ ++ public static final short CannotConnectTo_1 = 12; ++ /** * Empty path. */ public static final short EmptyPath = 3; /** -- * File system {0,choice,0#not|1#already} initialized for the “{1}” access key. ++ * File system {0,choice,0#not|1#already} initialized for the “{1}” server or access key. */ public static final short FileSystemInitialized_2 = 4; @@@ -90,6 -76,6 +95,21 @@@ */ public static final short MissingAccessKey_2 = 5; ++ /** ++ * No file system associated with the “{0}” path. ++ */ ++ public static final short MissingFileSystem_1 = 9; ++ ++ /** ++ * Port number {0} specified, but no host name. ++ */ ++ public static final short MissingHostName_1 = 10; ++ ++ /** ++ * Self-hosted file system “{0}” shall have a port number. ++ */ ++ public static final short MissingPortNumber_1 = 11; ++ /** * Specified path must be an absolute S3 path. */ diff --cc endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Resources.properties index ffd2c45e96,ffd2c45e96..3330b6ed46 --- a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Resources.properties +++ b/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Resources.properties @@@ -20,10 -20,10 +20,14 @@@ # For resources shared by all modules in the Apache SIS project, see "org.apache.sis.util.resources" package. # CanNotChangeToAbsolutePath = Cannot change a relative path to an absolute path. ++CannotConnectTo_1 = Cannot connect to \u201c{0}\u201d. EmptyPath = Empty path. --FileSystemInitialized_2 = File system {0,choice,0#not|1#already} initialized for the \u201c{1}\u201d access key. ++FileSystemInitialized_2 = File system {0,choice,0#not|1#already} initialized for the \u201c{1}\u201d server or access key. InvalidBucketName_1 = Invalid bucket name in \u201c{0}\u201d. MissingAccessKey_2 = Missing {0,choice,0#public|1#secret} access key in \u201c{1}\u201d URI. ++MissingFileSystem_1 = No file system associated with the \u201c{0}\u201d path. ++MissingHostName_1 = Port number {0} specified, but no host name. ++MissingPortNumber_1 = Self-hosted file system \u201c{0}\u201d shall have a port number. MustBeAbsolutePath = Specified path must be an absolute S3 path. MustHaveKeyComponent = Specified path cannot be the root. UnexpectedProtocol_1 = Unexpected \u201c{0}\u201d protocol. diff --cc endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Resources_fr.properties index c92a992f2a,c92a992f2a..876b7a850a --- a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Resources_fr.properties @@@ -25,10 -25,10 +25,14 @@@ # U+00A0 NO-BREAK SPACE before : # CanNotChangeToAbsolutePath = Ne peut pas changer un chemin relatif en chemin absolu. ++CannotConnectTo_1 = Ne peut pas se connecter \u00e0 \u00ab\u202f{0}\u202f\u00bb. EmptyPath = Le chemin est vide. --FileSystemInitialized_2 = Le syst\u00e8me de fichier {0,choice,0#n\u2019a pas \u00e9t\u00e9|1#est d\u00e9j\u00e0} initialis\u00e9 pour la cl\u00e9 d\u2019acc\u00e8s \u00ab\u202f{1}\u202f\u00bb. ++FileSystemInitialized_2 = Le syst\u00e8me de fichier {0,choice,0#n\u2019a pas \u00e9t\u00e9|1#est d\u00e9j\u00e0} initialis\u00e9 pour le serveur ou la cl\u00e9 d\u2019acc\u00e8s \u00ab\u202f{1}\u202f\u00bb. InvalidBucketName_1 = Le nom du compartiment dans \u00ab\u202f{0}\u202f\u00bb est invalide. --MissingAccessKey_2 = Il manque la cl\u00e9 d'acc\u00e8s {0,choice,0#publique|1#secr\u00e8te} dans l'URI \u00ab\u202f{1}\u202f\u00bb. ++MissingAccessKey_2 = Il manque la cl\u00e9 d\u2019acc\u00e8s {0,choice,0#publique|1#secr\u00e8te} dans l\u2019URI \u00ab\u202f{1}\u202f\u00bb. ++MissingFileSystem_1 = Aucun syst\u00e8me de fichier n\u2019a \u00e9t\u00e9 associ\u00e9 au chemin \u00ab\u202f{0}\u202f\u00bb. ++MissingHostName_1 = Un num\u00e9ro de port {0} a \u00e9t\u00e9 sp\u00e9cifi\u00e9, mais sans nom d\u2019h\u00f4te. ++MissingPortNumber_1 = L\u2019h\u00f4te \u00ab\u202f{0}\u202f\u00bb doit \u00eatre associ\u00e9 \u00e0 un num\u00e9ro de port. MustBeAbsolutePath = Le chemin sp\u00e9cifi\u00e9 doit \u00eatre un chemin S3 absolu. MustHaveKeyComponent = Le chemin sp\u00e9cifi\u00e9 ne peut pas \u00eatre la racine. UnexpectedProtocol_1 = Le protocole \u00ab\u202f{0}\u202f\u00bb est inattendu. diff --cc endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Server.java index 0000000000,0000000000..ede83bb87a new file mode 100644 --- /dev/null +++ b/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/Server.java @@@ -1,0 -1,0 +1,230 @@@ ++/* ++ * 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.cloud.aws.s3; ++ ++import java.net.URI; ++import java.net.URISyntaxException; ++import java.util.Objects; ++ ++ ++/** ++ * Information about the server to connect to. ++ * This is used as a key for identifying the file systems stored in {@link FileService#fileSystems}. ++ * ++ * @author Quentin Bialota (Geomatys) ++ * @author Martin Desruisseaux (Geomatys) ++ */ ++final class Server { ++ /** ++ * Default value when user did not specified explicitly a protocol. ++ * In such case, the default protocol is <abbr>HTTPS</abbr>. ++ * ++ * <p>Protocol can also be defined with:</p> ++ * <ul> ++ * <li>{@value FileService#IS_HTTPS} configuration properties.</li> ++ * </ul> ++ */ ++ static final boolean DEFAULT_IS_HTTPS = true; ++ ++ /** ++ * An arbitrary value when the user did not specified explicitly a port. ++ * In such case, no port is assigned and the default is chosen by Java. ++ * Note that "no port" is not the same as "default port" for this class, ++ * since the default ports for <abbr>HTTP</abbr> and <abbr>HTTPS</abbr> are hard-coded ++ * to {@value FileService#HTTP_PORT} and {@value FileService#HTTPS_PORT} respectively. ++ * ++ * <p>Port can also be defined with:</p> ++ * <ul> ++ * <li>{@value FileService#PORT} configuration properties.</li> ++ * </ul> ++ */ ++ static final int NO_PORT = -1; ++ ++ /** ++ * The <abbr>AWS</abbr> S3 access key, or {@code null} if none. ++ * In the latter case, the default mechanism documented in <abbr>AWS</abbr> <abbr>SDK</abbr> is used. ++ * In preference order: ++ * ++ * <ul> ++ * <li>{@code aws.accessKeyId} and {@code aws.secretAccessKey} Java system properties.</li> ++ * <li>{@code AWS_ACCESS_KEY_ID} and {@code AWS_SECRET_ACCESS_KEY} environment variables.</li> ++ * <li>{@code ~/.aws/credentials} or {@code ~/.aws/config} files.</li> ++ * </ul> ++ */ ++ final String accessKey; ++ ++ /** ++ * The S3 host (if not stored on Amazon <abbr>AWS</abbr> Infrastructure). ++ * May be {@code null} if unspecified. ++ * If and only if non-null, the {@link #port} shall be specified. ++ */ ++ final String host; ++ ++ /** ++ * The S3 port (if not stored on Amazon <abbr>AWS</abbr> Infrastructure). ++ * May be {@link #NO_PORT} if unspecified. ++ * The port shall be specified if and only if {@link #host} is non-null. ++ */ ++ final int port; ++ ++ /** ++ * Whether the S3 <abbr>HTTP</abbr> Protocol is secure. ++ * Default is {@code true}. ++ */ ++ final boolean isHttps; ++ ++ /** ++ * Creates a new server for the given access key. ++ * ++ * @param accessKey the S3 access key or {@code null} for using <abbr>AWS</abbr> configuration. ++ */ ++ Server(final String accessKey) { ++ this.accessKey = accessKey; ++ this.host = null; ++ this.port = NO_PORT; ++ this.isHttps = DEFAULT_IS_HTTPS; ++ } ++ ++ /** ++ * Creates a new server for the given access key, host, port and protocol (secure or not secure). ++ * ++ * @param accessKey the S3 access key or {@code null} for using <abbr>AWS</abbr> configuration. ++ * @param host the host or {@code null} for using <abbr>AWS</abbr> configuration. ++ * @param port the port or {@link #NO_PORT} for using <abbr>AWS</abbr> configuration. ++ * @param isHttps whether the protocol is secure. ++ * @param uri <abbr>URI</abbr> to use for completing missing information, or {@code null} if none. ++ */ ++ Server(final String accessKey, String host, int port, final boolean isHttps, final URI uri) { ++ if (uri != null) { ++ if (uri.getHost() == null) { ++ /* ++ * The host is null if the authority contains characters that are invalid for a host name. ++ * For example if the host contains underscore character ('_'), then it is considered invalid. ++ * We could use the authority instead, but that authority may contain a user name, port number, etc. ++ * Current version does not try to parse that string. ++ */ ++ String bucket = uri.getAuthority(); ++ if (bucket == null) bucket = uri.toString(); ++ throw new IllegalArgumentException(Resources.format(Resources.Keys.InvalidBucketName_1, bucket)); ++ } ++ /* ++ * Host and port number specified in the property map have precedence over the URI. ++ * If these information were not found anywhere, the AWS SDK will fetch them itself. ++ */ ++ if (port < 0) { ++ port = uri.getPort(); ++ if (port < 0) { ++ if (host != null) { ++ // Use default value only if the host was specified in the properties map. ++ // If the host and the port were specified by the URI, assume that the URI was complete. ++ port = isHttps ? FileService.HTTPS_PORT : FileService.HTTP_PORT; ++ } ++ } else if (host == null) { ++ host = uri.getHost(); ++ } ++ } ++ } ++ this.accessKey = accessKey; ++ this.host = host; ++ this.port = port; ++ this.isHttps = isHttps; ++ } ++ ++ /** ++ * Returns the protocol, host and port number as an <abbr>URI</abbr>. ++ * The access key is ignored. ++ * ++ * @return <abbr>URI</abbr> to the server, ignoring the access key. ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr. ++ */ ++ final URI toURI() throws URISyntaxException { ++ return new URI(isHttps ? "https" : "http", null, host, port, "", null, null); ++ } ++ ++ /** ++ * Indicates whether some other object is "equal to" this one. ++ * ++ * @param obj the object with which to compare. ++ * @return {@code true} if this object is equal to {@code obj}, {@code false} otherwise. ++ */ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ if (obj instanceof Server) { ++ final var that = (Server) obj; ++ return Objects.equals(accessKey, that.accessKey) && ++ Objects.equals(host, that.host) && ++ port == that.port && ++ isHttps == that.isHttps; ++ } ++ return false; ++ } ++ ++ /** ++ * Returns a hash code value for the object. ++ * ++ * @return a hash code value for this object. ++ */ ++ @Override ++ public int hashCode() { ++ return Objects.hash(accessKey, host, port, isHttps); ++ } ++ ++ /** ++ * Returns a string representation of the path to the server. ++ * The string uses the <abbr>URI</abbr> syntax. ++ * This is used for error message. ++ * ++ * @return path to the server. ++ */ ++ @Override ++ public String toString() { ++ if (host == null) { ++ return accessKey; ++ } ++ return toString(null).toString(); ++ } ++ ++ /** ++ * Returns a string representation of the path to the given bucket on this server. ++ * ++ * @param bucket the bucket for which to get the path, or {@code null}. ++ * @return path to the given bucket. ++ */ ++ final StringBuilder toString(final String bucket) { ++ final var sb = new StringBuilder(); ++ if (bucket != null || host != null) { ++ sb.append(KeyPath.SCHEME).append(KeyPath.SCHEME_SEPARATOR); ++ if (accessKey != null) { ++ sb.append(accessKey).append('@'); ++ } ++ if (host != null) { ++ sb.append(host); ++ if (port >= 0) { ++ sb.append(':').append(port); ++ } ++ sb.append('/'); ++ } ++ if (bucket != null) { ++ sb.append(bucket); ++ } ++ } ++ return sb; ++ } ++} diff --cc endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/package-info.java index caf3fc4632,695d6fe144..e46b5c215a --- a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/package-info.java +++ b/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/package-info.java @@@ -17,8 -17,8 +17,8 @@@ /** -- * Java NIO wrappers for Amazon Simple Storage Service (S3). -- * The wrapped framework is AWS SDK version 2. ++ * Java <abbr>NIO</abbr> wrappers for Amazon Simple Storage Service (S3). ++ * The wrapped framework is <abbr>AWS</abbr> <abbr>SDK</abbr> version 2. * * <h2><abbr>URL</abbr> syntax</h2> * The S3 storage mechanism is similar to a {@code java.util.Map}: diff --cc endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/ClientFileSystemTest.java index bad2ad252e,8139b8abff..c306599dda --- a/endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/ClientFileSystemTest.java +++ b/endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/ClientFileSystemTest.java @@@ -16,6 -16,6 +16,8 @@@ */ package org.apache.sis.cloud.aws.s3; ++import java.net.URISyntaxException; ++ // Test dependencies import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@@ -26,32 -28,44 +28,39 @@@ import org.apache.sis.test.TestCase * Tests {@link ClientFileSystem}. * * @author Martin Desruisseaux (Geomatys) ++ * @author Quentin Bialota (Geomatys) */ ++@SuppressWarnings("exports") public final class ClientFileSystemTest extends TestCase { /** -- * The instance to use for testing purposes. -- */ -- private final ClientFileSystem fs; - - private final ClientFileSystem fsSelfHosted; -- -- /** -- * Returns the file system to use for testing purpose. ++ * Creates a new test case. */ -- static ClientFileSystem create() { - return new ClientFileSystem(new FileService(), null); - return new ClientFileSystem(new FileService(), null, null); ++ public ClientFileSystemTest() { } /** - * Creates a new test case. + * Returns the file system to use for testing purpose. - * This file system is configured for self-hosted S3 server. - */ - static ClientFileSystem createSelfHosted() throws URISyntaxException { - return new ClientFileSystem(new FileService(), null, "testhost", 8581, true, null, null, null); - } - - /** - * Creates a new test case. ++ * ++ * @param selfHosted whether the service should be self hosted. ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ - public ClientFileSystemTest() { - public ClientFileSystemTest() throws URISyntaxException { -- fs = create(); - fsSelfHosted = createSelfHosted(); ++ static ClientFileSystem create(final boolean selfHosted) throws URISyntaxException { ++ final var fs = new FileService(); ++ if (selfHosted) { ++ return new ClientFileSystem(fs, null, new Server(null, "testhost", 8581, true, null), null, null); ++ } else { ++ return new ClientFileSystem(fs, new Server(null)); ++ } } /** * Tests {@link ClientFileSystem#getSeparator()}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testGetSeparator() { -- assertEquals("/", fs.getSeparator()); - assertEquals("/", fsSelfHosted.getSeparator()); ++ public void testGetSeparator() throws URISyntaxException { ++ assertEquals("/", create(false).getSeparator()); ++ assertEquals("/", create(true).getSeparator()); } } diff --cc endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/FileServiceTest.java index 0000000000,3970a9bd27..ea6ea97e20 mode 000000,100644..100644 --- a/endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/FileServiceTest.java +++ b/endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/FileServiceTest.java @@@ -1,0 -1,175 +1,159 @@@ + /* + * 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.cloud.aws.s3; + + import java.io.IOException; + import java.net.URI; + import java.net.URISyntaxException; -import java.nio.file.FileSystem; + import java.nio.file.FileSystemAlreadyExistsException; + import java.nio.file.Path; -import java.util.HashMap; + import java.util.Map; + + // Test dependencies + import org.junit.jupiter.api.Test; + import static org.junit.jupiter.api.Assertions.*; + import org.apache.sis.test.TestCase; + ++ + /** + * Tests {@link FileService}. + * + * @author Quentin Bialota (Geomatys) + */ ++@SuppressWarnings("exports") + public final class FileServiceTest extends TestCase { ++ /** ++ * The service to test. ++ */ ++ private final FileService service; + + /** + * Creates a new test case. + */ + public FileServiceTest() { ++ service = new FileService(); + } + + /** - * Tests AWS S3 FileSystem creation. ++ * Creates a new file system and performs a few consistency checks. ++ * ++ * @param uri an <abbr>URI</abbr> of the form {@code "s3://accessKey@[host:port/]bucket/file"}. ++ * @param complete whether the <abbr>URI</abbr> is sufficient for finding the file system. ++ * @param properties properties to configure the file system, or {@code null} if none. ++ * @return the new file system. ++ * @throws IOException if the tested method failed. + */ - @Test - public void testNewFileSystemAws() - throws URISyntaxException, IOException { - final FileService service = new FileService(); - final Map<String, Object> properties = new HashMap<>(); - properties.put(FileService.AWS_SECRET_ACCESS_KEY, "secret"); - - URI uri = new URI("S3://accessKey@bucket/file"); - FileSystem fs = service.newFileSystem(uri, properties); - assertNotNull(fs); - assertTrue(fs instanceof ClientFileSystem); - - assertThrows(FileSystemAlreadyExistsException.class, () -> { ++ private ClientFileSystem newFileSystem(final URI uri, final boolean complete, final Map<String, ?> properties) ++ throws IOException ++ { ++ ClientFileSystem fs = assertInstanceOf(ClientFileSystem.class, service.newFileSystem(uri, properties)); ++ var exception = assertThrows(FileSystemAlreadyExistsException.class, () -> { + service.newFileSystem(uri, properties); + }); ++ assertNotNull(exception.getMessage()); ++ if (complete) { ++ assertSame(fs, service.getFileSystem(uri)); ++ } ++ return fs; ++ } + - // Test FileSystem fetch by URI - FileSystem fetchedFs = service.getFileSystem(uri); - assertSame(fs, fetchedFs); ++ /** ++ * Tests AWS S3 FileSystem creation. ++ * ++ * @throws URISyntaxException if the <abbr>URI</abbr> used for this test is invalid. ++ * @throws IOException if the tested method failed. ++ */ ++ @Test ++ public void testNewFileSystemAws() throws URISyntaxException, IOException { ++ newFileSystem(new URI("S3://accessKey@bucket/file"), true, ++ Map.of(FileService.AWS_SECRET_ACCESS_KEY, "secret")); + } + + /** + * Tests Self-Hosted S3 FileSystem creation. ++ * ++ * @throws URISyntaxException if the <abbr>URI</abbr> used for this test is invalid. ++ * @throws IOException if the tested method failed. + */ + @Test - public void testNewFileSystemSelfHosted() - throws URISyntaxException, IOException { - final FileService service = new FileService(); - final Map<String, Object> properties = new HashMap<>(); - properties.put(FileService.AWS_SECRET_ACCESS_KEY, "secret"); - - URI uri = new URI("S3://accessKey@localhost:8080/bucket/file"); - FileSystem fs = service.newFileSystem(uri, properties); - assertNotNull(fs); - assertTrue(fs instanceof ClientFileSystem); - - assertThrows(FileSystemAlreadyExistsException.class, () -> { - service.newFileSystem(uri, properties); - }); - - // Test FileSystem fetch by URI - FileSystem fetchedFs = service.getFileSystem(uri); - assertSame(fs, fetchedFs); ++ public void testNewFileSystemSelfHosted() throws URISyntaxException, IOException { ++ newFileSystem(new URI("S3://accessKey@localhost:8080/bucket/file"), true, ++ Map.of(FileService.AWS_SECRET_ACCESS_KEY, "secret")); + } + + /** + * Tests Self-Hosted S3 FileSystem creation with properties. ++ * ++ * @throws URISyntaxException if the <abbr>URI</abbr> used for this test is invalid. ++ * @throws IOException if the tested method failed. + */ + @Test - public void testNewFileSystemSelfHostedWithPropertiesNoPort() - throws URISyntaxException, IOException { - final FileService service = new FileService(); - final Map<String, Object> properties = new HashMap<>(); - properties.put(FileService.AWS_SECRET_ACCESS_KEY, "secret"); - properties.put(FileService.S3_HOST_URL, "localhost"); - properties.put(FileService.S3_IS_HTTPS, false); - - URI uri = new URI("S3://accessKey@bucket/file"); - FileSystem fs = service.newFileSystem(uri, properties); - assertNotNull(fs); - assertTrue(fs instanceof ClientFileSystem); - - assertThrows(FileSystemAlreadyExistsException.class, () -> { - service.newFileSystem(uri, properties); - }); - - assertEquals(80, ((ClientFileSystem) fs).port); ++ public void testNewFileSystemSelfHostedWithPropertiesNoPort() throws URISyntaxException, IOException { ++ final var uri = new URI("S3://accessKey@bucket/file"); ++ final ClientFileSystem fs = newFileSystem(uri, false, Map.of( ++ FileService.AWS_SECRET_ACCESS_KEY, "secret", ++ FileService.HOST_URL, "localhost", ++ FileService.IS_HTTPS, Boolean.FALSE)); ++ ++ assertEquals(FileService.HTTP_PORT, fs.server.port); + Path basePath = fs.getPath("/bucket/file"); + URI generatedURI = basePath.toUri(); + assertEquals("S3://accessKey@localhost:80/bucket/file", generatedURI.toString()); + - // Test FileSystem fetch by URI - FileSystem fetchedFs = service.getFileSystem(uri); - assertNotSame(fs, fetchedFs); - fetchedFs = service.getFileSystem(generatedURI); - assertSame(fs, fetchedFs); ++ // Test FileSystem fetch by URI. ++ assertSame(fs, service.getFileSystem(generatedURI)); + } + + /** + * Tests Self-Hosted S3 FileSystem creation with properties. ++ * ++ * @throws URISyntaxException if the <abbr>URI</abbr> used for this test is invalid. ++ * @throws IOException if the tested method failed. + */ + @Test - public void testNewFileSystemSelfHostedWithProperties() - throws URISyntaxException, IOException { - final FileService service = new FileService(); - final Map<String, Object> properties = new HashMap<>(); - properties.put(FileService.AWS_SECRET_ACCESS_KEY, "secret"); - properties.put(FileService.S3_HOST_URL, "localhost"); - properties.put(FileService.S3_PORT, 8454); - properties.put(FileService.S3_IS_HTTPS, false); - - URI uri = new URI("S3://accessKey@bucket/file"); - FileSystem fs = service.newFileSystem(uri, properties); - assertNotNull(fs); - assertTrue(fs instanceof ClientFileSystem); - - assertThrows(FileSystemAlreadyExistsException.class, () -> { - service.newFileSystem(uri, properties); - }); - - assertEquals(8454, ((ClientFileSystem) fs).port); ++ public void testNewFileSystemSelfHostedWithProperties() throws URISyntaxException, IOException { ++ final URI uri = new URI("S3://accessKey@bucket/file"); ++ final ClientFileSystem fs = newFileSystem(uri, false, Map.of( ++ FileService.AWS_SECRET_ACCESS_KEY, "secret", ++ FileService.HOST_URL, "localhost", ++ FileService.PORT, 8454, ++ FileService.IS_HTTPS, Boolean.FALSE)); ++ ++ assertEquals(8454, fs.server.port); + Path basePath = fs.getPath("/bucket/file"); + URI generatedURI = basePath.toUri(); + assertEquals("S3://accessKey@localhost:8454/bucket/file", generatedURI.toString()); + - // Test FileSystem fetch by URI - FileSystem fetchedFs = service.getFileSystem(uri); - assertNotSame(fs, fetchedFs); - fetchedFs = service.getFileSystem(generatedURI); - assertSame(fs, fetchedFs); ++ // Test FileSystem fetch by URI. ++ assertSame(fs, service.getFileSystem(generatedURI)); + } + + /** + * Tests FileSystem creation with missing secret key. ++ * ++ * @throws URISyntaxException if the <abbr>URI</abbr> used for this test is invalid. + */ + @Test - public void testMissingSecretKey() - throws URISyntaxException { - final FileService service = new FileService(); - final Map<String, Object> properties = new HashMap<>(); - - URI uri = new URI("S3://accessKey@bucket/file"); - assertThrows(IllegalArgumentException.class, () -> { ++ public void testMissingSecretKey() throws URISyntaxException { ++ final Map<String, Object> properties = Map.of(); ++ final var uri = new URI("S3://accessKey@bucket/file"); ++ var exception = assertThrows(IllegalArgumentException.class, () -> { + service.newFileSystem(uri, properties); + }); ++ assertNotNull(exception.getMessage()); + } + } diff --cc endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/KeyPathMatcherTest.java index afcefb3975,afcefb3975..28b2f8617a --- a/endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/KeyPathMatcherTest.java +++ b/endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/KeyPathMatcherTest.java @@@ -17,6 -17,6 +17,7 @@@ package org.apache.sis.cloud.aws.s3; import java.util.Locale; ++import java.net.URISyntaxException; // Test dependencies import org.junit.jupiter.api.Test; @@@ -29,6 -29,6 +30,7 @@@ import org.apache.sis.test.TestCase * * @author Martin Desruisseaux (Geomatys) */ ++@SuppressWarnings("exports") public final class KeyPathMatcherTest extends TestCase { /** * Creates a new test case. @@@ -38,11 -38,11 +40,13 @@@ /** * Tests a pattern using "glob" syntax. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testGlob() { -- final KeyPathMatcher matcher = new KeyPathMatcher("glob:bar*foo/fuu/**/f?i", ClientFileSystem.DEFAULT_SEPARATOR); -- final ClientFileSystem fs = ClientFileSystemTest.create(); ++ public void testGlob() throws URISyntaxException { ++ final var matcher = new KeyPathMatcher("glob:bar*foo/fuu/**/f?i", ClientFileSystem.DEFAULT_SEPARATOR); ++ final ClientFileSystem fs = ClientFileSystemTest.create(false); assertTrue (matcher.matches(new KeyPath(fs, "bar_skip_foo/fuu/d1/d2/d3/f_i", false))); assertFalse(matcher.matches(new KeyPath(fs, "bar_sk/p_foo/fuu/d1/d2/d3/f_i", false))); } diff --cc endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/KeyPathTest.java index f3ce186b69,389b6e2bb8..d7b719d8fa --- a/endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/KeyPathTest.java +++ b/endorsed/src/org.apache.sis.cloud.aws/test/org/apache/sis/cloud/aws/s3/KeyPathTest.java @@@ -30,23 -31,33 +31,34 @@@ import org.apache.sis.test.TestCase * Tests {@link KeyPath}. * * @author Martin Desruisseaux (Geomatys) ++ * @author Quentin Bialota (Geomatys) */ ++@SuppressWarnings("exports") public final class KeyPathTest extends TestCase { /** * The file system used in for the test paths. */ -- private final ClientFileSystem fs; - - /** - * The file system used to test self-hosted S3 paths. - */ - private final ClientFileSystem fsSelfHosted; ++ private ClientFileSystem fs; /** * The path to test. */ -- private final KeyPath absolute, relative; ++ private KeyPath absolute, relative; /** - * The path to test with self-hosted S3 file system. + * Creates a new test case. */ - private final KeyPath absoluteSelfHosted, relativeSelfHosted; + public KeyPathTest() { - fs = ClientFileSystemTest.create(); ++ } + + /** - * Creates a new test case. ++ * Creates the file system and the paths to use for testing purpose. ++ * ++ * @param selfHosted whether the service should be self hosted. ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. + */ - public KeyPathTest() throws URISyntaxException { - fs = ClientFileSystemTest.create(); ++ private void createPaths(final boolean selfHosted) throws URISyntaxException { ++ fs = ClientFileSystemTest.create(selfHosted); final KeyPath root = new KeyPath(fs, Bucket.builder().name("the-bucket").build()); absolute = new KeyPath(root, "first/second/third/the-file", false); relative = new KeyPath(fs, "second/third/the-file", false); @@@ -54,9 -69,9 +66,12 @@@ /** * Tests the parsing done in the constructor. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testConstructor() { ++ public void testConstructor() throws URISyntaxException { ++ createPaths(false); assertEquals(relative, new KeyPath(fs, "second/third/the-file", new String[0], false)); assertEquals(relative, new KeyPath(fs, "second/", new String[] {"/third/", "the-file"}, false)); assertEquals(absolute, new KeyPath(fs, "S3://the-bucket/first/second/third/the-file", new String[0], false)); @@@ -66,20 -81,20 +81,26 @@@ /** * Tests {@link KeyPath#isAbsolute()}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testIsAbsolute() { ++ public void testIsAbsolute() throws URISyntaxException { ++ createPaths(false); assertTrue (absolute.isAbsolute()); assertFalse(relative.isAbsolute()); } /** * Tests {@link KeyPath#getRoot()}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testGetRoot() { ++ public void testGetRoot() throws URISyntaxException { ++ createPaths(false); assertNull(relative.getRoot()); -- final KeyPath p = (KeyPath) absolute.getRoot(); ++ final var p = (KeyPath) absolute.getRoot(); assertSame(p, p.getRoot()); assertTrue(p.isDirectory); assertNotNull(p.bucket); @@@ -88,9 -103,9 +109,12 @@@ /** * Tests {@link KeyPath#getFileName()}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testGetFileName() { ++ public void testGetFileName() throws URISyntaxException { ++ createPaths(false); KeyPath p = (KeyPath) absolute.getFileName(); assertSame(p, p.getFileName()); assertEquals("the-file", p.key); @@@ -106,9 -121,9 +130,12 @@@ /** * Tests {@link KeyPath#getParent()}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testGetParent() { ++ public void testGetParent() throws URISyntaxException { ++ createPaths(false); KeyPath p = (KeyPath) absolute.getParent(); assertEquals("first/second/third", p.key); assertTrue(p.isDirectory); @@@ -122,9 -137,9 +149,12 @@@ /** * Tests {@link KeyPath#getNameCount()}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testGetNameCount() { ++ public void testGetNameCount() throws URISyntaxException { ++ createPaths(false); assertEquals(5, absolute.getNameCount()); assertEquals(3, relative.getNameCount()); assertEquals(1, absolute.getRoot().getNameCount()); @@@ -132,9 -147,9 +162,12 @@@ /** * Tests {@link KeyPath#getName(int)}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testGetName() { ++ public void testGetName() throws URISyntaxException { ++ createPaths(false); assertEquals("S3://the-bucket", absolute.getName(0).toString()); assertEquals("first", absolute.getName(1).toString()); assertEquals("second", absolute.getName(2).toString()); @@@ -149,9 -164,9 +182,12 @@@ /** * Tests {@link KeyPath#subpath(int, int)}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testSubpath() { ++ public void testSubpath() throws URISyntaxException { ++ createPaths(false); assertEquals("S3://the-bucket/first/second", absolute.subpath(0, 3).toString()); assertEquals("second/third/the-file", absolute.subpath(2, 5).toString()); assertEquals("second/third", absolute.subpath(2, 4).toString()); @@@ -162,9 -177,9 +198,12 @@@ /** * Tests {@link KeyPath#startsWith(Path)}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testStartsWith() { ++ public void testStartsWith() throws URISyntaxException { ++ createPaths(false); assertFalse(absolute.startsWith(new KeyPath(fs, "first/second", true))); assertTrue (absolute.startsWith(new KeyPath(absolute, "first/second", true))); assertFalse(absolute.startsWith(new KeyPath(absolute, "first/secon", true))); @@@ -177,9 -192,9 +216,12 @@@ /** * Tests {@link KeyPath#endsWith(Path)}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testEndsWith() { ++ public void testEndsWith() throws URISyntaxException { ++ createPaths(false); assertTrue (relative.endsWith(relative)); assertTrue (absolute.endsWith(absolute)); assertTrue (absolute.endsWith(relative)); @@@ -191,11 -206,11 +233,14 @@@ /** * Tests {@link KeyPath#resolve(Path)}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testResolve() { ++ public void testResolve() throws URISyntaxException { ++ createPaths(false); assertSame(absolute, relative.resolve(absolute)); -- final KeyPath tip = new KeyPath(fs, "tip", false); ++ final var tip = new KeyPath(fs, "tip", false); assertEquals("second/third/the-file/tip", relative.resolve(tip).toString()); assertEquals("S3://the-bucket/first/second/third/the-file/tip", absolute.resolve(tip).toString()); assertEquals(absolute, new KeyPath(absolute, "first", true).resolve(relative)); @@@ -203,18 -218,18 +248,24 @@@ /** * Tests {@link KeyPath#relativize(Path)}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testRelativize() { -- final KeyPath base = new KeyPath(absolute, "first", true); ++ public void testRelativize() throws URISyntaxException { ++ createPaths(false); ++ final var base = new KeyPath(absolute, "first", true); assertEquals(relative, base.relativize(absolute)); } /** * Tests {@link KeyPath#iterator()}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testIterator() { ++ public void testIterator() throws URISyntaxException { ++ createPaths(false); verifyIterator(absolute.iterator(), true); verifyIterator(relative.iterator(), false); } @@@ -235,13 -250,35 +286,48 @@@ /** * Tests {@link KeyPath#compareTo(Path)}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. */ @Test -- public void testCompareTo() { ++ public void testCompareTo() throws URISyntaxException { ++ createPaths(false); assertEquals( 0, absolute.compareTo(absolute)); assertEquals( 0, relative.compareTo(relative)); assertEquals(-1, absolute.compareTo(relative)); assertEquals(+1, relative.compareTo(absolute)); assertTrue(absolute.compareTo(new KeyPath(absolute, "first", true)) > 0); } + + /** + * Tests {@link KeyPath#toString()}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. + */ + @Test - public void testToString() { ++ public void testToString() throws URISyntaxException { ++ createPaths(false); + assertEquals("S3://the-bucket/first/second/third/the-file", absolute.toString()); + assertEquals("second/third/the-file", relative.toString()); - assertEquals("S3://testhost:8581/the-bucket/first/second/third/the-file", absoluteSelfHosted.toString()); - assertEquals("second/third/the-file", relativeSelfHosted.toString()); ++ ++ createPaths(true); ++ assertEquals("S3://testhost:8581/the-bucket/first/second/third/the-file", absolute.toString()); ++ assertEquals("second/third/the-file", relative.toString()); + } + + /** + * Tests {@link KeyPath#toUri()}. ++ * ++ * @throws URISyntaxException if an error occurred while creating the <abbr>URI</abbr> for the self-hosted S3. + */ + @Test - public void testToUri() { ++ public void testToUri() throws URISyntaxException { ++ createPaths(false); + assertEquals("S3://the-bucket/first/second/third/the-file", absolute.toUri().toString()); + assertEquals("S3:/second/third/the-file", relative.toUri().toString()); - assertEquals("S3://testhost:8581/the-bucket/first/second/third/the-file", absoluteSelfHosted.toUri().toString()); - assertEquals("S3:/second/third/the-file", relativeSelfHosted.toUri().toString()); ++ ++ createPaths(true); ++ assertEquals("S3://testhost:8581/the-bucket/first/second/third/the-file", absolute.toUri().toString()); ++ assertEquals("S3:/second/third/the-file", relative.toUri().toString()); + } }
