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 c55ad2acaa894eb5d58831616bfc3a217baa8eca
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Dec 2 14:59:14 2023 +0100

    "SIS" command-line accept arguments of type `Object` instead of being 
restricted to `String` instances.
    The intend is to allow invocations from JShell, where the user could pass 
File, Path, URI, URL, etc.
    This commit contains also an opportunistic migration of JUnit 4 to JUnit 5 
for the impacted tests.
---
 .../main/org/apache/sis/console/AboutCommand.java  | 14 +++--
 .../main/org/apache/sis/console/CRSCommand.java    |  9 ++-
 .../main/org/apache/sis/console/Command.java       | 37 ++++++++-----
 .../main/org/apache/sis/console/CommandRunner.java | 64 +++++++++++++++-------
 .../apache/sis/console/FormattedOutputCommand.java | 18 +++---
 .../main/org/apache/sis/console/HelpCommand.java   |  3 +-
 .../org/apache/sis/console/IdentifierCommand.java  | 14 ++++-
 .../main/org/apache/sis/console/InfoCommand.java   |  6 +-
 .../org/apache/sis/console/MetadataCommand.java    |  9 ++-
 .../org/apache/sis/console/MimeTypeCommand.java    | 27 +++++----
 .../main/org/apache/sis/console/Option.java        | 12 ++--
 .../org/apache/sis/console/TransformCommand.java   | 61 +++++++++++++--------
 .../org/apache/sis/console/TranslateCommand.java   | 13 ++---
 .../org/apache/sis/console/AboutCommandTest.java   | 20 +++----
 .../org/apache/sis/console/CRSCommandTest.java     | 20 +++----
 .../org/apache/sis/console/CommandRunnerTest.java  | 55 ++++++++-----------
 .../org/apache/sis/console/HelpCommandTest.java    | 54 +++++++++---------
 .../apache/sis/console/MetadataCommandTest.java    | 14 ++---
 .../apache/sis/console/MimeTypeCommandTest.java    | 14 ++---
 .../org.apache.sis.storage/main/module-info.java   |  1 +
 .../main/org/apache/sis/io/stream/IOUtilities.java | 22 +++++++-
 21 files changed, 288 insertions(+), 199 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/AboutCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/AboutCommand.java
index abbb44a5e1..115fbec3b0 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/AboutCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/AboutCommand.java
@@ -36,18 +36,19 @@ import org.apache.sis.util.resources.Messages;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.util.internal.StandardDateFormat;
+import org.apache.sis.util.internal.X364;
 import org.apache.sis.system.Loggers;
 import org.apache.sis.system.Supervisor;
 import org.apache.sis.system.SupervisorMBean;
 import org.apache.sis.system.DataDirectory;
-import org.apache.sis.util.internal.StandardDateFormat;
-import org.apache.sis.util.internal.X364;
+import org.apache.sis.io.stream.IOUtilities;
 
 
 /**
  * The "about" subcommand.
  * By default this sub-command prints all information except the {@link 
About#LIBRARIES} section,
- * because the latter is considered too verbose. Available options are:
+ * because the latter is considered too verbose. Some available options are:
  *
  * <ul>
  *   <li>{@code --brief}:   prints only Apache SIS version number.</li>
@@ -69,7 +70,7 @@ final class AboutCommand extends CommandRunner {
      * @param  arguments     the command-line arguments provided by the user.
      * @throws InvalidOptionException if an illegal option has been provided, 
or the option has an illegal value.
      */
