You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/11/29 15:03:03 UTC

(sis) branch geoapi-4.0 updated (e8850ce897 -> 18eff4603a)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


    from e8850ce897 fix(Shapefile): handle case of bad geometry lines with NaN values
     new 149bb5fa11 Show the module path in the configuration, in addition to the class path. Since SIS 1.4, the class path is often empty.
     new 18eff4603a Add an `info` sub-command to the SIS command-line for showing the grid geometry of a file.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../main/org/apache/sis/console/CRSCommand.java    |   2 +-
 .../main/org/apache/sis/console/Command.java       |   1 +
 .../org/apache/sis/console/Commands.properties     |   3 +-
 .../org/apache/sis/console/Commands_fr.properties  |   3 +-
 .../apache/sis/console/FormattedOutputCommand.java |  11 +-
 .../main/org/apache/sis/console/InfoCommand.java   | 153 +++++++++++++++++++++
 .../org/apache/sis/console/MetadataCommand.java    |   5 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |  22 ++-
 .../org/apache/sis/storage/gpx/StoreProvider.java  |   2 +-
 .../main/org/apache/sis/setup/About.java           | 118 +++++++---------
 .../org/apache/sis/util/collection/TreeTables.java |  26 +++-
 .../org/apache/sis/util/resources/Vocabulary.java  |  10 +-
 .../sis/util/resources/Vocabulary.properties       |   2 +-
 .../sis/util/resources/Vocabulary_fr.properties    |   2 +-
 14 files changed, 270 insertions(+), 90 deletions(-)
 create mode 100644 endorsed/src/org.apache.sis.console/main/org/apache/sis/console/InfoCommand.java


(sis) 01/02: Show the module path in the configuration, in addition to the class path. Since SIS 1.4, the class path is often empty.

Posted by de...@apache.org.
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 149bb5fa11031d98eeff42834f0cd9e4e8e2ec79
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Nov 29 12:48:09 2023 +0100

    Show the module path in the configuration, in addition to the class path.
    Since SIS 1.4, the class path is often empty.
---
 .../org/apache/sis/storage/gpx/StoreProvider.java  |   2 +-
 .../main/org/apache/sis/setup/About.java           | 118 +++++++++------------
 .../org/apache/sis/util/resources/Vocabulary.java  |  10 +-
 .../sis/util/resources/Vocabulary.properties       |   2 +-
 .../sis/util/resources/Vocabulary_fr.properties    |   2 +-
 5 files changed, 60 insertions(+), 74 deletions(-)