-    AboutCommand(final int commandIndex, final String... arguments) throws 
InvalidOptionException {
+    AboutCommand(final int commandIndex, final Object[] arguments) throws 
InvalidOptionException {
         super(commandIndex, arguments, EnumSet.of(Option.LOCALE, 
Option.TIMEZONE, Option.ENCODING,
                 Option.BRIEF, Option.VERBOSE, Option.HELP, Option.DEBUG));
     }
@@ -115,7 +116,10 @@ final class AboutCommand extends CommandRunner {
                  *
                  * Tutorial: 
http://docs.oracle.com/javase/tutorial/jmx/remote/custom.html
                  */
-                final String address = files.get(0);
+                final String address = IOUtilities.toString(files.get(0));
+                if (address == null) {
+                    return Command.INVALID_ARGUMENT_EXIT_CODE;
+                }
                 final String path = toRemoteURL(address);
                 final long time = System.nanoTime();
                 final TreeTable table;
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/CRSCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/CRSCommand.java
index 0b2238708e..6baf13a77d 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/CRSCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/CRSCommand.java
@@ -23,7 +23,12 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
 
 /**
  * The "crs" sub-command.
- * CRS are considered as a kind of metadata here.
+ * CRS are considered as a kind of metadata.
+ * Some available options are:
+ *
+ * <ul>
+ *   <li>{@code --format}: the output format (WKT or XML).</li>
+ * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -31,7 +36,7 @@ final class CRSCommand extends FormattedOutputCommand {
     /**
      * Creates the {@code "crs"} sub-command.
      */
-    CRSCommand(final int commandIndex, final String... args) throws 
InvalidOptionException {
+    CRSCommand(final int commandIndex, final Object[] args) throws 
InvalidOptionException {
         super(commandIndex, args, MetadataCommand.options(), OutputFormat.WKT, 
OutputFormat.XML);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Command.java 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Command.java
index adf3405508..5c1bde2fcd 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Command.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Command.java
@@ -124,26 +124,34 @@ public final class Command {
      * Creates a new command for the given arguments. The first value in the 
given array which is
      * not an option is taken as the command name. All other values are 
options or filenames.
      *
+     * <p>Arguments should be instances of {@link String}, except the 
arguments for input or output files
+     * which can be any types accepted by {@link 
org.apache.sis.storage.StorageConnector}. This includes,
+     * for example, {@link String}, {@link java.io.File}, {@link 
java.nio.file.Path}, {@link java.net.URL},
+     * <i>etc.</i></p>
+     *
      * @param  args  the command-line arguments.
      * @throws InvalidCommandException if an invalid command has been given.
      * @throws InvalidOptionException if the given arguments contain an 
invalid option.
      */
-    protected Command(final String[] args) throws InvalidCommandException, 
InvalidOptionException {
+    protected Command(final Object[] args) throws InvalidCommandException, 
InvalidOptionException {
         int commandIndex = -1;
         String commandName = null;
         for (int i=0; i<args.length; i++) {
-            final String arg = args[i];
-            if (arg.startsWith(Option.PREFIX)) {
-                final String name = arg.substring(Option.PREFIX.length());
-                final Option option = Option.forLabel(name);
-                if (option.hasValue) {
-                    i++;                        // Skip the next argument.
+            final Object arg = args[i];
+            if (arg instanceof CharSequence) {
+                final String s = arg.toString();
+                if (s.startsWith(Option.PREFIX)) {
+                    final String name = s.substring(Option.PREFIX.length());
+                    final Option option = Option.forLabel(name);
+                    if (option.hasValue) {
+                        i++;                        // Skip the next argument.
+                    }
+                } else {
+                    // Takes the first non-argument option as the command name.
+                    commandName  = s;
+                    commandIndex = i;
+                    break;
                 }
-            } else {
-                // Takes the first non-argument option as the command name.
-                commandName = arg;
-                commandIndex = i;
-                break;
             }
         }
         if (commandName == null) {
@@ -182,7 +190,9 @@ public final class Command {
         if (command.options.containsKey(Option.HELP)) {
             command.help(command.commandName.toLowerCase(Locale.US));
         } else try {
-            return command.run();
+            int status = command.run();
+            command.flush();
+            return status;
         } catch (Exception e) {
             command.error(null, e);
             throw e;
@@ -280,7 +290,6 @@ public final class Command {
         } catch (Exception e) {
             status = exitCodeFor(e);
         }
-        c.command.flush();
         if (status != 0) {
             System.exit(status);
         }
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/CommandRunner.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/CommandRunner.java
index e76c930c70..44bdc42425 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/CommandRunner.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/CommandRunner.java
@@ -72,8 +72,10 @@ abstract class CommandRunner {
 
     /**
      * The command-line options allowed by this sub-command, together with 
their values.
+     * Values are usually instances of {@link String}, but other types are 
allowed when
+     * the values is expected to be a file.
      */
-    protected final EnumMap<Option,String> options;
+    protected final EnumMap<Option,Object> options;
 
     /**
      * The locale specified by the {@code "--locale"} option. If no such 
option was provided,
@@ -128,8 +130,10 @@ abstract class CommandRunner {
     /**
      * Any remaining parameters that are not command name or option.
      * They are typically file names, but can occasionally be other types like 
URL.
+     * Values are always instances of {@link String} when SIS is executed from 
bash,
+     * but may be other kinds of object when {@link SIS} is executed from 
JShell.
      */
-    protected final List<String> files;
+    protected final List<Object> files;
 
     /**
      * Copies the configuration of the given sub-command. This constructor is 
used
@@ -162,10 +166,10 @@ abstract class CommandRunner {
      * @throws InvalidOptionException if an illegal option has been provided, 
or the option has an illegal value.
      */
     @SuppressWarnings("UseOfSystemOutOrSystemErr")
-    protected CommandRunner(final int commandIndex, final String[] arguments, 
final EnumSet<Option> validOptions)
+    protected CommandRunner(final int commandIndex, final Object[] arguments, 
final EnumSet<Option> validOptions)
             throws InvalidOptionException
     {
-        commandName = (commandIndex >= 0) ? arguments[commandIndex] : null;
+        commandName = (commandIndex >= 0) ? arguments[commandIndex].toString() 
: null;
         this.validOptions = validOptions;
         options = new EnumMap<>(Option.class);
         files = new ArrayList<>(arguments.length);
@@ -173,14 +177,15 @@ abstract class CommandRunner {
             if (i == commandIndex) {
                 continue;
             }
-            final String arg = arguments[i];
-            if (arg.startsWith(Option.PREFIX)) {
-                final String name = arg.substring(Option.PREFIX.length());
+            final Object arg = arguments[i];
+            final String s;
+            if (arg instanceof CharSequence && (s = 
arg.toString()).startsWith(Option.PREFIX)) {
+                final String name = s.substring(Option.PREFIX.length());
                 final Option option = Option.forLabel(name);
                 if (!validOptions.contains(option)) {
                     throw new 
InvalidOptionException(Errors.format(Errors.Keys.UnknownOption_1, name), name);
                 }
-                String value = null;
+                Object value = null;
                 if (option.hasValue) {
                     if (++i >= arguments.length) {
                         throw new 
InvalidOptionException(Errors.format(Errors.Keys.MissingValueForOption_1, 
name), name);
@@ -199,21 +204,22 @@ abstract class CommandRunner {
          * Process the --locale, --encoding and --colors options.
          */
         Option option = null;                                           // In 
case of IllegalArgumentException.
-        String value  = null;
+        Object value  = null;
         final Console console;
         final boolean explicitEncoding;
         try {
             debug = options.containsKey(option = Option.DEBUG);
 
-            value = options.get(option = Option.LOCALE);
-            locale = (value != null) ? Locales.parse(value) : 
Locale.getDefault(Locale.Category.DISPLAY);
+            String s;
+            value = s = getOptionAsString(option = Option.LOCALE);
+            locale = (s != null) ? Locales.parse(s) : 
Locale.getDefault(Locale.Category.DISPLAY);
 
-            value = options.get(option = Option.TIMEZONE);
-            timezone = (value != null) ? TimeZone.getTimeZone(value) : 
TimeZone.getDefault();
+            value = s = getOptionAsString(option = Option.TIMEZONE);
+            timezone = (s != null) ? TimeZone.getTimeZone(s) : 
TimeZone.getDefault();
 
-            value = options.get(option = Option.ENCODING);
-            explicitEncoding = (value != null);
-            encoding = explicitEncoding ? Charset.forName(value) : 
Charset.defaultCharset();
+            value = s = getOptionAsString(option = Option.ENCODING);
+            explicitEncoding = (s != null);
+            encoding = explicitEncoding ? Charset.forName(s) : 
Charset.defaultCharset();
 
             value = options.get(option = Option.COLORS);
             console = System.console();
@@ -248,6 +254,23 @@ abstract class CommandRunner {
         }
     }
 
+    /**
+     * Returns the value of the specified option as a character string.
+     *
+     * @param  key  the option for which to get a value.
+     * @return the requested option, or {@code null} if not present.
+     * @throws InvalidOptionException if the value is not a character string.
+     */
+    final String getOptionAsString(final Option key) throws 
InvalidOptionException {
+        final Object value = options.get(key);
+        if (value == null) return null;
+        if (value instanceof CharSequence) {
+            return value.toString();
+        }
+        final String name = key.label();
+        throw new 
InvalidOptionException(Errors.format(Errors.Keys.IllegalOptionValue_2, name, 
value), name);
+    }
+
     /**
      * Return the value of a mandatory option.
      *
@@ -255,8 +278,8 @@ abstract class CommandRunner {
      * @return the option value, never {@code null}.
      * @throws InvalidOptionException if the option is missing.
      */
-    final String getMandatoryOption(final Option option) throws 
InvalidOptionException {
-        final String value = options.get(option);
+    final Object getMandatoryOption(final Option option) throws 
InvalidOptionException {
+        final Object value = options.get(option);
         if (value == null) {
             final String name = option.label();
             throw new 
InvalidOptionException(Errors.format(Errors.Keys.MissingValueForOption_1, 
name), name);
@@ -341,6 +364,7 @@ abstract class CommandRunner {
         } else {
             err.println(Exceptions.formatChainedMessages(locale, message, e));
         }
+        err.flush();
     }
 
     /**
@@ -365,8 +389,8 @@ abstract class CommandRunner {
     public abstract int run() throws Exception;
 
     /**
-     * Invoked before to exit the JVM for flushing and pending information to 
the output streams.
-     * The default information flushed {@link #out} and {@link #err} in that 
order.
+     * Flushes any pending information to the output streams.
+     * The default information flushes {@link #out} and {@link #err} in that 
order.
      * Subclasses may override if there is more things to flush.
      */
     protected void flush() {
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/FormattedOutputCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/FormattedOutputCommand.java
index fab233f71c..70c1ceb5a9 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/FormattedOutputCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/FormattedOutputCommand.java
@@ -95,7 +95,7 @@ abstract class FormattedOutputCommand extends CommandRunner {
      * @param  supportedFormats  the output formats to accept. The first 
format is the default one.
      * @throws InvalidOptionException if an illegal option has been provided, 
or the option has an illegal value.
      */
-    FormattedOutputCommand(final int commandIndex, final String[] arguments, 
final EnumSet<Option> validOptions,
+    FormattedOutputCommand(final int commandIndex, final Object[] arguments, 
final EnumSet<Option> validOptions,
             final OutputFormat... supportedFormats) throws 
InvalidOptionException
     {
         super(commandIndex, arguments, validOptions);
@@ -104,7 +104,7 @@ abstract class FormattedOutputCommand extends CommandRunner 
{
          * Output format can be either "text" (the default) or "xml".
          * In the case of "crs" sub-command, we accept also WKT variants.
          */
-        final String format = options.get(Option.FORMAT);
+        final String format = getOptionAsString(Option.FORMAT);
         if (format == null) {
             outputFormat = supportedFormats[0];
             convention   = Convention.WKT2_SIMPLIFIED;
@@ -190,14 +190,16 @@ abstract class FormattedOutputCommand extends 
CommandRunner {
             hasUnexpectedFileCount = true;
             return null;
         } else {
-            final String file = files.get(0);
-            if (CodeType.guess(file).isCRS) {
-                return CRS.forCode(file);
-            } else {
-                try (DataStore store = DataStores.open(file)) {
-                    return store.getMetadata();
+            final Object file = files.get(0);
+            if (file instanceof CharSequence) {
+                final String c = file.toString();
+                if (CodeType.guess(c).isCRS) {
+                    return CRS.forCode(c);
                 }
             }
+            try (DataStore store = DataStores.open(file)) {
+                return store.getMetadata();
+            }
         }
     }
 
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/HelpCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/HelpCommand.java
index bbcee10ed0..f374e83e01 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/HelpCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/HelpCommand.java
@@ -25,6 +25,7 @@ import org.apache.sis.util.resources.Vocabulary;
 
 /**
  * The "help" subcommand.
+ * This sub-command prints the same text than when {@code SIS} is invoked on 
the command-line without arguments.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -58,7 +59,7 @@ final class HelpCommand extends CommandRunner {
      * @param  arguments     the command-line arguments provided by the user.
      * @throws InvalidOptionException if an illegal option has been provided, 
or the option has an illegal value.
      */
-    HelpCommand(final int commandIndex, final String... arguments) throws 
InvalidOptionException {
+    HelpCommand(final int commandIndex, final Object[] arguments) throws 
InvalidOptionException {
         super(commandIndex, arguments, EnumSet.of(Option.LOCALE, 
Option.ENCODING, Option.HELP, Option.DEBUG));
     }
 
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/IdentifierCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/IdentifierCommand.java
index 6e0313be3d..60ad797a9a 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/IdentifierCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/IdentifierCommand.java
@@ -39,6 +39,11 @@ import org.apache.sis.util.resources.Vocabulary;
 
 /**
  * The "identifier" sub-command.
+ * Some available options are:
+ *
+ * <ul>
+ *   <li>{@code --format}: the output format (text).</li>
+ * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -100,7 +105,7 @@ final class IdentifierCommand extends 
FormattedOutputCommand {
     /**
      * Creates the {@code "identifier"} sub-command.
      */
-    IdentifierCommand(final int commandIndex, final String... args) throws 
InvalidOptionException {
+    IdentifierCommand(final int commandIndex, final Object[] args) throws 
InvalidOptionException {
         super(commandIndex, args, options(), OutputFormat.TEXT);
     }
 
@@ -126,7 +131,12 @@ final class IdentifierCommand extends 
FormattedOutputCommand {
                 final Identifier id = ((Metadata) 
metadata).getMetadataIdentifier();
                 if (id != null) {
                     CharSequence desc = id.getDescription();
-                    if (desc != null && !files.isEmpty()) desc = files.get(0);
+                    if (desc == null && !files.isEmpty()) {
+                        final Object c = files.get(0);
+                        if (c instanceof CharSequence) {
+                            desc = c.toString();
+                        }
+                    }
                     rows.add(new Row(State.VALID, 
IdentifiedObjects.toString(id), desc));
                 }
                 for (final ReferenceSystem rs : ((Metadata) 
metadata).getReferenceSystemInfo()) {
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/InfoCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/InfoCommand.java
index 4ed829c9b0..b09ab42c2f 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/InfoCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/InfoCommand.java
@@ -63,7 +63,7 @@ final class InfoCommand extends FormattedOutputCommand {
      * @param  arguments     the command-line arguments provided by the user.
      * @throws InvalidOptionException if an illegal option has been provided, 
or the option has an illegal value.
      */
-    InfoCommand(final int commandIndex, final String... arguments) throws 
InvalidOptionException {
+    InfoCommand(final int commandIndex, final Object[] arguments) throws 
InvalidOptionException {
         super(commandIndex, arguments, options(), OutputFormat.TEXT);
         gridBitMask = GridGeometry.EXTENT | GridGeometry.GEOGRAPHIC_EXTENT | 
GridGeometry.TEMPORAL_EXTENT
                     | GridGeometry.CRS | GridGeometry.RESOLUTION;
@@ -80,15 +80,13 @@ final class InfoCommand extends FormattedOutputCommand {
     @Override
     public int run() throws Exception {
         final Object input;
-        final String name;
         if (useStandardInput()) {
             input = System.in;
-            name  = "stdin";
         } else {
             if (hasUnexpectedFileCount = hasUnexpectedFileCount(1, 1)) {
                 return Command.INVALID_ARGUMENT_EXIT_CODE;
             }
-            input = name = files.get(0);
+            input = files.get(0);
         }
         final var tree = new DefaultTreeTable(TableColumn.VALUE_AS_TEXT);
         try (DataStore store = DataStores.open(input)) {
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/MetadataCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/MetadataCommand.java
index 61c64bbaad..b12a6b4010 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/MetadataCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/MetadataCommand.java
@@ -28,7 +28,12 @@ import org.apache.sis.util.collection.TreeTable;
 
 /**
  * The "metadata" sub-command.
- * This command shows ISO 19115 metadata for the content of a file.
+ * This sub-command shows ISO 19115 metadata for the content of a file.
+ * Some available options are:
+ *
+ * <ul>
+ *   <li>{@code --format}: the output format (text, XML or GPX).</li>
+ * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -48,7 +53,7 @@ final class MetadataCommand extends FormattedOutputCommand {
      * @param  arguments     the command-line arguments provided by the user.
      * @throws InvalidOptionException if an illegal option has been provided, 
or the option has an illegal value.
      */
-    MetadataCommand(final int commandIndex, final String... arguments) throws 
InvalidOptionException {
+    MetadataCommand(final int commandIndex, final Object[] arguments) throws 
InvalidOptionException {
         super(commandIndex, arguments, options(), OutputFormat.TEXT, 
OutputFormat.XML, OutputFormat.GPX);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/MimeTypeCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/MimeTypeCommand.java
index 6119c92b93..5ee89f7c6e 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/MimeTypeCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/MimeTypeCommand.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.console;
 
+import java.util.Arrays;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.EnumSet;
@@ -26,6 +27,7 @@ import java.nio.file.FileSystemNotFoundException;
 import org.apache.sis.storage.DataStores;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.io.stream.IOUtilities;
 
 
 /**
@@ -50,7 +52,7 @@ final class MimeTypeCommand extends CommandRunner {
      * @param  arguments     the command-line arguments provided by the user.
      * @throws InvalidOptionException if an illegal option has been provided, 
or the option has an illegal value.
      */
-    MimeTypeCommand(final int commandIndex, final String... arguments) throws 
InvalidOptionException {
+    MimeTypeCommand(final int commandIndex, final Object[] arguments) throws 
InvalidOptionException {
         super(commandIndex, arguments, EnumSet.of(Option.ENCODING, 
Option.HELP, Option.DEBUG));
     }
 
@@ -66,28 +68,24 @@ final class MimeTypeCommand extends CommandRunner {
             return Command.INVALID_ARGUMENT_EXIT_CODE;
         }
         /*
-         * Computes the width of the first column, which will contain file 
names.
+         * Computes the width of the first column, which will contain the URIs.
          */
-        int width = 0;
-        for (final String file : files) {
-            final int length = file.length() + 1;
-            if (length > width) {
-                width = length;
-            }
-        }
+        final String[] names = 
files.stream().map(IOUtilities::toString).toArray(String[]::new);
+        final int width = 
Arrays.stream(names).mapToInt(String::length).max().orElse(0) + 1;
         /*
          * Now detect and print MIME type.
          */
-        for (final String file : files) {
+        for (int i=0; i<names.length; i++) {
+            final Object file = files.get(i);
             final URI uri;
             try {
-                uri = new URI(file);
+                uri = IOUtilities.toURI(file);
             } catch (URISyntaxException e) {
                 canNotOpen(0, e);
                 return Command.IO_EXCEPTION_EXIT_CODE;
             }
             String type;
-            if (!uri.isAbsolute()) {
+            if (uri == null || !uri.isAbsolute()) {
                 /*
                  * If the URI is not absolute, we will not be able to convert 
to Path.
                  * Open as a String, leaving the conversion to DataStore 
implementations.
@@ -107,9 +105,10 @@ final class MimeTypeCommand extends CommandRunner {
              *   file: type
              */
             if (type != null) {
-                out.print(file);
+                final String name = names[i];
+                out.print(name);
                 out.print(':');
-                out.print(CharSequences.spaces(width - file.length()));
+                out.print(CharSequences.spaces(width - name.length()));
                 out.println(type);
                 out.flush();
             }
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Option.java 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Option.java
index 2e9f7537d9..90dd4ca0e2 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Option.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Option.java
@@ -99,7 +99,7 @@ enum Option {
      * Boolean values accepted on the command line. Values at even indices are 
{@code false}
      * and values at odd indices are {@code true}.
      *
-     * @see #parseBoolean(String)
+     * @see #parseBoolean(Object)
      */
     private static final String[] BOOLEAN_VALUES = {
         "false", "true",
@@ -166,13 +166,17 @@ enum Option {
      * @return the value as a boolean.
      * @throws InvalidOptionException if the given value is not recognized as 
a boolean.
      */
-    boolean parseBoolean(final String value) throws InvalidOptionException {
+    boolean parseBoolean(final Object value) throws InvalidOptionException {
+        if (value instanceof Boolean) {
+            return (Boolean) value;
+        }
+        final String s = value.toString();
         for (int i=0; i<BOOLEAN_VALUES.length; i++) {
-            if (value.equalsIgnoreCase(BOOLEAN_VALUES[i])) {
+            if (s.equalsIgnoreCase(BOOLEAN_VALUES[i])) {
                 return (i & 1) != 0;
             }
         }
-        final String name = name().toLowerCase(Locale.US);
+        final String name = label();
         throw new 
InvalidOptionException(Errors.format(Errors.Keys.IllegalOptionValue_2, name, 
value), name);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TransformCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TransformCommand.java
index 2c243135c5..ea45f0ee88 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TransformCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TransformCommand.java
@@ -22,9 +22,10 @@ import java.util.Collection;
 import java.util.EnumSet;
 import java.util.Locale;
 import java.io.IOException;
-import java.io.FileInputStream;
 import java.io.LineNumberReader;
 import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.BufferedReader;
 import java.text.NumberFormat;
 import static java.util.logging.Logger.getLogger;
 import javax.measure.Unit;
@@ -57,12 +58,14 @@ import org.apache.sis.referencing.util.ReferencingUtilities;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStores;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.base.CodeType;
 import org.apache.sis.system.Modules;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.internal.X364;
 import org.apache.sis.io.LineAppender;
 import org.apache.sis.io.TableAppender;
+import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.io.wkt.Colors;
 import org.apache.sis.io.wkt.Transliterator;
 import org.apache.sis.io.wkt.WKTFormat;
@@ -74,6 +77,7 @@ import 
org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.setup.OptionKey;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.referencing.ObjectDomain;
@@ -82,6 +86,13 @@ import org.opengis.referencing.ObjectDomain;
 /**
  * The "transform" subcommand.
  * The output is a comma separated values (CSV) file, with {@code '#'} as the 
first character of comment lines.
+ * The source and target CRS are mandatory and can be specified as EPSG codes, 
WKT strings, or read from data files.
+ * Those information are passed as options:
+ *
+ * <ul>
+ *   <li>{@code --sourceCRS}: the coordinate reference system of input 
points.</li>
+ *   <li>{@code --targetCRS}: the coordinate reference system of output 
points.</li>
+ * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -147,7 +158,7 @@ final class TransformCommand extends FormattedOutputCommand 
{
     /**
      * Creates the {@code "transform"} sub-command.
      */
-    TransformCommand(final int commandIndex, final String... args) throws 
InvalidOptionException {
+    TransformCommand(final int commandIndex, final Object[] args) throws 
InvalidOptionException {
         super(commandIndex, args, options(), OutputFormat.WKT, 
OutputFormat.TEXT);
         resources = Vocabulary.getResources(locale);
     }
@@ -161,26 +172,28 @@ final class TransformCommand extends 
FormattedOutputCommand {
      * @throws FactoryException if the operation failed for another reason.
      */
     private CoordinateReferenceSystem fetchCRS(final Option option) throws 
InvalidOptionException, FactoryException, DataStoreException {
-        final String identifier = getMandatoryOption(option);
-        if (CodeType.guess(identifier).isCRS) try {
-            return CRS.forCode(identifier);
-        } catch (NoSuchAuthorityCodeException e) {
-            final String name = option.label();
-            throw new 
InvalidOptionException(Errors.format(Errors.Keys.IllegalOptionValue_2, name, 
identifier), e, name);
-        } else {
-            final Metadata metadata;
-            try (DataStore store = DataStores.open(identifier)) {
-                metadata = store.getMetadata();
+        final Object identifier = getMandatoryOption(option);
+        if (identifier instanceof CharSequence) {
+            final String c = identifier.toString();
+            if (CodeType.guess(c).isCRS) try {
+                return CRS.forCode(c);
+            } catch (NoSuchAuthorityCodeException e) {
+                final String name = option.label();
+                throw new 
InvalidOptionException(Errors.format(Errors.Keys.IllegalOptionValue_2, name, 
identifier), e, name);
             }
-            if (metadata != null) {
-                for (final ReferenceSystem rs : 
metadata.getReferenceSystemInfo()) {
-                    if (rs instanceof CoordinateReferenceSystem) {
-                        return (CoordinateReferenceSystem) rs;
-                    }
+        }
+        final Metadata metadata;
+        try (DataStore store = DataStores.open(identifier)) {
+            metadata = store.getMetadata();
+        }
+        if (metadata != null) {
+            for (final ReferenceSystem rs : metadata.getReferenceSystemInfo()) 
{
+                if (rs instanceof CoordinateReferenceSystem) {
+                    return (CoordinateReferenceSystem) rs;
                 }
             }
-            throw new 
InvalidOptionException(Errors.format(Errors.Keys.UnspecifiedCRS), 
option.label());
         }
+        throw new 
InvalidOptionException(Errors.format(Errors.Keys.UnspecifiedCRS), 
option.label());
     }
 
     /**
@@ -205,8 +218,10 @@ final class TransformCommand extends 
FormattedOutputCommand {
                     points = readCoordinates(in, "stdin");
                 }
             } else {
-                for (final String file : files) {
-                    try (LineNumberReader in = new LineNumberReader(new 
InputStreamReader(new FileInputStream(file), encoding))) {
+                for (final Object file : files) {
+                    final var c = new StorageConnector(file);
+                    c.setOption(OptionKey.ENCODING, encoding);
+                    try (BufferedReader in = 
IOUtilities.toBuffered(c.commit(Reader.class, "transform"))) {
                         points = readCoordinates(in, file);
                     }
                 }
@@ -518,7 +533,7 @@ final class TransformCommand extends FormattedOutputCommand 
{
      * @param  filename  the filename, for error reporting only.
      * @return the coordinate values.
      */
-    private List<double[]> readCoordinates(final LineNumberReader in, final 
String filename) throws IOException {
+    private List<double[]> readCoordinates(final BufferedReader in, Object 
filename) throws IOException {
         final List<double[]> points = new ArrayList<>();
         try {
             String line;
@@ -529,7 +544,9 @@ final class TransformCommand extends FormattedOutputCommand 
{
                 }
             }
         } catch (NumberFormatException e) {
-            errorMessage = Errors.format(Errors.Keys.ErrorInFileAtLine_2, 
filename, in.getLineNumber());
+            if ((filename = IOUtilities.toString(filename)) == null) filename 
= "?";
+            Object line = (in instanceof LineNumberReader) ? 
((LineNumberReader) in).getLineNumber() : "?";
+            errorMessage = Errors.format(Errors.Keys.ErrorInFileAtLine_2, 
filename, line);
             errorCause = e;
         }
         return points;
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TranslateCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TranslateCommand.java
index 0c49519a3d..38864f912e 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TranslateCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TranslateCommand.java
@@ -17,7 +17,6 @@
 package org.apache.sis.console;
 
 import java.util.EnumSet;
-import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import org.apache.sis.setup.OptionKey;
 import org.apache.sis.storage.StorageConnector;
@@ -35,7 +34,7 @@ import org.apache.sis.util.resources.Errors;
 
 /**
  * The "translate" sub-command.
- * This command reads resources and rewrites them in another format.
+ * This sub-command reads resources and rewrites them in another format.
  * If more than one source file is specified, then all those files are 
aggregated in the output file.
  * This is possible only if the output format supports the storage of an 
arbitrary number of resources.
  *
@@ -49,7 +48,7 @@ final class TranslateCommand extends CommandRunner {
      * @param  arguments     the command-line arguments provided by the user.
      * @throws InvalidOptionException if an illegal option has been provided, 
or the option has an illegal value.
      */
-    TranslateCommand(final int commandIndex, final String... arguments) throws 
InvalidOptionException {
+    TranslateCommand(final int commandIndex, final Object[] arguments) throws 
InvalidOptionException {
         super(commandIndex, arguments, EnumSet.of(Option.OUTPUT, 
Option.FORMAT, Option.HELP, Option.DEBUG));
     }
 
@@ -64,15 +63,15 @@ final class TranslateCommand extends CommandRunner {
         if (hasUnexpectedFileCount(1, Integer.MAX_VALUE)) {
             return Command.INVALID_ARGUMENT_EXIT_CODE;
         }
-        final String output = getMandatoryOption(Option.OUTPUT);
-        final String format = options.get(Option.FORMAT);
-        final var connector = new StorageConnector(Path.of(output));
+        final Object output = getMandatoryOption(Option.OUTPUT);
+        final String format = getOptionAsString(Option.FORMAT);
+        final var connector = new StorageConnector(output);
         connector.setOption(OptionKey.OPEN_OPTIONS, new StandardOpenOption[] {
             StandardOpenOption.CREATE_NEW,
             StandardOpenOption.WRITE
         });
         try (DataStore target = DataStores.openWritable(connector, format)) {
-            for (final String file : files) {
+            for (final Object file : files) {
                 try (DataStore source = DataStores.open(file)) {
                     if (target instanceof WritableAggregate) {
                         ((WritableAggregate) target).add(source);
diff --git 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/AboutCommandTest.java
 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/AboutCommandTest.java
index f1ff81f4ba..18d7eca1b7 100644
--- 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/AboutCommandTest.java
+++ 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/AboutCommandTest.java
@@ -21,7 +21,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.DependsOn;
 import org.apache.sis.test.TestCase;
 import static org.apache.sis.test.TestUtilities.getSingleton;
@@ -47,7 +47,7 @@ public final class AboutCommandTest extends TestCase {
      */
     @Test
     public void testDefault() throws Exception {
-        final AboutCommand test = new AboutCommand(0, CommandRunner.TEST);
+        final AboutCommand test = new AboutCommand(0, new String[] 
{CommandRunner.TEST});
         test.run();
         verify(test.outputBuffer.toString());
     }
@@ -57,16 +57,16 @@ public final class AboutCommandTest extends TestCase {
      */
     private static void verify(final String result) {
         String expected = Version.SIS.toString();
-        assertTrue(expected, result.contains(expected));
+        assertTrue(result.contains(expected), expected);
 
         expected = System.getProperty("java.version");
-        assertTrue(expected, result.contains(expected));
+        assertTrue(result.contains(expected), expected);
 
         expected = System.getProperty("os.name");
-        assertTrue(expected, result.contains(expected));
+        assertTrue(result.contains(expected), expected);
 
         expected = System.getProperty("user.home");
-        assertTrue(expected, result.contains(expected));
+        assertTrue(result.contains(expected), expected);
     }
 
     /**
@@ -76,10 +76,10 @@ public final class AboutCommandTest extends TestCase {
      */
     @Test
     public void testBrief() throws Exception {
-        final AboutCommand test = new AboutCommand(0, CommandRunner.TEST, 
"--brief");
+        var test = new AboutCommand(0, new String[] {CommandRunner.TEST, 
"--brief"});
         test.run();
-        final String result = 
getSingleton(CharSequences.splitOnEOL(test.outputBuffer.toString().trim())).toString();
-        assertTrue(result, result.contains(Version.SIS.toString()));
+        String result = 
getSingleton(CharSequences.splitOnEOL(test.outputBuffer.toString().trim())).toString();
+        assertTrue(result.contains(Version.SIS.toString()), result);
     }
 
     /**
@@ -91,7 +91,7 @@ public final class AboutCommandTest extends TestCase {
      */
     @Test
     public void testVerbose() throws Exception {
-        final AboutCommand test = new AboutCommand(0, CommandRunner.TEST, 
"--verbose");
+        var test = new AboutCommand(0, new String[] {CommandRunner.TEST, 
"--verbose"});
         test.run();
         verify(test.outputBuffer.toString());
     }
diff --git 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/CRSCommandTest.java
 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/CRSCommandTest.java
index e64ca632b1..7eeea70fa0 100644
--- 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/CRSCommandTest.java
+++ 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/CRSCommandTest.java
@@ -20,7 +20,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.DependsOn;
 import org.apache.sis.test.TestCase;
@@ -73,10 +73,10 @@ public final class CRSCommandTest extends TestCase {
      */
     @Test
     public void testCode() throws Exception {
-        final CRSCommand test = new CRSCommand(0, CommandRunner.TEST, 
"EPSG:4326");
+        var test = new CRSCommand(0, new String[] {CommandRunner.TEST, 
"EPSG:4326"});
         test.run();
-        final String wkt = test.outputBuffer.toString();
-        assertTrue(wkt, wkt.matches(WGS84));
+        String wkt = test.outputBuffer.toString();
+        assertTrue(wkt.matches(WGS84), wkt);
     }
 
     /**
@@ -87,10 +87,10 @@ public final class CRSCommandTest extends TestCase {
     @Test
     @DependsOnMethod("testCode")
     public void testURN() throws Exception {
-        final CRSCommand test = new CRSCommand(0, CommandRunner.TEST, 
"urn:ogc:def:crs:epsg::4326");
+        var test = new CRSCommand(0, new String[] {CommandRunner.TEST, 
"urn:ogc:def:crs:epsg::4326"});
         test.run();
-        final String wkt = test.outputBuffer.toString();
-        assertTrue(wkt, wkt.matches(WGS84));
+        String wkt = test.outputBuffer.toString();
+        assertTrue(wkt.matches(WGS84), wkt);
     }
 
     /**
@@ -101,9 +101,9 @@ public final class CRSCommandTest extends TestCase {
     @Test
     @DependsOnMethod("testURN")
     public void testHTTP() throws Exception {
-        final CRSCommand test = new CRSCommand(0, CommandRunner.TEST, 
"http://www.opengis.net/gml/srs/epsg.xml#4326";);
+        var test = new CRSCommand(0, new String[] {CommandRunner.TEST, 
"http://www.opengis.net/gml/srs/epsg.xml#4326"});
         test.run();
-        final String wkt = test.outputBuffer.toString();
-        assertTrue(wkt, wkt.matches(WGS84));
+        String wkt = test.outputBuffer.toString();
+        assertTrue(wkt.matches(WGS84), wkt);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/CommandRunnerTest.java
 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/CommandRunnerTest.java
index 25afc306d2..c8286f0aa2 100644
--- 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/CommandRunnerTest.java
+++ 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/CommandRunnerTest.java
@@ -24,7 +24,7 @@ import java.nio.charset.StandardCharsets;
 
 // 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;
 import static org.apache.sis.test.TestUtilities.getSingleton;
@@ -74,8 +74,8 @@ public final class CommandRunnerTest extends TestCase {
     public void testLocale() throws InvalidOptionException {
         final CommandRunner c = new Dummy(EnumSet.allOf(Option.class), 
CommandRunner.TEST, "--locale", "ja");
         assertEquals(Option.LOCALE, getSingleton(c.options.keySet()));
-        assertSame("locale", Locale.JAPANESE, c.locale);
-        assertTrue("files.isEmpty()", c.files.isEmpty());
+        assertSame(Locale.JAPANESE, c.locale, "locale");
+        assertTrue(c.files.isEmpty(), "files.isEmpty()");
     }
 
     /**
@@ -87,9 +87,9 @@ public final class CommandRunnerTest extends TestCase {
     public void testTimeZone() throws InvalidOptionException {
         final CommandRunner c = new Dummy(EnumSet.allOf(Option.class), 
CommandRunner.TEST, "--timezone", "JST");
         assertEquals(Option.TIMEZONE, getSingleton(c.options.keySet()));
-        assertEquals("timezone", TimeZone.getTimeZone("JST"), c.timezone);
-        assertEquals("rawoffset", TimeUnit.HOURS.toMillis(9), 
c.timezone.getRawOffset());
-        assertTrue("files.isEmpty()", c.files.isEmpty());
+        assertEquals(TimeZone.getTimeZone("JST"), c.timezone, "timezone");
+        assertEquals(TimeUnit.HOURS.toMillis(9), c.timezone.getRawOffset(), 
"rawoffset");
+        assertTrue(c.files.isEmpty(), "files.isEmpty()");
     }
 
     /**
@@ -101,8 +101,8 @@ public final class CommandRunnerTest extends TestCase {
     public void testEncoding() throws InvalidOptionException {
         final CommandRunner c = new Dummy(EnumSet.allOf(Option.class), 
CommandRunner.TEST, "--encoding", "UTF-16");
         assertEquals(Option.ENCODING, getSingleton(c.options.keySet()));
-        assertEquals("encoding", StandardCharsets.UTF_16, c.encoding);
-        assertTrue("files.isEmpty()", c.files.isEmpty());
+        assertEquals(StandardCharsets.UTF_16, c.encoding, "encoding");
+        assertTrue(c.files.isEmpty(), "files.isEmpty()");
     }
 
     /**
@@ -115,13 +115,12 @@ public final class CommandRunnerTest extends TestCase {
     public void testOptionMix() throws InvalidOptionException {
         final CommandRunner c = new Dummy(EnumSet.allOf(Option.class), 
CommandRunner.TEST,
                 "--brief", "--locale", "ja", "--verbose", "--timezone", "JST");
-        assertEquals("options", EnumSet.of(
-                Option.BRIEF, Option.LOCALE, Option.VERBOSE, Option.TIMEZONE), 
c.options.keySet());
+        assertEquals(EnumSet.of(Option.BRIEF, Option.LOCALE, Option.VERBOSE, 
Option.TIMEZONE), c.options.keySet(), "options");
 
         // Test specific values.
-        assertSame  ("locale",   Locale.JAPANESE,             c.locale);
-        assertEquals("timezone", TimeZone.getTimeZone("JST"), c.timezone);
-        assertTrue("files.isEmpty()", c.files.isEmpty());
+        assertSame(Locale.JAPANESE, c.locale, "locale");
+        assertEquals(TimeZone.getTimeZone("JST"), c.timezone, "timezone");
+        assertTrue(c.files.isEmpty(), "files.isEmpty()");
     }
 
     /**
@@ -135,13 +134,10 @@ public final class CommandRunnerTest extends TestCase {
     public void testMissingOptionValue() throws InvalidOptionException {
         final CommandRunner c = new Dummy(EnumSet.allOf(Option.class), 
CommandRunner.TEST, "--brief"); // Should not comply.
         assertEquals(Option.BRIEF, getSingleton(c.options.keySet()));
-        try {
-            new Dummy(EnumSet.allOf(Option.class), CommandRunner.TEST, 
"--brief", "--locale");
-            fail("Expected InvalidOptionException");
-        } catch (InvalidOptionException e) {
-            final String message = e.getMessage();
-            assertTrue(message.contains("locale"));
-        }
+        String message = assertThrows(InvalidOptionException.class,
+                () -> new Dummy(EnumSet.allOf(Option.class), 
CommandRunner.TEST, "--brief", "--locale"))
+                .getMessage();
+        assertTrue(message.contains("locale"));
     }
 
     /**
@@ -151,13 +147,10 @@ public final class CommandRunnerTest extends TestCase {
      */
     @Test
     public void testUnexpectedOption() throws InvalidOptionException {
-        try {
-            new Dummy(EnumSet.of(Option.HELP, Option.BRIEF), 
CommandRunner.TEST, "--brief", "--verbose", "--help");
-            fail("Expected InvalidOptionException");
-        } catch (InvalidOptionException e) {
-            final String message = e.getMessage();
-            assertTrue(message.contains("verbose"));
-        }
+        String message = assertThrows(InvalidOptionException.class,
+                () -> new Dummy(EnumSet.of(Option.HELP, Option.BRIEF), 
CommandRunner.TEST, "--brief", "--verbose", "--help"))
+                .getMessage();
+        assertTrue(message.contains("verbose"));
     }
 
     /**
@@ -167,9 +160,9 @@ public final class CommandRunnerTest extends TestCase {
      */
     @Test
     public void testHasContradictoryOptions() throws InvalidOptionException {
-        final CommandRunner c = new Dummy(EnumSet.allOf(Option.class), 
CommandRunner.TEST, "--brief", "--verbose");
+        final var c = new Dummy(EnumSet.allOf(Option.class), 
CommandRunner.TEST, "--brief", "--verbose");
         assertTrue(c.hasContradictoryOptions(Option.BRIEF, Option.VERBOSE));
-        final String message = c.outputBuffer.toString();
+        String message = c.outputBuffer.toString();
         assertTrue(message.contains("brief"));
         assertTrue(message.contains("verbose"));
     }
@@ -181,13 +174,13 @@ public final class CommandRunnerTest extends TestCase {
      */
     @Test
     public void testHasUnexpectedFileCount() throws InvalidOptionException {
-        final CommandRunner c = new Dummy(EnumSet.allOf(Option.class), 
CommandRunner.TEST, "MyFile.txt");
+        final var c = new Dummy(EnumSet.allOf(Option.class), 
CommandRunner.TEST, "MyFile.txt");
         assertFalse(c.hasUnexpectedFileCount(0, 1));
         assertEquals("", c.outputBuffer.toString());
         assertFalse(c.hasUnexpectedFileCount(1, 2));
         assertEquals("", c.outputBuffer.toString());
         assertTrue(c.hasUnexpectedFileCount(2, 3));
-        final String message = c.outputBuffer.toString();
+        String message = c.outputBuffer.toString();
         assertTrue(message.length() != 0);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/HelpCommandTest.java
 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/HelpCommandTest.java
index 3f226ba49a..c9123f1c01 100644
--- 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/HelpCommandTest.java
+++ 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/HelpCommandTest.java
@@ -20,7 +20,7 @@ import java.io.IOException;
 
 // Test dependencies
 import org.junit.Test;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
 
@@ -46,16 +46,16 @@ public final class HelpCommandTest extends TestCase {
      */
     @Test
     public void testDefault() throws InvalidOptionException, IOException {
-        final HelpCommand test = new HelpCommand(0, CommandRunner.TEST);
+        var test = new HelpCommand(0, new String[] {CommandRunner.TEST});
         test.run();
-        final String result = test.outputBuffer.toString();
-        assertTrue("Apache SIS", result.startsWith("Apache SIS"));
-        assertTrue("--locale",   result.contains("--locale"));
-        assertTrue("--encoding", result.contains("--encoding"));
-        assertTrue("--timezone", result.contains("--timezone"));
-        assertTrue("--brief",    result.contains("--brief"));
-        assertTrue("--verbose",  result.contains("--verbose"));
-        assertTrue("--help",     result.contains("--help"));
+        String result = test.outputBuffer.toString();
+        assertTrue(result.startsWith("Apache SIS"));
+        assertTrue(result.contains("--locale"));
+        assertTrue(result.contains("--encoding"));
+        assertTrue(result.contains("--timezone"));
+        assertTrue(result.contains("--brief"));
+        assertTrue(result.contains("--verbose"));
+        assertTrue(result.contains("--help"));
     }
 
     /**
@@ -67,16 +67,16 @@ public final class HelpCommandTest extends TestCase {
      */
     @Test
     public void testHelp() throws InvalidOptionException, IOException {
-        final HelpCommand test = new HelpCommand(0, CommandRunner.TEST, 
"--help");
+        var test = new HelpCommand(0, new String[] {CommandRunner.TEST, 
"--help"});
         test.help("help");
-        final String result = test.outputBuffer.toString();
-        assertTrue ("help",       result.startsWith("help"));
-        assertTrue ("--locale",   result.contains("--locale"));
-        assertTrue ("--encoding", result.contains("--encoding"));
-        assertFalse("--timezone", result.contains("--timezone"));
-        assertFalse("--brief",    result.contains("--brief"));
-        assertFalse("--verbose",  result.contains("--verbose"));
-        assertTrue ("--help",     result.contains("--help"));
+        String result = test.outputBuffer.toString();
+        assertTrue (result.startsWith("help"));
+        assertTrue (result.contains("--locale"));
+        assertTrue (result.contains("--encoding"));
+        assertFalse(result.contains("--timezone"));
+        assertFalse(result.contains("--brief"));
+        assertFalse(result.contains("--verbose"));
+        assertTrue (result.contains("--help"));
     }
 
     /**
@@ -87,11 +87,11 @@ public final class HelpCommandTest extends TestCase {
      */
     @Test
     public void testEnglishLocale() throws InvalidOptionException, IOException 
{
-        final HelpCommand test = new HelpCommand(0, CommandRunner.TEST, 
"--help", "--locale", "en");
+        var test = new HelpCommand(0, new String[] {CommandRunner.TEST, 
"--help", "--locale", "en"});
         test.help("help");
-        final String result = test.outputBuffer.toString();
-        assertTrue(result, result.contains("Show a help overview."));
-        assertTrue(result, result.contains("The locale to use"));
+        String result = test.outputBuffer.toString();
+        assertTrue(result.contains("Show a help overview."));
+        assertTrue(result.contains("The locale to use"));
     }
 
     /**
@@ -102,10 +102,10 @@ public final class HelpCommandTest extends TestCase {
      */
     @Test
     public void testFrenchLocale() throws InvalidOptionException, IOException {
-        final HelpCommand test = new HelpCommand(0, CommandRunner.TEST, 
"--help", "--locale", "fr");
+        var test = new HelpCommand(0, new String[] {CommandRunner.TEST, 
"--help", "--locale", "fr"});
         test.help("help");
-        final String result = test.outputBuffer.toString();
-        assertTrue(result, result.contains("Affiche un écran d’aide."));
-        assertTrue(result, result.contains("Les paramètres régionaux"));
+        String result = test.outputBuffer.toString();
+        assertTrue(result.contains("Affiche un écran d’aide."));
+        assertTrue(result.contains("Les paramètres régionaux"));
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MetadataCommandTest.java
 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MetadataCommandTest.java
index 2d8fa33298..850072701a 100644
--- 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MetadataCommandTest.java
+++ 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MetadataCommandTest.java
@@ -20,7 +20,7 @@ import java.net.URL;
 
 // 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.DependsOn;
 import org.apache.sis.test.TestCase;
@@ -50,7 +50,7 @@ public final class MetadataCommandTest extends TestCase {
     @Test
     public void testNetCDF() throws Exception {
         final URL url = TestData.NETCDF_2D_GEOGRAPHIC.location();
-        final MetadataCommand test = new MetadataCommand(0, 
CommandRunner.TEST, url.toString());
+        var test = new MetadataCommand(0, new String[] {CommandRunner.TEST, 
url.toString()});
         test.run();
         verifyNetCDF("Metadata", test.outputBuffer.toString());
     }
@@ -60,10 +60,10 @@ public final class MetadataCommandTest extends TestCase {
      * This method will check only for some keyword - this is not an extensive 
check of the result.
      */
     private static void verifyNetCDF(final String expectedHeader, final String 
result) {
-        assertTrue(expectedHeader,                           
result.startsWith(expectedHeader));
-        assertTrue("Sea Surface Temperature Analysis Model", 
result.contains("Sea Surface Temperature Analysis Model"));
-        assertTrue("GCMD Science Keywords",                  
result.contains("GCMD Science Keywords"));
-        assertTrue("NOAA/NWS/NCEP",                          
result.contains("NOAA/NWS/NCEP"));
+        assertTrue(result.startsWith(expectedHeader));
+        assertTrue(result.contains("Sea Surface Temperature Analysis Model"));
+        assertTrue(result.contains("GCMD Science Keywords"));
+        assertTrue(result.contains("NOAA/NWS/NCEP"));
     }
 
     /**
@@ -75,7 +75,7 @@ public final class MetadataCommandTest extends TestCase {
     @DependsOnMethod("testNetCDF")
     public void testFormatXML() throws Exception {
         final URL url = TestData.NETCDF_2D_GEOGRAPHIC.location();
-        final MetadataCommand test = new MetadataCommand(0, 
CommandRunner.TEST, url.toString(), "--format", "XML");
+        var test = new MetadataCommand(0, new String[] {CommandRunner.TEST, 
url.toString(), "--format", "XML"});
         test.run();
         verifyNetCDF("<?xml", test.outputBuffer.toString());
     }
diff --git 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MimeTypeCommandTest.java
 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MimeTypeCommandTest.java
index aea2cf35bb..4e53d12049 100644
--- 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MimeTypeCommandTest.java
+++ 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MimeTypeCommandTest.java
@@ -20,7 +20,7 @@ import java.net.URL;
 
 // Test dependencies
 import org.junit.Test;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.storage.gpx.TestData;
 import org.apache.sis.metadata.iso.extent.DefaultExtentTest;
 import org.apache.sis.test.DependsOn;
@@ -48,10 +48,10 @@ public final class MimeTypeCommandTest extends TestCase {
     @Test
     public void testWithMetadataXML() throws Exception {
         final URL url = DefaultExtentTest.getTestFileURL();
-        final MimeTypeCommand test = new MimeTypeCommand(0, 
CommandRunner.TEST, url.toString());
+        var test = new MimeTypeCommand(0, new String[] {CommandRunner.TEST, 
url.toString()});
         test.run();
-        final String output = test.outputBuffer.toString().trim();
-        assertTrue(output, output.endsWith(".xml: 
application/vnd.iso.19139+xml"));
+        String output = test.outputBuffer.toString().trim();
+        assertTrue(output.endsWith(".xml: application/vnd.iso.19139+xml"), 
output);
     }
 
     /**
@@ -63,9 +63,9 @@ public final class MimeTypeCommandTest extends TestCase {
     public void testWithMetadataGPX() throws Exception {
         final URL url = TestData.V1_1.getURL(TestData.METADATA);
         assertNotNull(url);
-        final MimeTypeCommand test = new MimeTypeCommand(0, 
CommandRunner.TEST, url.toString());
+        var test = new MimeTypeCommand(0, new String[] {CommandRunner.TEST, 
url.toString()});
         test.run();
-        final String output = test.outputBuffer.toString().trim();
-        assertTrue(output, output.endsWith("metadata.xml: 
application/gpx+xml"));
+        String output = test.outputBuffer.toString().trim();
+        assertTrue(output.endsWith("metadata.xml: application/gpx+xml"), 
output);
     }
 }
diff --git a/endorsed/src/org.apache.sis.storage/main/module-info.java 
b/endorsed/src/org.apache.sis.storage/main/module-info.java
index b7ce508e35..7e234050d7 100644
--- a/endorsed/src/org.apache.sis.storage/main/module-info.java
+++ b/endorsed/src/org.apache.sis.storage/main/module-info.java
@@ -68,6 +68,7 @@ module org.apache.sis.storage {
             org.apache.sis.storage.coveragejson,        // In the "incubator" 
sub-project.
             org.apache.sis.storage.shapefile,           // In the "incubator" 
sub-project.
             org.apache.sis.cloud.aws,
+            org.apache.sis.console,
             org.apache.sis.gui;                         // In the "optional" 
sub-project.
 
     exports org.apache.sis.storage.xml to
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 516a4cd6f5..a4232f2849 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
@@ -193,8 +193,8 @@ public final class IOUtilities extends Static {
             return path.toString();
         }
         /*
-         * While toString() would work too on the default implementation, the 
following
-         * type is not final. So we are better to invoke the dedicated method.
+         * While `toString()` would work too on the default `File` 
implementation,
+         * the class is not final. So we are better to invoke the dedicated 
method.
          */
         if (path instanceof File) {
             return ((File) path).getPath();
@@ -202,6 +202,24 @@ public final class IOUtilities extends Static {
         return null;
     }
 
+    /**
+     * Converts the given object to an URI if possible, or returns {@code 
null} otherwise.
+     * The current implementation recognizes the {@link Path}, {@link File}, 
{@link URL},
+     * {@link URI} and {@link CharSequence} types.
+     *
+     * @param  path  the path for which to return an URI.
+     * @return the given path as an URI, or {@code null}.
+     * @throws URISyntaxException if the URI cannot be parsed.
+     */
+    public static URI toURI(final Object path) throws URISyntaxException {
+        if (path instanceof URI)          return  (URI)  path;
+        if (path instanceof URL)          return ((URL)  path).toURI();
+        if (path instanceof File)         return ((File) path).toURI();
+        if (path instanceof Path)         return ((Path) path).toUri();
+        if (path instanceof CharSequence) return new URI(path.toString());
+        return null;
+    }
+
     /**
      * 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

Reply via email to