diff --git a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/StoreProvider.java b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/StoreProvider.java
index 48e63bbf5b..91f01b1d00 100644
--- a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/StoreProvider.java
+++ b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/StoreProvider.java
@@ -40,7 +40,7 @@ import org.apache.sis.util.Version;
  */
 @StoreMetadata(formatName    = StoreProvider.NAME,
                fileSuffixes  = "xml",
-               capabilities  = {Capability.READ, Capability.WRITE},
+               capabilities  = {Capability.READ, Capability.WRITE, Capability.CREATE},
                resourceTypes = {FeatureSet.class})
 public final class StoreProvider extends StaxDataStoreProvider {
     /**
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/About.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/About.java
index 3514eaca85..f492bbc746 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/About.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/About.java
@@ -33,7 +33,6 @@ import java.util.jar.Manifest;
 import java.util.logging.Level;
 import java.util.logging.Handler;
 import java.io.File;
-import java.io.FileFilter;
 import java.io.IOException;
 import java.text.Format;
 import java.text.DateFormat;
@@ -75,11 +74,11 @@ import static org.apache.sis.util.internal.StandardDateFormat.UTC;
  *   <li>Version numbers (SIS, Java, Operation system).</li>
  *   <li>Default locale, timezone and character encoding.</li>
  *   <li>Current directory, user home and Java home.</li>
- *   <li>Libraries on the classpath and extension directories.</li>
+ *   <li>Libraries on the module path.</li>
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.3
  */
 public enum About {
@@ -146,7 +145,7 @@ public enum About {
      *
      * <ul>
      *   <li>JAR files in the extension directories</li>
-     *   <li>JAR files and directories in the application classpath</li>
+     *   <li>JAR files and directories in the application module path</li>
      * </ul>
      */
     LIBRARIES(Vocabulary.Keys.Libraries);
@@ -334,7 +333,7 @@ fill:   for (int i=0; ; i++) {
                                 .getMethod("providers", Locale.class, Vocabulary.class).invoke(null, locale, resources);
                         value = resources.getString(Vocabulary.Keys.EntryCount_1, children.length / 2);
                     } catch (ClassNotFoundException e) {
-                        // org.apache.sis.storage module not in the classpath.
+                        // org.apache.sis.storage module not on the module path.
                         Logging.recoverableException(getLogger(Modules.STORAGE), About.class, "configuration", e);
                     } catch (ReflectiveOperationException e) {
                         value = Exceptions.unwrap(e).toString();
@@ -424,30 +423,30 @@ fill:   for (int i=0; ; i++) {
                 }
                 case 17: {
                     if (sections.contains(PATHS)) {
-                        nameKey = Vocabulary.Keys.TemporaryFiles;
-                        value = getProperty("java.io.tmpdir");
+                        nameKey = Vocabulary.Keys.JavaHome;
+                        value = javaHome = getProperty("java.home");
                     }
                     break;
                 }
                 case 18: {
                     if (sections.contains(PATHS)) {
-                        nameKey = Vocabulary.Keys.JavaHome;
-                        value = javaHome = getProperty("java.home");
+                        nameKey = Vocabulary.Keys.TemporaryFiles;
+                        value = getProperty("java.io.tmpdir");
                     }
                     break;
                 }
                 case 19: {
                     newSection = LIBRARIES;
                     if (sections.contains(LIBRARIES)) {
-                        nameKey = Vocabulary.Keys.JavaExtensions;
-                        value = classpath(getProperty("java.ext.dirs"), true);
+                        nameKey = Vocabulary.Keys.ModulePath;
+                        value = modulePath(getProperty("jdk.module.path"), false);
                     }
                     break;
                 }
                 case 20: {
                     if (sections.contains(LIBRARIES)) {
                         nameKey = Vocabulary.Keys.Classpath;
-                        value = classpath(getProperty("java.class.path"), false);
+                        value = modulePath(getProperty("java.class.path"), true);
                     }
                     break;
                 }
@@ -540,60 +539,50 @@ pathTree:   for (int j=0; ; j++) {
      * Returns a map of all JAR files or class directories found in the given paths,
      * associated to a description obtained from their {@code META-INF/MANIFEST.MF}.
      *
-     * @param  paths          the paths using the {@link File#pathSeparatorChar} separator.
-     * @param  asDirectories  {@code true} if the paths are directories, or {@code false} for JAR files.
+     * @param  paths      the paths using the {@link File#pathSeparatorChar} separator.
+     * @param  classpath  whether to scan the class-path manifest attribute.
      * @return the paths, or {@code null} if none.
      */
-    private static Map<File,CharSequence> classpath(final String paths, final boolean asDirectories) {
+    private static Map<File,CharSequence> modulePath(final String paths, final boolean classpath) {
         final Map<File,CharSequence> files = new LinkedHashMap<>();
-        return classpath(paths, null, asDirectories, files) ? files : null;
+        return modulePath(paths, File.pathSeparatorChar, classpath, null, files) ? files : null;
     }
 
     /**
-     * Implementation of {@link #classpath(String, boolean)} to be invoked recursively.
-     * The {@code paths} argument may contains many path separated by one of the
-     * following separators:
-     *
-     * <ul>
-     *   <li>If {@code directory} is null, then {@code paths} is assumed to be a
-     *       system property value using the {@link File#pathSeparatorChar}.</li>
-     *   <li>If {@code directory} is non-null, then {@code paths} is assumed to be
-     *       a {@code MANIFEST.MF} attribute using space as the path separator.</li>
-     * </ul>
+     * Implementation of {@link #modulePath(String, boolean)} to be invoked recursively.
+     * The {@code paths} argument may contain many paths separated by the given separator.
+     * That separator is usually {@link File#pathSeparatorChar}, except for the value of
+     * the {@code MANIFEST.MF} attribute in which case the separator is a space.
      *
-     * @param  paths          the paths using the separator described above.
-     * @param  directory      the directory of {@code MANIFEST.MF} classpath, or {@code null}.
-     * @param  asDirectories  {@code true} if the paths are directories, or {@code false} for JAR files.
-     * @param  files          where to add the paths.
+     * @param  paths      the paths using the specified path separator.
+     * @param  separator  the path separator: {@link File#pathSeparatorChar} or space.
+     * @param  classpath  whether to scan the class-path manifest attribute.
+     * @param  directory  the directory of {@code MANIFEST.MF} classpath, or {@code null}.
+     * @param  files      where to add the paths.
      * @return {@code true} if the given map has been changed as a result of this method call.
      */
-    private static boolean classpath(final String paths, final File directory,
-            final boolean asDirectories, final Map<File,CharSequence> files)
+    private static boolean modulePath(final String paths, final char separator, final boolean classpath,
+            final File directory, final Map<File,CharSequence> files)
     {
         if (paths == null) {
             return false;
         }
         boolean changed = false;
-        for (final CharSequence path : CharSequences.split(paths, (directory == null) ? File.pathSeparatorChar : ' ')) {
+        for (final CharSequence path : CharSequences.split(paths, separator)) {
             final File file = new File(directory, path.toString());
-            if (file.exists()) {
-                if (!asDirectories) {
-                    if (!files.containsKey(file)) {
-                        files.put(file, null);
-                        changed = true;
-                    }
-                } else {
-                    // If we are scanning extensions, then the path are directories
-                    // rather than files. So we need to scan the directory content.
-                    final JARFilter filter = new JARFilter();
-                    final File[] list = file.listFiles(filter);
-                    if (list != null) {
-                        Arrays.sort(list);
-                        for (final File ext : list) {
-                            if (!files.containsKey(ext)) {
-                                files.put(ext, null);
-                                changed = true;
-                            }
+            if (file.isFile()) {
+                if (!files.containsKey(file)) {
+                    files.put(file, null);
+                    changed = true;
+                }
+            } else if (file.isDirectory()) {
+                final File[] list = file.listFiles((pathname) -> pathname.getName().endsWith(".jar"));
+                if (list != null) {
+                    Arrays.sort(list);
+                    for (final File ext : list) {
+                        if (!files.containsKey(ext)) {
+                            files.put(ext, null);
+                            changed = true;
                         }
                     }
                 }
@@ -608,8 +597,7 @@ pathTree:   for (int j=0; ; j++) {
          */
         IOException error = null;
         for (final Map.Entry<File,CharSequence> entry : files.entrySet()) {
-            CharSequence title = entry.getValue();
-            if (title != null) {
+            if (entry.getValue() != null) {
                 continue;               // This file has already been processed by a recursive method invocation.
             }
             final File file = entry.getKey();
@@ -619,6 +607,7 @@ pathTree:   for (int j=0; ; j++) {
                     if (manifest != null) {
                         final Attributes attributes = manifest.getMainAttributes();
                         if (attributes != null) {
+                            CharSequence title;
                             title = concatenate(attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE),
                                     attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION), false);
                             if (title == null) {
@@ -631,10 +620,16 @@ pathTree:   for (int j=0; ; j++) {
                                 }
                             }
                             entry.setValue(title);
-                            if (classpath(attributes.getValue(Attributes.Name.CLASS_PATH),
-                                    file.getParentFile(), false, files))
-                            {
-                                break;          // Necessary for avoiding ConcurrentModificationException.
+                            /*
+                             * If scanning a class path, this JAR file implicitly adds the content of the
+                             * CLASS-PATH attribute as transitive dependencies. If scanning a module path,
+                             * this is ignored.
+                             */
+                            if (classpath) {
+                                String cp = attributes.getValue(Attributes.Name.CLASS_PATH);
+                                if (modulePath(cp, ' ', true, file.getParentFile(), files)) {
+                                    break;          // Necessary for avoiding ConcurrentModificationException.
+                                }
                             }
                         }
                     }
@@ -775,15 +770,6 @@ pathTree:   for (int j=0; ; j++) {
         return df.format(Math.abs(offset), buffer.append(offset < 0 ? '-' : '+').append(' '), new FieldPosition(0));
     }
 
-    /**
-     * Filters the JAR files in an extension directory.
-     */
-    private static final class JARFilter implements FileFilter {
-        @Override public boolean accept(final File pathname) {
-            return pathname.getName().endsWith(".jar");
-        }
-    }
-
     /**
      * Returns the given file relative to the given root, or {@code null} if the root is not
      * a parent of that file.
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
index 7b67be2cf2..fc24f83221 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
@@ -684,11 +684,6 @@ public class Vocabulary extends IndexedResourceBundle {
          */
         public static final short Isolines = 252;
 
-        /**
-         * Java extensions
-         */
-        public static final short JavaExtensions = 109;
-
         /**
          * Java home directory
          */
@@ -849,6 +844,11 @@ public class Vocabulary extends IndexedResourceBundle {
          */
         public static final short ModifiedJulian = 136;
 
+        /**
+         * Module path
+         */
+        public static final short ModulePath = 109;
+
         /**
          * … {0} more…
          */
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
index e9abdbb809..4bfda2c7a8 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
@@ -141,7 +141,6 @@ Invalid                 = Invalid
 InverseOperation        = Inverse operation
 Interval                = Interval
 Isolines                = Isolines
-JavaExtensions          = Java extensions
 JavaHome                = Java home directory
 Julian                  = Julian
 Latitude                = Latitude
@@ -174,6 +173,7 @@ Minimum                 = Minimum
 MinimumValue            = Minimum value
 MissingValue            = Missing value
 ModifiedJulian          = Modified Julian
+ModulePath              = Module path
 More_1                  = \u2026 {0} more\u2026
 Multiplicity            = Multiplicity
 Name                    = Name
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
index 67bebdcb73..18ca8ac441 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -148,7 +148,6 @@ Invalid                 = Invalide
 InverseOperation        = Op\u00e9ration inverse
 Interval                = Intervalle
 Isolines                = Isolignes
-JavaExtensions          = Extensions du Java
 JavaHome                = R\u00e9pertoire du Java
 Julian                  = Julien
 Latitude                = Latitude
@@ -181,6 +180,7 @@ Minimum                 = Minimum
 MinimumValue            = Valeur minimale
 MissingValue            = Valeur manquante
 ModifiedJulian          = Julien modifi\u00e9
+ModulePath              = Chemin des modules
 More_1                  = \u2026 {0} de plus\u2026
 Multiplicity            = Multiplicit\u00e9
 Name                    = Nom


(sis) 02/02: Add an `info` sub-command to the SIS command-line for showing the grid geometry of a file.

Posted by de...@apache.org.
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 18eff4603a64538e3a1fc10e32bfe814e8fd5053
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Nov 29 15:30:21 2023 +0100

    Add an `info` sub-command to the SIS command-line for showing the grid geometry of a file.
---
 .../main/org/apache/sis/console/CRSCommand.java    |   2 +-
 .../main/org/apache/sis/console/Command.java       |   1 +
 .../org/apache/sis/console/Commands.properties     |   3 +-
 .../org/apache/sis/console/Commands_fr.properties  |   3 +-
 .../apache/sis/console/FormattedOutputCommand.java |  11 +-
 .../main/org/apache/sis/console/InfoCommand.java   | 153 +++++++++++++++++++++
 .../org/apache/sis/console/MetadataCommand.java    |   5 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |  22 ++-
 .../org/apache/sis/util/collection/TreeTables.java |  26 +++-
 9 files changed, 210 insertions(+), 16 deletions(-)

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 d0c5a76eec..0b2238708e 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
@@ -36,7 +36,7 @@ final class CRSCommand extends FormattedOutputCommand {
     }
 
     /**
-     * Prints metadata or CRS information.
+     * Prints CRS information.
      *
      * @return 0 on success, or an exit code if the command failed for a reason other than an uncaught Java exception.
      */
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 6a6f4c9111..adf3405508 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
@@ -155,6 +155,7 @@ public final class Command {
                 case "mime-type":  command = new MimeTypeCommand  (commandIndex, args); break;
                 case "metadata":   command = new MetadataCommand  (commandIndex, args); break;
                 case "crs":        command = new CRSCommand       (commandIndex, args); break;
+                case "info":       command = new InfoCommand      (commandIndex, args); break;
                 case "identifier": command = new IdentifierCommand(commandIndex, args); break;
                 case "transform":  command = new TransformCommand (commandIndex, args); break;
                 case "translate":  command = new TranslateCommand (commandIndex, args); break;
diff --git a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Commands.properties b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Commands.properties
index 14e23b348f..09d6e5534c 100644
--- a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Commands.properties
+++ b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Commands.properties
@@ -6,7 +6,8 @@ Usage=Usage: sis <command> [options] [files]
 help=Show a help overview.
 about=Show information about Apache SIS and system configuration.
 mime-type=Show MIME type for the given file.
-metadata=Show metadata information for the given file.
+info=Show information about the content of the given file.
+metadata=Show ISO 19115 metadata information for the given file.
 crs=Show Coordinate Reference System (CRS) information for the given file.
 identifier=Show identifiers for metadata and referencing systems in the given file.
 transform=Convert or transform coordinates from given source CRS to target CRS.
diff --git a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Commands_fr.properties b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Commands_fr.properties
index 65747e839d..c91847685a 100644
--- a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Commands_fr.properties
+++ b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Commands_fr.properties
@@ -6,7 +6,8 @@ Usage=Usage: sis <commande> [options] [fichiers]
 help=Affiche un \u00e9cran d\u2019aide.
 about=Affiche des informations \u00e0 propos de Apache SIS et de la configuration du syst\u00e8me.
 mime-type=Affiche le type MIME du fichier sp\u00e9cifi\u00e9.
-metadata=Affiche les m\u00e9ta-donn\u00e9es du fichier sp\u00e9cifi\u00e9.
+info=Affiche des informations \u00e0 propos du contenu du fichier sp\u00e9cifi\u00e9.
+metadata=Affiche les m\u00e9ta-donn\u00e9es ISO 19115 du fichier sp\u00e9cifi\u00e9.
 crs=Affiche le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es du fichier sp\u00e9cifi\u00e9.
 identifier=Affiche les identifiants des m\u00e9ta-donn\u00e9es et des syst\u00e8mes de r\u00e9f\u00e9rences du fichier sp\u00e9cifi\u00e9.
 transform=Transforme des coordonn\u00e9es du syst\u00e8me de r\u00e9f\u00e9rence source vers le syst\u00e8me destination donn\u00e9.
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 504a66ce43..fab233f71c 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
@@ -88,7 +88,6 @@ abstract class FormattedOutputCommand extends CommandRunner {
 
     /**
      * Creates a new sub-command with the given command-line arguments.
-     * This constructor is for {@code MetadataCommand} subclasses.
      *
      * @param  commandIndex      index of the {@code arguments} element containing the sub-command name, or -1 if none.
      * @param  arguments         the command-line arguments provided by the user.
@@ -217,7 +216,7 @@ abstract class FormattedOutputCommand extends CommandRunner {
                 final TreeTable tree = MetadataStandard.ISO_19115.asTreeTable(object,
                         (object instanceof Metadata) ? Metadata.class : null,
                         ValueExistencePolicy.COMPACT);
-                final TreeTableFormat tf = new TreeTableFormat(locale, timezone);
+                final var tf = new TreeTableFormat(locale, timezone);
                 tf.setColumns(TableColumn.NAME, TableColumn.VALUE);
                 tf.setNodeFilter(getNodeFilter());
                 tf.format(tree, out);
@@ -225,7 +224,7 @@ abstract class FormattedOutputCommand extends CommandRunner {
             }
 
             case WKT: {
-                final WKTFormat f = new WKTFormat(locale, timezone);
+                final var f = new WKTFormat(locale, timezone);
                 if (convention != null) {
                     f.setConvention(convention);
                 }
@@ -238,7 +237,7 @@ abstract class FormattedOutputCommand extends CommandRunner {
             }
 
             case XML: {
-                final MarshallerPool pool = new MarshallerPool(null);
+                final var pool = new MarshallerPool(null);
                 final Marshaller marshaller = pool.acquireMarshaller();
                 marshaller.setProperty(XML.LOCALE,   locale);
                 marshaller.setProperty(XML.TIMEZONE, timezone);
@@ -254,7 +253,7 @@ abstract class FormattedOutputCommand extends CommandRunner {
             }
 
             default: {
-                final StorageConnector connector = new StorageConnector(out);
+                final var connector = new StorageConnector(out);
                 connector.setOption(OptionKey.TIMEZONE, timezone);
                 connector.setOption(OptionKey.LOCALE,   locale);
                 connector.setOption(OptionKey.ENCODING, encoding);
@@ -266,7 +265,7 @@ abstract class FormattedOutputCommand extends CommandRunner {
                      * Note: after such generalization is done, revert the xml-store dependency
                      *       scope in pom.xml from "compile" to "runtime".
                      */
-                    final org.apache.sis.storage.gpx.WritableStore fs = (org.apache.sis.storage.gpx.WritableStore) store;
+                    final var fs = (org.apache.sis.storage.gpx.WritableStore) store;
                     if (version != null) {
                         fs.setVersion(version);
                     }
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
new file mode 100644
index 0000000000..4ed829c9b0
--- /dev/null
+++ b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/InfoCommand.java
@@ -0,0 +1,153 @@
+/*
+ * 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.console;
+
+import java.util.EnumSet;
+import org.apache.sis.coverage.Category;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStores;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.measure.RangeFormat;
+import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.collection.TreeTables;
+import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.util.collection.TreeTableFormat;
+import org.apache.sis.util.collection.DefaultTreeTable;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.resources.Vocabulary;
+
+
+/**
+ * The "info" sub-command.
+ * The content varies depending on the resource type.
+ * For grid coverage, it contains the grid geometry.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class InfoCommand extends FormattedOutputCommand {
+    /**
+     * Returns valid options for the {@code "metadata"} command.
+     */
+    static EnumSet<Option> options() {
+        return EnumSet.of(Option.LOCALE, Option.TIMEZONE, Option.COLORS, Option.VERBOSE, Option.HELP, Option.DEBUG);
+    }
+
+    /**
+     * The bit mask of information to request from a grid geometry.
+     */
+    private int gridBitMask;
+
+    /**
+     * Creates the {@code "info"} sub-command.
+     *
+     * @param  commandIndex  index of the {@code arguments} element containing the {@code "info"} command name, or -1 if none.
+     * @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 {
+        super(commandIndex, arguments, options(), OutputFormat.TEXT);
+        gridBitMask = GridGeometry.EXTENT | GridGeometry.GEOGRAPHIC_EXTENT | GridGeometry.TEMPORAL_EXTENT
+                    | GridGeometry.CRS | GridGeometry.RESOLUTION;
+        if (options.containsKey(Option.VERBOSE)) {
+            gridBitMask |= GridGeometry.ENVELOPE | GridGeometry.GRID_TO_CRS;
+        }
+    }
+
+    /**
+     * Prints resource information.
+     *
+     * @return 0 on success, or an exit code if the command failed for a reason other than an uncaught Java exception.
+     */
+    @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);
+        }
+        final var tree = new DefaultTreeTable(TableColumn.VALUE_AS_TEXT);
+        try (DataStore store = DataStores.open(input)) {
+            define(tree.getRoot(), store);
+        } catch (BackingStoreException e) {
+            throw e.unwrapOrRethrow(DataStoreException.class);
+        }
+        final var tf = new TreeTableFormat(locale, timezone);
+        tf.format(tree, out);
+        return 0;
+    }
+
+    /**
+     * Sets the values of the given node using information in the given resource.
+     *
+     * @param  target  the tree node to define.
+     * @param  source  the resource from which to get the information.
+     * @throws DataStoreException if an error occurred while reading the resource.
+     */
+    private void define(final TreeTable.Node target, final Resource source) throws DataStoreException {
+        String name = source.getIdentifier().map((id) -> id.toInternationalString().toString(locale))
+                .orElseGet(() -> Vocabulary.getResources(locale).getString(Vocabulary.Keys.Unnamed));
+        target.setValue(TableColumn.VALUE_AS_TEXT, name);
+        if (source instanceof GridCoverageResource) {
+            final var grid = (GridCoverageResource) source;
+            TreeTable.Node root = grid.getGridGeometry().toTree(locale, gridBitMask).getRoot();
+            TreeTables.moveChildren(root, target);
+            toTree(grid.getSampleDimensions(), target.newChild(), TableColumn.VALUE_AS_TEXT);
+        } else if (source instanceof Aggregate) {
+            for (Resource component : ((Aggregate) source).components()) {
+                define(target.newChild(), component);
+            }
+        }
+    }
+
+    /**
+     * Appends information about the sample dimensions in a tree.
+     *
+     * @param bands   the sample dimensions to format.
+     * @param target  where to format the sample dimensions.
+     * @param column  the column where to write the texts.
+     */
+    private void toTree(final Iterable<SampleDimension> bands, final TreeTable.Node target,
+                        final TableColumn<? super String> column)
+    {
+        target.setValue(column, Vocabulary.getResources(locale).getString(Vocabulary.Keys.SampleDimensions));
+        final var rf = new RangeFormat(locale, timezone);
+        final var sb = new StringBuffer();
+        for (SampleDimension band : bands) {
+            band = band.forConvertedValues(true);
+            final TreeTable.Node bn = target.newChild();
+            bn.setValue(column, band.getName().toInternationalString().toString(locale));
+            for (final Category category : band.getCategories()) {
+                final TreeTable.Node cn = bn.newChild();
+                sb.append(category.getName().toString(locale)).append(" (");
+                rf.format(category.getSampleRange(), sb, null).append(')');
+                cn.setValue(column, sb.toString());
+                sb.setLength(0);
+            }
+        }
+    }
+}
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 9e22472a51..61c64bbaad 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,6 +28,7 @@ import org.apache.sis.util.collection.TreeTable;
 
 /**
  * The "metadata" sub-command.
+ * This command shows ISO 19115 metadata for the content of a file.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -52,7 +53,7 @@ final class MetadataCommand extends FormattedOutputCommand {
     }
 
     /**
-     * Prints metadata or CRS information.
+     * Prints metadata.
      *
      * @return 0 on success, or an exit code if the command failed for a reason other than an uncaught Java exception.
      */
@@ -68,7 +69,7 @@ final class MetadataCommand extends FormattedOutputCommand {
         }
         if (metadata != null) {
             if (!(metadata instanceof Metadata)) {
-                final DefaultMetadata md = new DefaultMetadata();
+                final var md = new DefaultMetadata();
                 md.setReferenceSystemInfo(Set.of((ReferenceSystem) metadata));
                 metadata = md;
             }
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index 52a0dca631..0477b3cfc4 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -56,6 +56,7 @@ import org.apache.sis.coverage.grid.j2d.SampleModelFactory;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.internal.UnmodifiableArrayList;
 import org.apache.sis.util.internal.Numerics;
 import org.apache.sis.util.internal.Strings;
@@ -1519,7 +1520,26 @@ final class ImageFileDirectory extends DataCube {
                                 minValues.get(Math.min(band, minValues.size()-1)), true,
                                 maxValues.get(Math.min(band, maxValues.size()-1)), true);
                     }
-                    builder.setName(band + 1).setBackground(getFillValue(true));
+                    short nameKey = 0;
+                    switch (photometricInterpretation) {
+                        case PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO:
+                        case PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO: nameKey = Vocabulary.Keys.Grayscale;  break;
+                        case PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR: nameKey = Vocabulary.Keys.ColorIndex; break;
+                        case PHOTOMETRIC_INTERPRETATION_RGB: {
+                            switch (band) {
+                                case 0: nameKey = Vocabulary.Keys.Red;   break;
+                                case 1: nameKey = Vocabulary.Keys.Green; break;
+                                case 2: nameKey = Vocabulary.Keys.Blue;  break;
+                            }
+                            break;
+                        }
+                    }
+                    if (nameKey != 0) {
+                        builder.setName(Vocabulary.formatInternational(nameKey));
+                    } else {
+                        builder.setName(band + 1);
+                    }
+                    builder.setBackground(getFillValue(true));
                     final SampleDimension sd;
                     if (isIndexValid) {
                         sd = reader.store.customizer.customize(index, band, sampleRange, builder);
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/TreeTables.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/TreeTables.java
index dd212c1d1b..6d176b35d6 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/TreeTables.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/TreeTables.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sis.util.collection;
 
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -34,9 +36,6 @@ import org.apache.sis.util.ArgumentChecks;
  * This class provides methods for some tasks considered generic enough,
  * and example codes for more specialized tasks that developers can customize.
  *
- * <p>The remaining of this class javadoc contains example codes placed in public domain.
- * Developers can copy and adapt those examples as they see fit.</p>
- *
  * <h2>Example 1: Reduce the depth of a tree</h2>
  * For every branch containing exactly one child, the following method concatenates in-place
  * that branch and its child together. This method can be used for simplifying depth trees into
@@ -99,7 +98,7 @@ import org.apache.sis.util.ArgumentChecks;
  * }
  *
  * @author  Martin Desruisseaux
- * @version 0.3
+ * @version 1.5
  *
  * @see TreeTable
  *
@@ -254,6 +253,25 @@ public final class TreeTables extends Static {
         return changes;
     }
 
+    /**
+     * Removes all children of the given source, then adds them to the given target.
+     * Children need to be removed first because they cannot have two parents.
+     * Caller should ensure that the two tables use the same columns.
+     *
+     * @param  source  source from which to remove children.
+     * @param  target  where to add the children.
+     *
+     * @since 1.5
+     */
+    public static void moveChildren(final TreeTable.Node source, final TreeTable.Node target) {
+        ArgumentChecks.ensureNonNull("source", source);
+        ArgumentChecks.ensureNonNull("target", target);
+        final Collection<TreeTable.Node> children = source.getChildren();
+        final TreeTable.Node[] array = children.toArray(TreeTable.Node[]::new);
+        children.clear();
+        target.getChildren().addAll(Arrays.asList(array));
+    }
+
     /**
      * Returns a string representation of the given tree table.
      * The current implementation uses a shared instance of {@link TreeTableFormat}.