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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 2b9172f57c Provides metadata information about the software used by 
`NetcdfStore` for reading a netCDF file. This commit completes the previous 
one, which was providing this information for all other stores. The netCDF case 
required additional pre-defined metadata and helper methods for fetching 
version. This commit also simplifies the way to build that 
"formatSpecificationCitation" metadata node.
2b9172f57c is described below

commit 2b9172f57cd57b920ca923d33b9074fa275bc426
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sun Sep 29 15:15:29 2024 +0200

    Provides metadata information about the software used by `NetcdfStore` for 
reading a netCDF file.
    This commit completes the previous one, which was providing this 
information for all other stores.
    The netCDF case required additional pre-defined metadata and helper methods 
for fetching version.
    This commit also simplifies the way to build that 
"formatSpecificationCitation" metadata node.
---
 .../sis/metadata/iso/citation/Citations.java       |   2 +-
 .../main/org/apache/sis/metadata/sql/Citations.sql |  34 +--
 .../apache/sis/metadata/sql/MetadataFallback.java  |  24 +-
 .../sis/metadata/iso/citation/CitationsTest.java   |   2 +-
 .../sis/metadata/sql/MetadataFallbackVerifier.java |   3 +-
 .../operation/provider/NorthPoleRotation.java      |   3 +-
 .../operation/provider/SouthPoleRotation.java      |   3 +-
 .../apache/sis/storage/landsat/LandsatStore.java   |   2 +-
 .../apache/sis/storage/landsat/MetadataReader.java |  14 +-
 .../sis/storage/landsat/MetadataReaderTest.java    | 282 +++++++++------------
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  11 +-
 .../apache/sis/storage/netcdf/MetadataReader.java  |  41 +--
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |   7 +-
 .../sis/storage/netcdf/NetcdfStoreProvider.java    |  12 +-
 .../apache/sis/storage/netcdf/base/Decoder.java    |  15 +-
 .../sis/storage/netcdf/classic/ChannelDecoder.java |  16 +-
 .../sis/storage/netcdf/ucar/DecoderWrapper.java    |  65 ++++-
 .../sis/storage/netcdf/MetadataReaderTest.java     | 104 ++++----
 .../apache/sis/storage/base/MetadataBuilder.java   |  80 +++---
 .../main/org/apache/sis/storage/csv/Store.java     |  32 +--
 .../org/apache/sis/storage/esri/RasterStore.java   |  11 +-
 .../apache/sis/storage/image/WorldFileStore.java   |   9 +-
 .../main/org/apache/sis/util/Version.java          |  53 +++-
 .../main/org/apache/sis/util/privy/Constants.java  |   6 +
 .../test/org/apache/sis/util/VersionTest.java      |   9 +
 geoapi/snapshot                                    |   2 +-
 26 files changed, 454 insertions(+), 388 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/Citations.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/Citations.java
index bdd25d3564..5c3c60ee61 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/Citations.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/Citations.java
@@ -336,7 +336,7 @@ public final class Citations extends Static {
      *
      * @since 0.4
      */
-    public static final IdentifierSpace<String> NETCDF = new 
CitationConstant.Authority<>("NetCDF");
+    public static final IdentifierSpace<String> NETCDF = new 
CitationConstant.Authority<>(Constants.NETCDF);
 
     /**
      * The authority for identifiers of objects defined by the the GeoTIFF 
specification.
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Citations.sql
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Citations.sql
index 34af3febff..7dd4abb1c4 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Citations.sql
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Citations.sql
@@ -36,6 +36,7 @@ CREATE TABLE metadata."OnlineResource" (
 INSERT INTO metadata."OnlineResource" ("ID", "linkage") VALUES
   ('EPSG',    'https://epsg.org/'),
   ('ESRI',    'https://www.esri.com/'),
+  ('GDAL',    'https://gdal.org/'),
   ('GeoTIFF', 'https://trac.osgeo.org/geotiff/'),
   ('IHO',     'https://www.iho.int/'),
   ('IOGP',    'https://www.iogp.org/'),
@@ -50,7 +51,7 @@ INSERT INTO metadata."OnlineResource" ("ID", "linkage") VALUES
   ('PostGIS', 'https://postgis.net/'),
   ('PROJ',    'https://proj.org/'),
   ('SIS',     'https://sis.apache.org/'),
-  ('GDAL',    'https://gdal.org/'),
+  ('Unidata', 'https://www.unidata.ucar.edu/'),
   ('WMO',     'https://www.wmo.int/'),
   ('WMS',     'https://www.ogc.org/standards/wms');
 
@@ -108,6 +109,7 @@ INSERT INTO metadata."Organisation" ("ID", "name") VALUES
   ('{org}NATO',   'North Atlantic Treaty Organization'),
   ('{org}OGC',    'Open Geospatial Consortium'),
   ('{org}OSGeo',  'The Open Source Geospatial Foundation'),
+  ('{org}UCAR',   'University Corporation for Atmospheric Research'),
   ('{org}WMO',    'World Meteorological Organization');
 
 INSERT INTO metadata."Responsibility" ("ID", "party", "role") VALUES
@@ -122,6 +124,7 @@ INSERT INTO metadata."Responsibility" ("ID", "party", 
"role") VALUES
   ('NATO',    '{org}NATO',   'principalInvestigator'),
   ('OGC',     '{org}OGC',    'principalInvestigator'),
   ('OSGeo',   '{org}OSGeo',  'resourceProvider'),
+  ('UCAR',    '{org}UCAR',   'resourceProvider'),
   ('WMO',     '{org}WMO',    'principalInvestigator');
 
 
@@ -198,19 +201,20 @@ INSERT INTO metadata."Identifier" ("ID", "code", 
"codeSpace", "version") VALUES
   ('SIS',         'SIS',     'Apache',    NULL);
 
 INSERT INTO metadata."Citation" ("ID", "onlineResource", "edition", 
"citedResponsibleParty", "presentationForm", "alternateTitle" , "title") VALUES
-  ('ISBN',       'ISBN',  NULL,              'ISBN',    NULL,             
'ISBN',         'International Standard Book Number'),
-  ('ISSN',       'ISSN',  NULL,              'ISSN',    NULL,             
'ISSN',         'International Standard Serial Number'),
-  ('ISO 19115-1', NULL,  'ISO 19115-1:2014', 'ISO',    'documentDigital', 'ISO 
19115-1',  'Geographic Information — Metadata Part 1: Fundamentals'),
-  ('ISO 19115-2', NULL,  'ISO 19115-2:2019', 'ISO',    'documentDigital', 'ISO 
19115-2',  'Geographic Information — Metadata Part 2: Extensions for imagery 
and gridded data'),
-  ('IHO S-57',    NULL,  '3.1',              'IHO',    'documentDigital', 
'S-57',         'IHO transfer standard for digital hydrographic data'),
-  ('MGRS',        NULL,   NULL,              'NATO',   'documentDigital',  
NULL,          'Military Grid Reference System'),
-  ('WMS',        'WMS',  '1.3',              'OGC',    'documentDigital', 
'WMS',          'Web Map Server'),
-  ('EPSG',       'EPSG',  NULL,              'IOGP',   'tableDigital',    
'EPSG Dataset', 'EPSG Geodetic Parameter Dataset'),
-  ('ArcGIS',     'ESRI',  NULL,              'ESRI',    NULL,              
NULL,          'ArcGIS'),
-  ('MapInfo',     NULL,   NULL,              'MapInfo', NULL,             
'MapInfo',      'MapInfo Pro'),
-  ('PROJ',       'PROJ',  NULL,              'OSGeo',   NULL,             
'Proj',         'PROJ coordinate transformation software library'),
-  ('GDAL',       'GDAL',  NULL,              'OSGeo',   NULL,              
NULL,          'Geospatial Data Abstraction Library'),
-  ('SIS',        'SIS',   NULL,              'Apache',  NULL,             
'Apache SIS',   'Apache Spatial Information System');
+  ('ISBN',       'ISBN',    NULL,              'ISBN',    NULL,             
'ISBN',         'International Standard Book Number'),
+  ('ISSN',       'ISSN',    NULL,              'ISSN',    NULL,             
'ISSN',         'International Standard Serial Number'),
+  ('ISO 19115-1', NULL,    'ISO 19115-1:2014', 'ISO',    'documentDigital', 
'ISO 19115-1',  'Geographic Information — Metadata Part 1: Fundamentals'),
+  ('ISO 19115-2', NULL,    'ISO 19115-2:2019', 'ISO',    'documentDigital', 
'ISO 19115-2',  'Geographic Information — Metadata Part 2: Extensions for 
imagery and gridded data'),
+  ('IHO S-57',    NULL,    '3.1',              'IHO',    'documentDigital', 
'S-57',         'IHO transfer standard for digital hydrographic data'),
+  ('MGRS',        NULL,     NULL,              'NATO',   'documentDigital',  
NULL,          'Military Grid Reference System'),
+  ('WMS',        'WMS',    '1.3',              'OGC',    'documentDigital', 
'WMS',          'Web Map Server'),
+  ('EPSG',       'EPSG',    NULL,              'IOGP',   'tableDigital',    
'EPSG Dataset', 'EPSG Geodetic Parameter Dataset'),
+  ('ArcGIS',     'ESRI',    NULL,              'ESRI',    NULL,              
NULL,          'ArcGIS'),
+  ('MapInfo',     NULL,     NULL,              'MapInfo', NULL,             
'MapInfo',      'MapInfo Pro'),
+  ('PROJ',       'PROJ',    NULL,              'OSGeo',   NULL,             
'Proj',         'PROJ coordinate transformation software library'),
+  ('GDAL',       'GDAL',    NULL,              'OSGeo',   NULL,             
'GDAL',         'Geospatial Data Abstraction Library'),
+  ('Unidata',    'Unidata', NULL,              'UCAR',    NULL,              
NULL,          'Unidata netCDF library'),
+  ('SIS',        'SIS',     NULL,              'Apache',  NULL,             
'Apache SIS',   'Apache Spatial Information System');
 
 
 
@@ -233,4 +237,4 @@ INSERT INTO metadata."Citation" ("ID", "onlineResource", 
"citedResponsibleParty"
   ('WMO',  'WMO',   'WMO',  'documentDigital', 'WMO Information System (WIS)'),
   ('IOGP', 'IOGP',  'IOGP', 'documentDigital', 'IOGP Surveying and Positioning 
Guidance Note 7');
 
-UPDATE metadata."Citation" SET "identifier" = "ID" WHERE "ID"<>'ISBN' AND 
"ID"<>'ISSN' AND "ID"<>'MGRS';
+UPDATE metadata."Citation" SET "identifier" = "ID" WHERE "ID"<>'ISBN' AND 
"ID"<>'ISSN' AND "ID"<>'MGRS' AND "ID"<>'Unidata';
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataFallback.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataFallback.java
index c275652657..edee1c42fe 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataFallback.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataFallback.java
@@ -110,7 +110,7 @@ final class MetadataFallback extends MetadataSource {
         CharSequence     title;
         CharSequence     alternateTitle        = null;
         CharSequence     edition               = null;
-        String           code                  = null;
+        String           code                  = key;
         String           codeSpace             = null;
         String           version               = null;
         CharSequence     citedResponsibleParty = null;
@@ -141,29 +141,25 @@ final class MetadataFallback extends MetadataSource {
                 title             = "Web Map Server";
                 alternateTitle    = "WMS";
                 edition = version = "1.3";
-                code              = "WMS";      // Note: OGC internal code is 
06-042.
                 codeSpace         = "OGC";
                 copyFrom          = "OGC";
                 presentationForm  = PresentationForm.DOCUMENT_DIGITAL;
                 break;
             }
-            case "OGC": {
+            case Constants.OGC: {
                 title                 = "OGC Naming Authority";
-                code                  = Constants.OGC;
                 citedResponsibleParty = "Open Geospatial Consortium";
                 presentationForm      = PresentationForm.DOCUMENT_DIGITAL;
                 break;
             }
             case "WMO": {
                 title                 = "WMO Information System (WIS)";
-                code                  = key;
                 citedResponsibleParty = "World Meteorological Organization";
                 presentationForm      = PresentationForm.DOCUMENT_DIGITAL;
                 break;
             }
-            case "IOGP": {       // Not in public API (see Citations.IOGP 
javadoc)
+            case Constants.IOGP: {  // Not in public API (see Citations.IOGP 
javadoc)
                 title            = "IOGP Surveying and Positioning Guidance 
Note 7";
-                code             = Constants.IOGP;
                 copyFrom         = Constants.EPSG;
                 presentationForm = PresentationForm.DOCUMENT_DIGITAL;
                 break;
@@ -171,7 +167,6 @@ final class MetadataFallback extends MetadataSource {
             case Constants.EPSG: {
                 title                 = "EPSG Geodetic Parameter Dataset";
                 alternateTitle        = "EPSG Dataset";
-                code                  = Constants.EPSG;
                 codeSpace             = Constants.IOGP;
                 citedResponsibleParty = "International Association of Oil & 
Gas producers";
                 presentationForm      = PresentationForm.TABLE_DIGITAL;
@@ -179,32 +174,37 @@ final class MetadataFallback extends MetadataSource {
             }
             case Constants.SIS: {
                 title     = "Apache Spatial Information System";
-                code      = key;
                 codeSpace = "Apache";
                 break;
             }
             case "ISBN": {
                 title = "International Standard Book Number";
                 alternateTitle = key;
+                code = null;
                 break;
             }
             case "ISSN": {
                 title = "International Standard Serial Number";
                 alternateTitle = key;
+                code = null;
                 break;
             }
             case "PROJ": {
                 title     = "PROJ coordinate transformation software library";
-                code      = "PROJ";
                 codeSpace = "OSGeo";
                 break;
             }
             case Constants.GDAL: {
-                title     = "Geospatial Data Abstraction Library";
-                code      = key;
+                title = "Geospatial Data Abstraction Library";
+                alternateTitle = "GDAL";
                 codeSpace = "OSGeo";
                 break;
             }
+            case "Unidata": {
+                title = "Unidata netCDF library";
+                code  = null;
+                break;
+            }
             case "IHO S-57": {
                 title = code     = "S-57";
                 codeSpace        = "IHO";
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/CitationsTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/CitationsTest.java
index 8be419e1a4..1e15401bc4 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/CitationsTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/CitationsTest.java
@@ -91,7 +91,7 @@ public final class CitationsTest extends TestCase {
         assertSame(IOGP,             fromName(Constants.IOGP));
         assertSame(IOGP,             fromName("OGP"));
         assertSame(ESRI,             fromName("ESRI"));          // Handled in 
a way very similar to "OGC".
-        assertSame(NETCDF,           fromName("NetCDF"));
+        assertSame(NETCDF,           fromName(Constants.NETCDF));
         assertSame(GEOTIFF,          fromName(Constants.GEOTIFF));
         assertSame(PROJ4,            fromName("Proj.4"));
         assertSame(PROJ4,            fromName("Proj4"));
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/MetadataFallbackVerifier.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/MetadataFallbackVerifier.java
index a1d4ad0cd1..6ff30f8380 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/MetadataFallbackVerifier.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/MetadataFallbackVerifier.java
@@ -24,6 +24,7 @@ import org.apache.sis.metadata.MetadataStandard;
 import org.apache.sis.metadata.internal.CitationConstant;
 import org.apache.sis.metadata.iso.citation.Citations;
 import static org.apache.sis.util.privy.CollectionsExt.first;
+import org.apache.sis.util.privy.Constants;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
@@ -45,7 +46,7 @@ public final class MetadataFallbackVerifier {
     /**
      * Identifier for which {@link MetadataFallback} does not provide 
hard-coded values.
      */
-    private static final Set<String> EXCLUDES = Set.of("NetCDF", "GeoTIFF", 
"ArcGIS", "MapInfo");
+    private static final Set<String> EXCLUDES = Set.of(Constants.NETCDF, 
Constants.GEOTIFF, "ArcGIS", "MapInfo");
 
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NorthPoleRotation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NorthPoleRotation.java
index 84ab4fecf0..d796a8d838 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NorthPoleRotation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NorthPoleRotation.java
@@ -30,6 +30,7 @@ import org.apache.sis.parameter.Parameters;
 import org.apache.sis.measure.Longitude;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.measure.Units;
+import org.apache.sis.util.privy.Constants;
 
 
 /**
@@ -104,7 +105,7 @@ public final class NorthPoleRotation extends 
AbstractProvider {
      */
     public static final ParameterDescriptorGroup PARAMETERS;
     static {
-        final ParameterBuilder builder = new 
ParameterBuilder().setCodeSpace(Citations.NETCDF, "NetCDF").setRequired(true);
+        final ParameterBuilder builder = new 
ParameterBuilder().setCodeSpace(Citations.NETCDF, 
Constants.NETCDF).setRequired(true);
 
         POLE_LATITUDE = builder
                 .addNameAndIdentifier(Citations.SIS, 
SouthPoleRotation.POLE_LATITUDE)
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/SouthPoleRotation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/SouthPoleRotation.java
index 4720654bef..c945ca7936 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/SouthPoleRotation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/SouthPoleRotation.java
@@ -30,6 +30,7 @@ import org.apache.sis.parameter.Parameters;
 import org.apache.sis.measure.Longitude;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.measure.Units;
+import org.apache.sis.util.privy.Constants;
 
 
 /**
@@ -114,7 +115,7 @@ public final class SouthPoleRotation extends 
AbstractProvider {
      */
     public static final ParameterDescriptorGroup PARAMETERS;
     static {
-        final ParameterBuilder builder = new 
ParameterBuilder().setCodeSpace(Citations.NETCDF, "NetCDF").setRequired(true);
+        final ParameterBuilder builder = new 
ParameterBuilder().setCodeSpace(Citations.NETCDF, 
Constants.NETCDF).setRequired(true);
 
         POLE_LATITUDE = builder
                 .addName(Citations.SIS, "Latitude of rotated pole")
diff --git 
a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/LandsatStore.java
 
b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/LandsatStore.java
index 6f5907a965..6dab2cb236 100644
--- 
a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/LandsatStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/LandsatStore.java
@@ -223,7 +223,7 @@ public class LandsatStore extends DataStore implements 
Aggregate {
             source = null;      // Will be closed at the end of this 
try-finally block.
             final var parser = new MetadataReader(this, getDisplayName(), 
listeners);
             parser.read(reader);
-            parser.addFormatReader(getProvider());
+            parser.addFormatReaderSIS(LandsatStoreProvider.NAME);
             metadata = parser.getMetadata();
             /*
              * Create the array of components. The resource identifier is the 
band name.
diff --git 
a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/MetadataReader.java
 
b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/MetadataReader.java
index ba59a0aa86..a4cf6a0906 100644
--- 
a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/MetadataReader.java
+++ 
b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/MetadataReader.java
@@ -48,7 +48,6 @@ import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.metadata.iso.content.DefaultAttributeGroup;
 import org.apache.sis.metadata.iso.content.DefaultSampleDimension;
 import org.apache.sis.metadata.iso.content.DefaultCoverageDescription;
-import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.storage.DataStoreException;
@@ -469,15 +468,12 @@ final class MetadataReader extends MetadataBuilder {
              * Value is "GEOTIFF".
              */
             case "OUTPUT_FORMAT": {
-                String name = value;
-                if (Constants.GEOTIFF.equalsIgnoreCase(name)) try {
-                    name = Constants.GEOTIFF;       // Because 
`metadata.setPredefinedFormat(…)` is case-sensitive.
-                    setPredefinedFormat(name);
-                    break;
-                } catch (MetadataStoreException e) {
-                    warning(key, null, e);
+                if (Constants.GEOTIFF.equalsIgnoreCase(value)) {
+                    setPredefinedFormat(Constants.GEOTIFF, listeners, true);
+                } else {
+                    addFormatName(value);
                 }
-                addFormatName(name);
+                // Do not invoke `addFormatReaderSIS(name)`, it will be done 
by the caller.
                 break;
             }
             /*
diff --git 
a/endorsed/src/org.apache.sis.storage.earthobservation/test/org/apache/sis/storage/landsat/MetadataReaderTest.java
 
b/endorsed/src/org.apache.sis.storage.earthobservation/test/org/apache/sis/storage/landsat/MetadataReaderTest.java
index 954c104830..31099f6d44 100644
--- 
a/endorsed/src/org.apache.sis.storage.earthobservation/test/org/apache/sis/storage/landsat/MetadataReaderTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.earthobservation/test/org/apache/sis/storage/landsat/MetadataReaderTest.java
@@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import static java.util.Map.entry;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -55,6 +56,16 @@ import org.opengis.test.dataset.ContentVerifier;
  * @author  Martin Desruisseaux (Geomatys)
  */
 public final class MetadataReaderTest extends TestCase {
+    /**
+     * Helper class for verifying metadata content.
+     */
+    private ContentVerifier verifier;
+
+    /**
+     * A buffer for building paths to expected properties.
+     */
+    private StringBuilder buffer;
+
     /**
      * Creates a new test case.
      */
@@ -92,180 +103,139 @@ public final class MetadataReaderTest extends TestCase {
             reader.read(in);
             actual = reader.getMetadata();
         }
-        final ContentVerifier verifier = new ContentVerifier();
+        verifier = new ContentVerifier();
         verifier.addPropertyToIgnore(Metadata.class, "metadataStandard");      
     // Because hard-coded in SIS.
         verifier.addPropertyToIgnore(Metadata.class, "referenceSystemInfo");   
     // Very verbose and depends on EPSG connection.
         verifier.addPropertyToIgnore(TemporalExtent.class, "extent");          
     // Because currently time-zone sensitive.
         verifier.addMetadataToVerify(actual);
         verifier.addExpectedValues(
-            "defaultLocale+otherLocale[0]",                                    
                      "en",
-            "metadataIdentifier.code",                                         
                      "LandsatTest",
-            "metadataScope[0].resourceScope",                                  
                      ScopeCode.COVERAGE,
-            "dateInfo[0].date",                                                
                      OffsetDateTime.of(2016, 6, 27, 16, 48, 12, 0, 
ZoneOffset.UTC),
-            "dateInfo[0].dateType",                                            
                      DateType.CREATION,
-            "identificationInfo[0].topicCategory[0]",                          
                      TopicCategory.GEOSCIENTIFIC_INFORMATION,
-            "identificationInfo[0].citation.date[0].date",                     
                      OffsetDateTime.of(2016, 6, 27, 16, 48, 12, 0, 
ZoneOffset.UTC),
-            "identificationInfo[0].citation.date[0].dateType",                 
                      DateType.CREATION,
-            "identificationInfo[0].citation.title",                            
                      "LandsatTest",
-            "identificationInfo[0].credit[0]",                                 
                      "Derived from U.S. Geological Survey data",
-            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.title",    
         "GeoTIFF Coverage Encoding Profile",
-            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.alternateTitle[0]",
 "GeoTIFF",
-            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].party[0].name",
 "Open Geospatial Consortium",
-            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].role",
 Role.PRINCIPAL_INVESTIGATOR,
-            
"identificationInfo[0].extent[0].geographicElement[0].extentTypeCode",          
         true,
-            
"identificationInfo[0].extent[0].geographicElement[0].westBoundLongitude",      
         108.34,
-            
"identificationInfo[0].extent[0].geographicElement[0].eastBoundLongitude",      
         110.44,
-            
"identificationInfo[0].extent[0].geographicElement[0].southBoundLatitude",      
          10.50,
-            
"identificationInfo[0].extent[0].geographicElement[0].northBoundLatitude",      
          12.62,
-            "identificationInfo[0].spatialResolution[0].distance",             
                       15.0,
-            "identificationInfo[0].spatialResolution[1].distance",             
                       30.0,
-
-            "acquisitionInformation[0].platform[0].identifier.code",           
    "Pseudo LANDSAT",
-            
"acquisitionInformation[0].platform[0].instrument[0].identifier.code", "Pseudo 
TIRS",
-            
"acquisitionInformation[0].acquisitionRequirement[0].identifier.code", 
"Software unit tests",
-            
"acquisitionInformation[0].operation[0].significantEvent[0].context",  
Context.ACQUISITION,
-            "acquisitionInformation[0].operation[0].significantEvent[0].time", 
    OffsetDateTime.of(2016, 6, 26, 3, 2, 1, 90_000_000, ZoneOffset.UTC),
-            "acquisitionInformation[0].operation[0].status",                   
    Progress.COMPLETED,
-            "acquisitionInformation[0].operation[0].type",                     
    OperationType.REAL,
-
-            "contentInfo[0].processingLevelCode.authority.title",          
"Landsat",
-            "contentInfo[0].processingLevelCode.codeSpace",                
"Landsat",
-            "contentInfo[0].processingLevelCode.code",                     
"Pseudo LT1",
-
-            "contentInfo[0].attributeGroup[0].attribute[0].description",   
"Coastal Aerosol",
-            "contentInfo[0].attributeGroup[0].attribute[1].description",   
"Blue",
-            "contentInfo[0].attributeGroup[0].attribute[2].description",   
"Green",
-            "contentInfo[0].attributeGroup[0].attribute[3].description",   
"Red",
-            "contentInfo[0].attributeGroup[0].attribute[4].description",   
"Near-Infrared",
-            "contentInfo[0].attributeGroup[0].attribute[5].description",   
"Short Wavelength Infrared (SWIR) 1",
-            "contentInfo[0].attributeGroup[0].attribute[6].description",   
"Short Wavelength Infrared (SWIR) 2",
-            "contentInfo[0].attributeGroup[0].attribute[7].description",   
"Cirrus",
-            "contentInfo[0].attributeGroup[1].attribute[0].description",   
"Panchromatic",
-            "contentInfo[0].attributeGroup[2].attribute[0].description",   
"Thermal Infrared Sensor (TIRS) 1",
-            "contentInfo[0].attributeGroup[2].attribute[1].description",   
"Thermal Infrared Sensor (TIRS) 2",
+            entry("defaultLocale+otherLocale[0]",   "en"),
+            entry("metadataIdentifier.code",        "LandsatTest"),
+            entry("metadataScope[0].resourceScope", ScopeCode.COVERAGE),
+            entry("dateInfo[0].date",               OffsetDateTime.of(2016, 6, 
27, 16, 48, 12, 0, ZoneOffset.UTC)),
+            entry("dateInfo[0].dateType",           DateType.CREATION),
 
-            "contentInfo[0].attributeGroup[0].attribute[0].minValue",      1.0,
-            "contentInfo[0].attributeGroup[0].attribute[1].minValue",      1.0,
-            "contentInfo[0].attributeGroup[0].attribute[2].minValue",      1.0,
-            "contentInfo[0].attributeGroup[0].attribute[3].minValue",      1.0,
-            "contentInfo[0].attributeGroup[0].attribute[4].minValue",      1.0,
-            "contentInfo[0].attributeGroup[0].attribute[5].minValue",      1.0,
-            "contentInfo[0].attributeGroup[0].attribute[6].minValue",      1.0,
-            "contentInfo[0].attributeGroup[0].attribute[7].minValue",      1.0,
-            "contentInfo[0].attributeGroup[1].attribute[0].minValue",      1.0,
-            "contentInfo[0].attributeGroup[2].attribute[0].minValue",      1.0,
-            "contentInfo[0].attributeGroup[2].attribute[1].minValue",      1.0,
+            entry("identificationInfo[0].topicCategory[0]",          
TopicCategory.GEOSCIENTIFIC_INFORMATION),
+            entry("identificationInfo[0].citation.date[0].date",     
OffsetDateTime.of(2016, 6, 27, 16, 48, 12, 0, ZoneOffset.UTC)),
+            entry("identificationInfo[0].citation.date[0].dateType", 
DateType.CREATION),
+            entry("identificationInfo[0].citation.title",            
"LandsatTest"),
+            entry("identificationInfo[0].credit[0]", "Derived from U.S. 
Geological Survey data"),
 
-            "contentInfo[0].attributeGroup[0].attribute[0].maxValue",      
65535.0,
-            "contentInfo[0].attributeGroup[0].attribute[1].maxValue",      
65535.0,
-            "contentInfo[0].attributeGroup[0].attribute[2].maxValue",      
65535.0,
-            "contentInfo[0].attributeGroup[0].attribute[3].maxValue",      
65535.0,
-            "contentInfo[0].attributeGroup[0].attribute[4].maxValue",      
65535.0,
-            "contentInfo[0].attributeGroup[0].attribute[5].maxValue",      
65535.0,
-            "contentInfo[0].attributeGroup[0].attribute[6].maxValue",      
65535.0,
-            "contentInfo[0].attributeGroup[0].attribute[7].maxValue",      
65535.0,
-            "contentInfo[0].attributeGroup[1].attribute[0].maxValue",      
65535.0,
-            "contentInfo[0].attributeGroup[2].attribute[0].maxValue",      
65535.0,
-            "contentInfo[0].attributeGroup[2].attribute[1].maxValue",      
65535.0,
+            
entry("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.title",
 "GeoTIFF Coverage Encoding Profile"),
+            
entry("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.alternateTitle[0]",
 "GeoTIFF"),
+            
entry("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].party[0].name",
 "Open Geospatial Consortium"),
+            
entry("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].role",
 Role.PRINCIPAL_INVESTIGATOR),
 
-            "contentInfo[0].attributeGroup[0].attribute[0].peakResponse",    
433.0,
-            "contentInfo[0].attributeGroup[0].attribute[1].peakResponse",    
482.0,
-            "contentInfo[0].attributeGroup[0].attribute[2].peakResponse",    
562.0,
-            "contentInfo[0].attributeGroup[0].attribute[3].peakResponse",    
655.0,
-            "contentInfo[0].attributeGroup[0].attribute[4].peakResponse",    
865.0,
-            "contentInfo[0].attributeGroup[0].attribute[5].peakResponse",   
1610.0,
-            "contentInfo[0].attributeGroup[0].attribute[6].peakResponse",   
2200.0,
-            "contentInfo[0].attributeGroup[0].attribute[7].peakResponse",   
1375.0,
-            "contentInfo[0].attributeGroup[1].attribute[0].peakResponse",    
590.0,
-            "contentInfo[0].attributeGroup[2].attribute[0].peakResponse",  
10800.0,
-            "contentInfo[0].attributeGroup[2].attribute[1].peakResponse",  
12000.0,
+            
entry("identificationInfo[0].extent[0].geographicElement[0].extentTypeCode",    
 true),
+            
entry("identificationInfo[0].extent[0].geographicElement[0].westBoundLongitude",
 108.34),
+            
entry("identificationInfo[0].extent[0].geographicElement[0].eastBoundLongitude",
 110.44),
+            
entry("identificationInfo[0].extent[0].geographicElement[0].southBoundLatitude",
  10.50),
+            
entry("identificationInfo[0].extent[0].geographicElement[0].northBoundLatitude",
  12.62),
+            entry("identificationInfo[0].spatialResolution[0].distance", 15.0),
+            entry("identificationInfo[0].spatialResolution[1].distance", 30.0),
 
-            
"contentInfo[0].attributeGroup[0].attribute[0].transferFunctionType",  
TransferFunctionType.LINEAR,
-            
"contentInfo[0].attributeGroup[0].attribute[1].transferFunctionType",  
TransferFunctionType.LINEAR,
-            
"contentInfo[0].attributeGroup[0].attribute[2].transferFunctionType",  
TransferFunctionType.LINEAR,
-            
"contentInfo[0].attributeGroup[0].attribute[3].transferFunctionType",  
TransferFunctionType.LINEAR,
-            
"contentInfo[0].attributeGroup[0].attribute[4].transferFunctionType",  
TransferFunctionType.LINEAR,
-            
"contentInfo[0].attributeGroup[0].attribute[5].transferFunctionType",  
TransferFunctionType.LINEAR,
-            
"contentInfo[0].attributeGroup[0].attribute[6].transferFunctionType",  
TransferFunctionType.LINEAR,
-            
"contentInfo[0].attributeGroup[0].attribute[7].transferFunctionType",  
TransferFunctionType.LINEAR,
-            
"contentInfo[0].attributeGroup[1].attribute[0].transferFunctionType",  
TransferFunctionType.LINEAR,
-            
"contentInfo[0].attributeGroup[2].attribute[0].transferFunctionType",  
TransferFunctionType.LINEAR,
-            
"contentInfo[0].attributeGroup[2].attribute[1].transferFunctionType",  
TransferFunctionType.LINEAR,
+            entry("acquisitionInformation[0].platform[0].identifier.code",     
          "Pseudo LANDSAT"),
+            
entry("acquisitionInformation[0].platform[0].instrument[0].identifier.code", 
"Pseudo TIRS"),
+            
entry("acquisitionInformation[0].acquisitionRequirement[0].identifier.code", 
"Software unit tests"),
+            
entry("acquisitionInformation[0].operation[0].significantEvent[0].context",  
Context.ACQUISITION),
+            
entry("acquisitionInformation[0].operation[0].significantEvent[0].time",     
OffsetDateTime.of(2016, 6, 26, 3, 2, 1, 90_000_000, ZoneOffset.UTC)),
+            entry("acquisitionInformation[0].operation[0].status", 
Progress.COMPLETED),
+            entry("acquisitionInformation[0].operation[0].type",   
OperationType.REAL),
 
-            "contentInfo[0].attributeGroup[0].attribute[0].scaleFactor",  
2.0E-5,
-            "contentInfo[0].attributeGroup[0].attribute[1].scaleFactor",  
2.0E-5,
-            "contentInfo[0].attributeGroup[0].attribute[2].scaleFactor",  
2.0E-5,
-            "contentInfo[0].attributeGroup[0].attribute[3].scaleFactor",  
2.0E-5,
-            "contentInfo[0].attributeGroup[0].attribute[4].scaleFactor",  
2.0E-5,
-            "contentInfo[0].attributeGroup[0].attribute[5].scaleFactor",  
2.0E-5,
-            "contentInfo[0].attributeGroup[0].attribute[6].scaleFactor",  
2.0E-5,
-            "contentInfo[0].attributeGroup[0].attribute[7].scaleFactor",  
2.0E-5,
-            "contentInfo[0].attributeGroup[1].attribute[0].scaleFactor",  
2.0E-5,
-            "contentInfo[0].attributeGroup[2].attribute[0].scaleFactor",  
0.000334,
-            "contentInfo[0].attributeGroup[2].attribute[1].scaleFactor",  
0.000334,
+            entry("contentInfo[0].processingLevelCode.authority.title", 
"Landsat"),
+            entry("contentInfo[0].processingLevelCode.codeSpace",       
"Landsat"),
+            entry("contentInfo[0].processingLevelCode.code",            
"Pseudo LT1"),
 
-            "contentInfo[0].attributeGroup[0].attribute[0].offset",      -0.1,
-            "contentInfo[0].attributeGroup[0].attribute[1].offset",      -0.1,
-            "contentInfo[0].attributeGroup[0].attribute[2].offset",      -0.1,
-            "contentInfo[0].attributeGroup[0].attribute[3].offset",      -0.1,
-            "contentInfo[0].attributeGroup[0].attribute[4].offset",      -0.1,
-            "contentInfo[0].attributeGroup[0].attribute[5].offset",      -0.1,
-            "contentInfo[0].attributeGroup[0].attribute[6].offset",      -0.1,
-            "contentInfo[0].attributeGroup[0].attribute[7].offset",      -0.1,
-            "contentInfo[0].attributeGroup[1].attribute[0].offset",      -0.1,
-            "contentInfo[0].attributeGroup[2].attribute[0].offset",       0.1,
-            "contentInfo[0].attributeGroup[2].attribute[1].offset",       0.1,
+            entry("contentInfo[0].cloudCoverPercentage",        8.3),
+            entry("contentInfo[0].illuminationAzimuthAngle",  116.9),
+            entry("contentInfo[0].illuminationElevationAngle", 58.8),
 
-            "contentInfo[0].attributeGroup[0].attribute[0].units", "",
-            "contentInfo[0].attributeGroup[0].attribute[1].units", "",
-            "contentInfo[0].attributeGroup[0].attribute[2].units", "",
-            "contentInfo[0].attributeGroup[0].attribute[3].units", "",
-            "contentInfo[0].attributeGroup[0].attribute[4].units", "",
-            "contentInfo[0].attributeGroup[0].attribute[5].units", "",
-            "contentInfo[0].attributeGroup[0].attribute[6].units", "",
-            "contentInfo[0].attributeGroup[0].attribute[7].units", "",
-            "contentInfo[0].attributeGroup[1].attribute[0].units", "",
+            entry("spatialRepresentationInfo[0].numberOfDimensions", 2),
+            entry("spatialRepresentationInfo[1].numberOfDimensions", 2),
+            
entry("spatialRepresentationInfo[0].axisDimensionProperties[0].dimensionName", 
DimensionNameType.SAMPLE),
+            
entry("spatialRepresentationInfo[1].axisDimensionProperties[0].dimensionName", 
DimensionNameType.SAMPLE),
+            
entry("spatialRepresentationInfo[0].axisDimensionProperties[1].dimensionName", 
DimensionNameType.LINE),
+            
entry("spatialRepresentationInfo[1].axisDimensionProperties[1].dimensionName", 
DimensionNameType.LINE),
+            
entry("spatialRepresentationInfo[0].axisDimensionProperties[0].dimensionSize",  
7600),
+            
entry("spatialRepresentationInfo[0].axisDimensionProperties[1].dimensionSize",  
7800),
+            
entry("spatialRepresentationInfo[1].axisDimensionProperties[0].dimensionSize", 
15000),
+            
entry("spatialRepresentationInfo[1].axisDimensionProperties[1].dimensionSize", 
15500),
+            
entry("spatialRepresentationInfo[0].transformationParameterAvailability", 
false),
+            
entry("spatialRepresentationInfo[1].transformationParameterAvailability", 
false),
+            entry("spatialRepresentationInfo[0].checkPointAvailability", 
false),
+            entry("spatialRepresentationInfo[1].checkPointAvailability", 
false),
 
-            "contentInfo[0].attributeGroup[0].attribute[0].boundUnits",   "nm",
-            "contentInfo[0].attributeGroup[0].attribute[1].boundUnits",   "nm",
-            "contentInfo[0].attributeGroup[0].attribute[2].boundUnits",   "nm",
-            "contentInfo[0].attributeGroup[0].attribute[3].boundUnits",   "nm",
-            "contentInfo[0].attributeGroup[0].attribute[4].boundUnits",   "nm",
-            "contentInfo[0].attributeGroup[0].attribute[5].boundUnits",   "nm",
-            "contentInfo[0].attributeGroup[0].attribute[6].boundUnits",   "nm",
-            "contentInfo[0].attributeGroup[0].attribute[7].boundUnits",   "nm",
-            "contentInfo[0].attributeGroup[1].attribute[0].boundUnits",   "nm",
-            "contentInfo[0].attributeGroup[2].attribute[0].boundUnits",   "nm",
-            "contentInfo[0].attributeGroup[2].attribute[1].boundUnits",   "nm",
+            entry("resourceLineage[0].source[0].description", "Pseudo GLS"));
 
-            "contentInfo[0].attributeGroup[0].contentType[0]", 
CoverageContentType.PHYSICAL_MEASUREMENT,
-            "contentInfo[0].attributeGroup[1].contentType[0]", 
CoverageContentType.PHYSICAL_MEASUREMENT,
-            "contentInfo[0].attributeGroup[2].contentType[0]", 
CoverageContentType.PHYSICAL_MEASUREMENT,
-
-            "contentInfo[0].cloudCoverPercentage",         8.3,
-            "contentInfo[0].illuminationAzimuthAngle",   116.9,
-            "contentInfo[0].illuminationElevationAngle",  58.8,
-
-            "spatialRepresentationInfo[0].numberOfDimensions",                 
      2,
-            "spatialRepresentationInfo[1].numberOfDimensions",                 
      2,
-            
"spatialRepresentationInfo[0].axisDimensionProperties[0].dimensionName", 
DimensionNameType.SAMPLE,
-            
"spatialRepresentationInfo[1].axisDimensionProperties[0].dimensionName", 
DimensionNameType.SAMPLE,
-            
"spatialRepresentationInfo[0].axisDimensionProperties[1].dimensionName", 
DimensionNameType.LINE,
-            
"spatialRepresentationInfo[1].axisDimensionProperties[1].dimensionName", 
DimensionNameType.LINE,
-            
"spatialRepresentationInfo[0].axisDimensionProperties[0].dimensionSize", 7600,
-            
"spatialRepresentationInfo[0].axisDimensionProperties[1].dimensionSize", 7800,
-            
"spatialRepresentationInfo[1].axisDimensionProperties[0].dimensionSize", 15000,
-            
"spatialRepresentationInfo[1].axisDimensionProperties[1].dimensionSize", 15500,
-            
"spatialRepresentationInfo[0].transformationParameterAvailability",      false,
-            
"spatialRepresentationInfo[1].transformationParameterAvailability",      false,
-            "spatialRepresentationInfo[0].checkPointAvailability",             
      false,
-            "spatialRepresentationInfo[1].checkPointAvailability",             
      false,
-
-            "resourceLineage[0].source[0].description", "Pseudo GLS");
+        /*
+         * The expected values in 
"contentInfo[0].attributeGroup[…].attribute[…].*" have a lot of redundancy.
+         * Therefore, we set those expected values by loop instead of 
repeating tens of long property paths.
+         */
+        final String[] descriptions = {
+            "Coastal Aerosol",
+            "Blue",
+            "Green",
+            "Red",
+            "Near-Infrared",
+            "Short Wavelength Infrared (SWIR) 1",
+            "Short Wavelength Infrared (SWIR) 2",
+            "Cirrus",
+            "Panchromatic",
+            "Thermal Infrared Sensor (TIRS) 1",
+            "Thermal Infrared Sensor (TIRS) 2"
+        };
+        final short[] peakResponses = {433, 482, 562, 655, 865, 1610, 2200, 
1375, 590, 10800, 12000};
+        int band = 0;
 
+        buffer = new 
StringBuilder(80).append("contentInfo[0].attributeGroup[");
+        final int groupBase = buffer.length();
+        final int[] numAttributes = {8, 1, 2};
+        for (int group = 0; group < numAttributes.length; group++) {
+            final boolean mainGroups = (group != 2);
+            /*
+             * contentInfo[0].attributeGroup[0…2].contentType[0]
+             */
+            buffer.setLength(groupBase);
+            buffer.append(group).append("].");
+            addExpectedValue("contentType[0]", 
CoverageContentType.PHYSICAL_MEASUREMENT);
+            /*
+             * contentInfo[0].attributeGroup[0…2].attribute[…].minValue
+             * contentInfo[0].attributeGroup[0…2].attribute[…].maxValue
+             * ... etc ...
+             */
+            final int attributeBase = buffer.append("attribute[").length();
+            for (int attribute = 0; attribute < numAttributes[group]; 
attribute++) {
+                buffer.setLength(attributeBase);
+                buffer.append(attribute).append("].");
+                addExpectedValue("minValue", 1.0);
+                addExpectedValue("maxValue", 65535.0);
+                addExpectedValue("description", descriptions[band]);
+                addExpectedValue("peakResponse", (double) 
peakResponses[band++]);
+                addExpectedValue("boundUnits", "nm");
+                addExpectedValue("transferFunctionType", 
TransferFunctionType.LINEAR);
+                addExpectedValue("scaleFactor", mainGroups ? 2.0E-5 : 
0.000334);
+                addExpectedValue("offset", mainGroups ? -0.1 : 0.1);
+                if (mainGroups) {
+                    addExpectedValue("units", "");
+                }
+            }
+        }
+        assertEquals(descriptions.length, band);
+        assertEquals(peakResponses.length, band);
         verifier.assertMetadataEquals();
     }
 
+    /**
+     * Adds an expected value for the given property. The path to that 
property is the
+     * current content of {@link #buffer}, including a trailing {@code '.'} 
separator.
+     * The buffer is reset to its original length after this method call.
+     */
+    private void addExpectedValue(final String tip, Object value) {
+        final int length = buffer.length();
+        verifier.addExpectedValue(buffer.append(tip).toString(), value);
+        buffer.setLength(length);
+    }
+
     /**
      * Creates a dummy set of store listeners.
      * Used only for constructors that require a non-null {@link 
StoreListeners} instance.
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index d16c7bd3b5..9ddabf81b3 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -21,7 +21,6 @@ import java.util.List;
 import java.util.Locale;
 import java.util.TimeZone;
 import java.util.Optional;
-import java.util.logging.Level;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.net.URI;
@@ -62,7 +61,6 @@ import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.metadata.iso.DefaultMetadata;
-import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -382,13 +380,8 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
      * Sets the {@code metadata/identificationInfo/resourceFormat} node to 
"GeoTIFF" format.
      */
     final void setFormatInfo(final MetadataBuilder builder) {
-        try {
-            builder.setPredefinedFormat(Constants.GEOTIFF);
-        } catch (MetadataStoreException e) {
-            builder.addFormatName(Constants.GEOTIFF);
-            listeners.warning(Level.FINE, null, e);
-        }
-        builder.addFormatReader(getProvider());
+        builder.setPredefinedFormat(Constants.GEOTIFF, listeners, true);
+        builder.addFormatReaderSIS(Constants.GEOTIFF);
         builder.addLanguage(Locale.ENGLISH, encoding, 
MetadataBuilder.Scope.METADATA);
         builder.addResourceScope(ScopeCode.COVERAGE, null);
     }
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java
index 1d257bc899..890cf53904 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java
@@ -25,7 +25,6 @@ import java.util.LinkedHashSet;
 import java.util.LinkedHashMap;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.logging.Level;
 import java.io.IOException;
 import java.time.temporal.Temporal;
 import ucar.nc2.constants.CF;       // String constants are copied by the 
compiler with no UCAR reference left.
@@ -51,7 +50,6 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.metadata.iso.citation.*;
 import org.apache.sis.metadata.iso.identification.*;
-import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.event.StoreListeners;
@@ -69,6 +67,7 @@ import org.apache.sis.system.Configuration;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.util.privy.CollectionsExt;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.CodeLists;
 import org.apache.sis.util.privy.Strings;
 import org.apache.sis.util.resources.Errors;
@@ -656,22 +655,30 @@ split:  while ((start = 
CharSequences.skipLeadingWhitespaces(value, start, lengt
             addBoundingPolygon(new StoreFormat(null, null, decoder.geomlib, 
decoder.listeners).parseGeometry(wkt,
                     stringValue(GEOSPATIAL_BOUNDS + "_crs"), 
stringValue(GEOSPATIAL_BOUNDS + "_vertical_crs")));
         }
-        final String[] format = decoder.getFormatDescription();
-        String id = format[0];
-        if (NetcdfStoreProvider.NAME.equalsIgnoreCase(id)) try {
-            setPredefinedFormat(NetcdfStoreProvider.NAME);
-            id = null;
-        } catch (MetadataStoreException e) {
-            // Will add `id` at the end of this method.
-            decoder.listeners.warning(Level.FINE, null, e);
-        }
-        if (format.length >= 2) {
-            addFormatName(format[1]);
-            if (format.length >= 3) {
-                setFormatEdition(format[2]);
-            }
+        /*
+         * Add a description of the format. The description is determined by 
the decoder in use.
+         * That decoder may itself infer that description from another library 
such as UCAR.
+         */
+        decoder.addFormatDescription(this);
+    }
+
+    /**
+     * Adds the format description with a check about whether the given format 
identifier is recognized.
+     * This is a helper method for {@link 
Decoder#addFormatDescription(MetadataBuilder)} implementations.
+     *
+     * @param  format     format identifier. Recognized value is {@value 
NetcdfStoreProvider#NAME}.
+     * @param  listeners  ignored. Will be replaced by the listeners of the 
decoder.
+     * @param  fallback   whether to use a fallback if the description was not 
found.
+     * @return whether the format description has been added.
+     */
+    @Override
+    public boolean setPredefinedFormat(String format, StoreListeners 
listeners, boolean fallback) {
+        if (Constants.NETCDF.equalsIgnoreCase(format)) {
+            return super.setPredefinedFormat(format, decoder.listeners, 
fallback);
+        } else if (fallback) {
+            addFormatName(format);
         }
-        addFormatName(id);          // Do nothing is `id` is null.
+        return false;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java
index 21fb0746c5..39f7c2229b 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java
@@ -48,6 +48,7 @@ import org.apache.sis.setup.OptionKey;
 import org.apache.sis.util.Version;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.privy.Strings;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.UnmodifiableArrayList;
 import org.apache.sis.util.collection.DefaultTreeTable;
 import org.apache.sis.util.collection.TableColumn;
@@ -117,7 +118,7 @@ public class NetcdfStore extends DataStore implements 
Aggregate {
             throw new DataStoreException(e);
         }
         if (decoder == null) {
-            throw new UnsupportedStorageException(super.getLocale(), 
NetcdfStoreProvider.NAME,
+            throw new UnsupportedStorageException(super.getLocale(), 
Constants.NETCDF,
                     connector.getStorage(), 
connector.getOption(OptionKey.OPEN_OPTIONS));
         }
         decoder.location = path;
@@ -217,7 +218,7 @@ public class NetcdfStore extends DataStore implements 
Aggregate {
     public Optional<TreeTable> getNativeMetadata() throws DataStoreException {
         final DefaultTreeTable table = new DefaultTreeTable(TableColumn.NAME, 
TableColumn.VALUE);
         final TreeTable.Node root = table.getRoot();
-        root.setValue(TableColumn.NAME, NetcdfStoreProvider.NAME);
+        root.setValue(TableColumn.NAME, Constants.NETCDF);
         decoder().addAttributesTo(root);
         return Optional.of(table);
     }
@@ -294,7 +295,7 @@ public class NetcdfStore extends DataStore implements 
Aggregate {
     private Decoder decoder() throws DataStoreClosedException {
         final Decoder reader = decoder;
         if (reader == null) {
-            throw new DataStoreClosedException(getLocale(), 
NetcdfStoreProvider.NAME, StandardOpenOption.READ);
+            throw new DataStoreClosedException(getLocale(), Constants.NETCDF, 
StandardOpenOption.READ);
         }
         return reader;
     }
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
index 8acabbcba4..2a37675b3b 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
@@ -52,6 +52,7 @@ import org.apache.sis.setup.OptionKey;
 import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.Version;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.privy.Constants;
 
 
 /**
@@ -72,7 +73,7 @@ import org.apache.sis.util.logging.Logging;
  *
  * @since 0.3
  */
-@StoreMetadata(formatName    = NetcdfStoreProvider.NAME,
+@StoreMetadata(formatName    = Constants.NETCDF,
                fileSuffixes  = "nc",
                capabilities  = Capability.READ,
                resourceTypes = {Aggregate.class, FeatureSet.class, 
GridCoverageResource.class},
@@ -85,11 +86,6 @@ import org.apache.sis.util.logging.Logging;
  * specialized readers to be tested before this generic netCDF reader.
  */
 public class NetcdfStoreProvider extends DataStoreProvider {
-    /**
-     * The format name.
-     */
-    static final String NAME = "NetCDF";
-
     /**
      * The MIME type for netCDF files.
      */
@@ -98,7 +94,7 @@ public class NetcdfStoreProvider extends DataStoreProvider {
     /**
      * The parameter descriptor to be returned by {@link #getOpenParameters()}.
      */
-    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR = 
URIDataStoreProvider.descriptor(NAME);
+    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR = 
URIDataStoreProvider.descriptor(Constants.NETCDF);
 
     /**
      * The name of the {@link ucar.nc2.NetcdfFile} class, which is {@value}.
@@ -161,7 +157,7 @@ public class NetcdfStoreProvider extends DataStoreProvider {
      */
     @Override
     public String getShortName() {
-        return NAME;
+        return Constants.NETCDF;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
index 545f8c968b..14bf4b1c2a 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
@@ -39,6 +39,7 @@ import org.apache.sis.system.Modules;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.storage.netcdf.internal.Resources;
 import org.apache.sis.util.Utilities;
@@ -233,19 +234,15 @@ public abstract class Decoder extends 
ReferencingFactoryContainer {
     public abstract String getFilename();
 
     /**
-     * Returns an identification of the file format. This method should 
returns an array of length 1, 2 or 3 as below:
+     * Adds to the given metadata an identification of the file format.
+     * Subclasses should invoke the following methods:
      *
      * <ul>
-     *   <li>One of the following identifier in the first element: {@code 
"NetCDF"}, {@code "NetCDF-4"} or other values
-     *       defined by the UCAR library. If known, it will be used as an 
identifier for a more complete description to
-     *       be provided by {@link 
org.apache.sis.metadata.sql.MetadataSource#lookup(Class, String)}.</li>
-     *   <li>Optionally a human-readable description in the second array 
element.</li>
-     *   <li>Optionally a version in the third array element.</li>
+     *   <li>{@link MetadataBuilder#setPredefinedFormat(String, 
StoreListeners, boolean)}</li>
+     *   <li>{@link MetadataBuilder#addFormatReaderSIS(String)} (if 
applicable)</li>
      * </ul>
-     *
-     * @return identification of the file format, human-readable description 
and version number.
      */
-    public abstract String[] getFormatDescription();
+    public abstract void addFormatDescription(MetadataBuilder builder);
 
     /**
      * Defines the groups where to search for named attributes, in preference 
order.
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java
index e920decb07..c8430ff09c 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java
@@ -45,6 +45,7 @@ import 
org.opengis.parameter.InvalidParameterCardinalityException;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.netcdf.base.DataType;
 import org.apache.sis.storage.netcdf.base.Decoder;
 import org.apache.sis.storage.netcdf.base.Node;
@@ -262,8 +263,9 @@ public final class ChannelDecoder extends Decoder {
          * Read the dimension, attribute and variable declarations. We expect 
exactly 3 lists,
          * where any of them can be flagged as absent by a long (64 bits) 0.
          */
-        DimensionInfo[] dimensions = null;
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         VariableInfo[]  variables  = null;
+        DimensionInfo[] dimensions = null;
         List<Map.Entry<String,Object>> attributes = List.of();
         for (int i=0; i<3; i++) {
             final long tn = input.readLong();                   // Combination 
of tag and nelems
@@ -676,14 +678,13 @@ public final class ChannelDecoder extends Decoder {
     }
 
     /**
-     * Returns an identification of the file format. The returned value is a 
reference to a database entry
+     * Sets an identification of the file format. This method uses a reference 
to a database entry
      * known to {@link 
org.apache.sis.metadata.sql.MetadataSource#lookup(Class, String)}.
-     *
-     * @return an identification of the file format in an array of length 1.
      */
     @Override
-    public String[] getFormatDescription() {
-        return new String[] {"NetCDF"};
+    public void addFormatDescription(MetadataBuilder builder) {
+        builder.setPredefinedFormat(Constants.NETCDF, null, true);
+        builder.addFormatReaderSIS(Constants.NETCDF);
     }
 
     /**
@@ -719,6 +720,7 @@ public final class ChannelDecoder extends Decoder {
      * @return dimension of the given name, or {@code null} if none.
      */
     @Override
+    @SuppressWarnings("StringEquality")
     protected Dimension findDimension(final String dimName) {
         DimensionInfo dim = dimensionMap.get(dimName);          // Give 
precedence to exact match before to ignore case.
         if (dim == null) {
@@ -736,6 +738,7 @@ public final class ChannelDecoder extends Decoder {
      * @param  name  the name of the variable to search, or {@code null}.
      * @return the variable of the given name, or {@code null} if none.
      */
+    @SuppressWarnings("StringEquality")
     private VariableInfo findVariableInfo(final String name) {
         VariableInfo v = variableMap.get(name);
         if (v == null && name != null) {
@@ -782,6 +785,7 @@ public final class ChannelDecoder extends Decoder {
      *
      * @see #getAttributeNames()
      */
+    @SuppressWarnings("StringEquality")
     private Object findAttribute(final String name) {
         if (name == null) {
             return null;
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/DecoderWrapper.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/DecoderWrapper.java
index b62529d2d7..5f813e9e2f 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/DecoderWrapper.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/DecoderWrapper.java
@@ -42,11 +42,19 @@ import ucar.nc2.ft.FeatureDataset;
 import ucar.nc2.ft.FeatureDatasetPoint;
 import ucar.nc2.ft.FeatureDatasetFactoryManager;
 import ucar.nc2.ft.DsgFeatureCollection;
+import org.opengis.metadata.citation.Citation;
+import org.apache.sis.util.Version;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.metadata.sql.MetadataSource;
+import org.apache.sis.metadata.sql.MetadataStoreException;
+import org.apache.sis.referencing.ImmutableIdentifier;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.base.MetadataBuilder;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.storage.netcdf.base.Decoder;
 import org.apache.sis.storage.netcdf.base.Variable;
 import org.apache.sis.storage.netcdf.base.Dimension;
@@ -55,7 +63,6 @@ import org.apache.sis.storage.netcdf.base.Grid;
 import org.apache.sis.storage.netcdf.base.Convention;
 import org.apache.sis.storage.netcdf.base.DiscreteSampling;
 import org.apache.sis.setup.GeometryLibrary;
-import org.apache.sis.storage.event.StoreListeners;
 
 
 /**
@@ -64,6 +71,17 @@ import org.apache.sis.storage.event.StoreListeners;
  * @author  Martin Desruisseaux (Geomatys)
  */
 public final class DecoderWrapper extends Decoder implements CancelTask {
+    /**
+     * Version of the <abbr>UCAR</abbr> library, fetched when first requested.
+     * May be {@code null} if no version information was found.
+     */
+    private static Version version;
+
+    /**
+     * Whether {@link #version} has been initialized. The result may still be 
null.
+     */
+    private static boolean versionInitialized;
+
     /**
      * The netCDF file to read.
      * This file is set at construction time.
@@ -171,21 +189,45 @@ public final class DecoderWrapper extends Decoder 
implements CancelTask {
 
     /**
      * Returns the file format information provided by the UCAR library.
+     * The information includes:
+     *
+     * <ol>
+     *   <li>{@code "NetCDF"}, {@code "NetCDF-4"} or other values defined by 
the UCAR library.
+     *       If known, it will be used as an identifier for a more complete 
description to be
+     *       provided by {@link 
org.apache.sis.metadata.sql.MetadataSource#lookup(Class, String)}.</li>
+     *   <li>Optionally a human-readable description.</li>
+     *   <li>Optionally a file format version.</li>
+     * </ol>
      *
      * @return identification of the file format, human-readable description 
and version number.
      */
     @Override
-    @SuppressWarnings("fallthrough")
-    public String[] getFormatDescription() {
-        final String version = Utils.nonEmpty(file.getFileTypeVersion());
-        final String[] format = new String[version != null ? 3 : 2];
-        switch (format.length) {
-            default: format[2] = version;                           // 
Fallthrough everywhere.
-            case 2:  format[1] = file.getFileTypeDescription();
-            case 1:  format[0] = file.getFileTypeId();
-            case 0:  break;                                         // As a 
matter of principle.
+    public void addFormatDescription(MetadataBuilder builder) {
+        String name = Utils.nonEmpty(file.getFileTypeId());
+        if (builder.setPredefinedFormat(name, null, false)) {
+            name = null;
         }
-        return format;
+        builder.addFormatName(Utils.nonEmpty(file.getFileTypeDescription()));
+        builder.setFormatEdition(Utils.nonEmpty(file.getFileTypeVersion()));
+        builder.addFormatName(name);        // Do nothing if `name` is null.
+        Citation provider;
+        try {
+            provider = MetadataSource.getProvided().lookup(Citation.class, 
"Unidata");
+        } catch (MetadataStoreException e) {
+            provider = null;
+        }
+        builder.addFormatReader(new ImmutableIdentifier(provider, "UCAR", 
Constants.NETCDF), getVersion());
+    }
+
+    /**
+     * Returns the version number of the netCDF library, or {@code null} if 
not found.
+     */
+    private static synchronized Version getVersion() {
+        if (!versionInitialized) {
+            versionInitialized = true;
+            version = Version.ofLibrary(NetcdfFile.class).orElse(null);
+        }
+        return version;
     }
 
     /**
@@ -194,6 +236,7 @@ public final class DecoderWrapper extends Decoder 
implements CancelTask {
      */
     @Override
     public void setSearchPath(final String... groupNames) {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final Group[] groups = new Group[groupNames.length];
         int count = 0;
         for (final String name : groupNames) {
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/MetadataReaderTest.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/MetadataReaderTest.java
index 3550322273..ceabb95e9e 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/MetadataReaderTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/MetadataReaderTest.java
@@ -22,6 +22,7 @@ import java.time.LocalDateTime;
 import java.time.temporal.Temporal;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.citation.Role;
+import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.citation.DateType;
 import org.opengis.metadata.extent.TemporalExtent;
 import org.opengis.metadata.identification.KeywordType;
@@ -41,6 +42,7 @@ import org.apache.sis.storage.netcdf.base.TestCase;
 import org.apache.sis.storage.netcdf.classic.ChannelDecoderTest;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import static java.util.Map.entry;
 import org.opengis.test.dataset.ContentVerifier;
 import org.opengis.test.dataset.TestData;
 
@@ -118,66 +120,70 @@ public final class MetadataReaderTest extends TestCase {
      * @param  ucar      whether the UCAR wrapper is used.
      */
     static ContentVerifier compareToExpected(final Metadata actual, final 
boolean ucar) {
-        final ContentVerifier verifier = new ContentVerifier();
+        final var verifier = new ContentVerifier();
         verifier.addPropertyToIgnore(Metadata.class, "metadataStandard");
         verifier.addPropertyToIgnore(Metadata.class, "referenceSystemInfo");
+        verifier.addPropertyToIgnore(Citation.class, "otherCitationDetails");  
 // "Read by Foo version XYZ" in format citation.
         verifier.addPropertyToIgnore(TemporalExtent.class, "extent");
+        verifier.addPropertyToIgnore((path) -> 
path.equals("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.identifier[0].authority"));
         verifier.addMetadataToVerify(actual);
         verifier.addExpectedValues(
             // Hard-coded
-            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.alternateTitle[0]",
 "NetCDF",
-            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.title", 
"NetCDF Classic and 64-bit Offset Format",
-            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].party[0].name",
 "Open Geospatial Consortium",
-            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].role",
 Role.PRINCIPAL_INVESTIGATOR,
+            
entry("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.alternateTitle[0]",
 "NetCDF"),
+            
entry("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.title",
 "NetCDF Classic and 64-bit Offset Format"),
+            
entry("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].party[0].name",
 "Open Geospatial Consortium"),
+            
entry("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].role",
 Role.PRINCIPAL_INVESTIGATOR),
+            
entry("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.identifier[0].codeSpace",
 ucar ? "UCAR" : "SIS"),
+            
entry("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.identifier[0].code",
 "NetCDF"),
 
             // Read from the file
-            "dateInfo[0].date",                                                
        actual(LocalDateTime.of(2018, 5, 15, 13, 1), ucar),
-            "dateInfo[0].dateType",                                            
        DateType.REVISION,
-            "metadataScope[0].resourceScope",                                  
        ScopeCode.DATASET,
-            "identificationInfo[0].abstract",                                  
        "Global, two-dimensional model data",
-            "identificationInfo[0].purpose",                                   
        "GeoAPI conformance tests",
-            "identificationInfo[0].supplementalInformation",                   
        "For testing purpose only.",
-            "identificationInfo[0].citation.title",                            
        "Test data from Sea Surface Temperature Analysis Model",
-            "identificationInfo[0].descriptiveKeywords[0].keyword[0]",         
        "EARTH SCIENCE > Oceans > Ocean Temperature > Sea Surface Temperature",
-            
"identificationInfo[0].descriptiveKeywords[0].thesaurusName.title",        
"GCMD Science Keywords",
-            "identificationInfo[0].descriptiveKeywords[0].type",               
        KeywordType.THEME,
-            "identificationInfo[0].pointOfContact[0].role",                    
        Role.POINT_OF_CONTACT,
-            "identificationInfo[0].pointOfContact[0].party[0].name",           
        "NOAA/NWS/NCEP",
-            "identificationInfo[0].citation.citedResponsibleParty[0].role",    
        Role.ORIGINATOR,
-            
"identificationInfo[0].citation.citedResponsibleParty[0].party[0].name",   
"NOAA/NWS/NCEP",
-            "identificationInfo[0].citation.date[0].date",                     
        actual(LocalDateTime.of(2005, 9, 22,  0, 0), ucar),
-            "identificationInfo[0].citation.date[1].date",                     
        actual(LocalDateTime.of(2018, 5, 15, 13, 0), ucar),
-            "identificationInfo[0].citation.date[0].dateType",                 
        DateType.CREATION,
-            "identificationInfo[0].citation.date[1].dateType",                 
        DateType.REVISION,
-            "identificationInfo[0].citation.identifier[0].code",               
        "NCEP/SST/Global_5x2p5deg/SST_Global_5x2p5deg_20050922_0000.nc",
-            "identificationInfo[0].citation.identifier[0].authority.title",    
        "edu.ucar.unidata",
-            "identificationInfo[0].resourceConstraints[0].useLimitation[0]",   
        "Freely available",
-            
"identificationInfo[0].extent[0].geographicElement[0].extentTypeCode",     
Boolean.TRUE,
-            
"identificationInfo[0].extent[0].geographicElement[0].westBoundLongitude", 
-180.0,
-            
"identificationInfo[0].extent[0].geographicElement[0].eastBoundLongitude",  
180.0,
-            
"identificationInfo[0].extent[0].geographicElement[0].southBoundLatitude",  
-90.0,
-            
"identificationInfo[0].extent[0].geographicElement[0].northBoundLatitude",   
90.0,
-            "identificationInfo[0].extent[0].verticalElement[0].maximumValue", 
           0.0,
-            "identificationInfo[0].extent[0].verticalElement[0].minimumValue", 
           0.0,
-            "identificationInfo[0].spatialRepresentationType[0]",              
        SpatialRepresentationType.GRID,
-            "spatialRepresentationInfo[0].cellGeometry",                       
        CellGeometry.AREA,
-            "spatialRepresentationInfo[0].numberOfDimensions",                 
        2,
-            
"spatialRepresentationInfo[0].axisDimensionProperties[0].dimensionName",   
DimensionNameType.COLUMN,
-            
"spatialRepresentationInfo[0].axisDimensionProperties[1].dimensionName",   
DimensionNameType.ROW,
-            
"spatialRepresentationInfo[0].axisDimensionProperties[0].dimensionSize",   73,
-            
"spatialRepresentationInfo[0].axisDimensionProperties[1].dimensionSize",   73,
-            
"spatialRepresentationInfo[0].transformationParameterAvailability",        
false,
+            entry("dateInfo[0].date",                                          
              actual(LocalDateTime.of(2018, 5, 15, 13, 1), ucar)),
+            entry("dateInfo[0].dateType",                                      
              DateType.REVISION),
+            entry("metadataScope[0].resourceScope",                            
              ScopeCode.DATASET),
+            entry("identificationInfo[0].abstract",                            
              "Global, two-dimensional model data"),
+            entry("identificationInfo[0].purpose",                             
              "GeoAPI conformance tests"),
+            entry("identificationInfo[0].supplementalInformation",             
              "For testing purpose only."),
+            entry("identificationInfo[0].citation.title",                      
              "Test data from Sea Surface Temperature Analysis Model"),
+            entry("identificationInfo[0].descriptiveKeywords[0].keyword[0]",   
              "EARTH SCIENCE > Oceans > Ocean Temperature > Sea Surface 
Temperature"),
+            
entry("identificationInfo[0].descriptiveKeywords[0].thesaurusName.title",       
 "GCMD Science Keywords"),
+            entry("identificationInfo[0].descriptiveKeywords[0].type",         
              KeywordType.THEME),
+            entry("identificationInfo[0].pointOfContact[0].role",              
              Role.POINT_OF_CONTACT),
+            entry("identificationInfo[0].pointOfContact[0].party[0].name",     
              "NOAA/NWS/NCEP"),
+            
entry("identificationInfo[0].citation.citedResponsibleParty[0].role",           
 Role.ORIGINATOR),
+            
entry("identificationInfo[0].citation.citedResponsibleParty[0].party[0].name",  
 "NOAA/NWS/NCEP"),
+            entry("identificationInfo[0].citation.date[0].date",               
              actual(LocalDateTime.of(2005, 9, 22,  0, 0), ucar)),
+            entry("identificationInfo[0].citation.date[1].date",               
              actual(LocalDateTime.of(2018, 5, 15, 13, 0), ucar)),
+            entry("identificationInfo[0].citation.date[0].dateType",           
              DateType.CREATION),
+            entry("identificationInfo[0].citation.date[1].dateType",           
              DateType.REVISION),
+            entry("identificationInfo[0].citation.identifier[0].code",         
              "NCEP/SST/Global_5x2p5deg/SST_Global_5x2p5deg_20050922_0000.nc"),
+            
entry("identificationInfo[0].citation.identifier[0].authority.title",           
 "edu.ucar.unidata"),
+            
entry("identificationInfo[0].resourceConstraints[0].useLimitation[0]",          
 "Freely available"),
+            
entry("identificationInfo[0].extent[0].geographicElement[0].extentTypeCode",    
 Boolean.TRUE),
+            
entry("identificationInfo[0].extent[0].geographicElement[0].westBoundLongitude",
 -180.0),
+            
entry("identificationInfo[0].extent[0].geographicElement[0].eastBoundLongitude",
  180.0),
+            
entry("identificationInfo[0].extent[0].geographicElement[0].southBoundLatitude",
  -90.0),
+            
entry("identificationInfo[0].extent[0].geographicElement[0].northBoundLatitude",
   90.0),
+            
entry("identificationInfo[0].extent[0].verticalElement[0].maximumValue",        
    0.0),
+            
entry("identificationInfo[0].extent[0].verticalElement[0].minimumValue",        
    0.0),
+            entry("identificationInfo[0].spatialRepresentationType[0]",        
              SpatialRepresentationType.GRID),
+            entry("spatialRepresentationInfo[0].cellGeometry",                 
              CellGeometry.AREA),
+            entry("spatialRepresentationInfo[0].numberOfDimensions",           
              2),
+            
entry("spatialRepresentationInfo[0].axisDimensionProperties[0].dimensionName",  
 DimensionNameType.COLUMN),
+            
entry("spatialRepresentationInfo[0].axisDimensionProperties[1].dimensionName",  
 DimensionNameType.ROW),
+            
entry("spatialRepresentationInfo[0].axisDimensionProperties[0].dimensionSize",  
 73),
+            
entry("spatialRepresentationInfo[0].axisDimensionProperties[1].dimensionSize",  
 73),
+            
entry("spatialRepresentationInfo[0].transformationParameterAvailability",       
 false),
 
             // Variable descriptions (only one in this test).
-            
"contentInfo[0].attributeGroup[0].attribute[0].sequenceIdentifier",        
"SST",
-            "contentInfo[0].attributeGroup[0].attribute[0].description",       
        "Sea temperature",
-            "contentInfo[0].attributeGroup[0].attribute[0].name[0].code",      
        "sea_water_temperature",
-            
"contentInfo[0].attributeGroup[0].attribute[0].transferFunctionType",      
TransferFunctionType.LINEAR,
-            "contentInfo[0].attributeGroup[0].attribute[0].scaleFactor",       
        0.0011,
-            "contentInfo[0].attributeGroup[0].attribute[0].offset",            
        -1.85,
-            "contentInfo[0].attributeGroup[0].attribute[0].units",             
        "°C",
+            
entry("contentInfo[0].attributeGroup[0].attribute[0].sequenceIdentifier",       
 "SST"),
+            entry("contentInfo[0].attributeGroup[0].attribute[0].description", 
              "Sea temperature"),
+            
entry("contentInfo[0].attributeGroup[0].attribute[0].name[0].code",             
 "sea_water_temperature"),
+            
entry("contentInfo[0].attributeGroup[0].attribute[0].transferFunctionType",     
 TransferFunctionType.LINEAR),
+            entry("contentInfo[0].attributeGroup[0].attribute[0].scaleFactor", 
              0.0011),
+            entry("contentInfo[0].attributeGroup[0].attribute[0].offset",      
              -1.85),
+            entry("contentInfo[0].attributeGroup[0].attribute[0].units",       
              "°C"),
 
-            "resourceLineage[0].statement", "Decimated and modified by GeoAPI 
for inclusion in conformance test suite.");
+            entry("resourceLineage[0].statement", "Decimated and modified by 
GeoAPI for inclusion in conformance test suite."));
 
         return verifier;
     }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
index a6acdad992..258a05277a 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
@@ -97,7 +97,6 @@ import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.storage.AbstractResource;
 import org.apache.sis.storage.AbstractFeatureSet;
 import org.apache.sis.storage.AbstractGridCoverageResource;
-import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.storage.internal.Resources;
@@ -1037,34 +1036,34 @@ public class MetadataBuilder {
      * </ul>
      *
      * This method should be invoked <strong>before</strong> any other method 
writing in the
-     * {@code identificationInfo/resourceFormat} node. If this exception 
throws an exception,
-     * than that exception should be reported as a warning. Example:
-     *
-     * {@snippet lang="java" :
-     *     try {
-     *         metadata.setPredefinedFormat("MyFormat");
-     *     } catch (MetadataStoreException e) {
-     *         metadata.addFormatName("MyFormat");
-     *         listeners.warning(Level.FINE, null, e);
-     *     }
-     *     metadata.addCompression("decompression technique");
-     *     }
+     * {@code identificationInfo/resourceFormat} node.
      *
      * @param  abbreviation  the format short name or abbreviation, or {@code 
null} for no-operation.
-     * @throws MetadataStoreException  if this method cannot connect to the 
{@code jdbc/SpatialMetadata} database.
-     *         Callers should generally handle this exception as a recoverable 
one (i.e. log a warning and continue).
+     * @param  listeners     where to report a failure to connect to the 
{@code jdbc/SpatialMetadata} database.
+     * @param  fallback      whether to fallback on {@link 
#addFormatName(String)} if the description was not found.
+     * @return whether the format description has been added.
      *
      * @see #addCompression(CharSequence)
      * @see #addFormatName(CharSequence)
      */
-    public final void setPredefinedFormat(final String abbreviation) throws 
MetadataStoreException {
+    public boolean setPredefinedFormat(final String abbreviation, final 
StoreListeners listeners, boolean fallback) {
         if (abbreviation != null && abbreviation.length() != 0) {
-            if (format == null) {
+            if (format == null) try {
                 format = MetadataSource.getProvided().lookup(Format.class, 
abbreviation);
-            } else {
+                return true;
+            } catch (MetadataStoreException e) {
+                if (listeners != null) {
+                    listeners.warning(Level.FINE, null, e);
+                } else {
+                    Logging.recoverableException(StoreUtilities.LOGGER, null, 
null, e);
+                }
+            }
+            if (fallback) {
                 addFormatName(abbreviation);
+                return true;
             }
         }
+        return false;
     }
 
     /**
@@ -3028,12 +3027,11 @@ public class MetadataBuilder {
      *   <li>{@code 
metadata/identificationInfo/resourceFormat/formatSpecificationCitation/alternateTitle}</li>
      * </ul>
      *
-     * If this method is used together with {@link 
#setPredefinedFormat(String)},
-     * then {@code setPredefinedFormat(…)} should be invoked 
<strong>before</strong> this method.
+     * If this method is used together with {@link #setPredefinedFormat 
setPredefinedFormat(…)},
+     * then the predefined format should be set <strong>before</strong> this 
method.
      *
      * @param value  the format name, or {@code null} for no-operation.
      *
-     * @see #setPredefinedFormat(String)
      * @see #setFormatEdition(CharSequence)
      * @see #addCompression(CharSequence)
      */
@@ -3073,12 +3071,11 @@ public class MetadataBuilder {
      *   <li>{@code 
metadata/identificationInfo/resourceFormat/formatSpecificationCitation/edition}</li>
      * </ul>
      *
-     * If this method is used together with {@link 
#setPredefinedFormat(String)},
-     * then {@code setPredefinedFormat(…)} should be invoked 
<strong>before</strong> this method.
+     * If this method is used together with {@link #setPredefinedFormat 
setPredefinedFormat(…)},
+     * then the predefined format should be set <strong>before</strong> this 
method.
      *
      * @param value  the format edition, or {@code null} for no-operation.
      *
-     * @see #setPredefinedFormat(String)
      * @see #addFormatName(CharSequence)
      */
     public final void setFormatEdition(final CharSequence value) {
@@ -3097,19 +3094,29 @@ public class MetadataBuilder {
      *   <li>{@code 
metadata/identificationInfo/resourceFormat/formatSpecificationCitation/otherCitationDetails}</li>
      * </ul>
      *
-     * If this method is used together with {@link 
#setPredefinedFormat(String)},
-     * then {@code setPredefinedFormat(…)} should be invoked 
<strong>before</strong> this method.
+     * If this method is used together with {@link #setPredefinedFormat 
setPredefinedFormat(…)},
+     * then the predefined format should be set <strong>before</strong> this 
method.
      *
      * @param driver   library-specific way to identify the format (mandatory).
      * @param version  the library version, or {@code null} if unknown.
      */
     public final void addFormatReader(final Identifier driver, final Version 
version) {
+        CharSequence title = null;
+        Citation authority = driver.getAuthority();
+        if (authority != null) {
+            title = authority.getTitle();
+            if (title != null) {
+                for (CharSequence t : authority.getAlternateTitles()) {
+                    if (t.length() < title.length()) {
+                        title = t;      // Alternate titles are often 
abbreviations.
+                    }
+                }
+            }
+        }
         final DefaultCitation c = getFormatCitation();
         addIfNotPresent(c.getIdentifiers(), driver);
         addIfNotPresent(c.getOtherCitationDetails(),
-                Resources.formatInternational(
-                        Resources.Keys.ReadBy_2,
-                        driver.getCodeSpace(),
+                Resources.formatInternational(Resources.Keys.ReadBy_2, (title 
!= null) ? title : driver.getCodeSpace(),
                         (version != null) ? version : 
Vocabulary.formatInternational(Vocabulary.Keys.Unspecified)));
     }
 
@@ -3117,13 +3124,11 @@ public class MetadataBuilder {
      * Adds a note saying that Apache <abbr>SIS</abbr> has been used for 
decoding the format.
      * This method should not be invoked before the {@linkplain #addFormatName 
format name} has been set.
      *
-     * @param  provider  the data store provider, or {@code null} if 
unspecified.
+     * @param  name  the format name, or {@code null} if unspecified.
      */
-    public final void addFormatReader(final DataStoreProvider provider) {
-        if (provider != null) {
-            String name = provider.getShortName();
-            var driver = (name != null) ? new 
ImmutableIdentifier(Citations.SIS, Constants.SIS, name) : null;
-            addFormatReader(driver, Version.SIS);
+    public void addFormatReaderSIS(final String name) {
+        if (name != null) {
+            addFormatReader(new ImmutableIdentifier(Citations.SIS, 
Constants.SIS, name), Version.SIS);
         }
     }
 
@@ -3135,12 +3140,11 @@ public class MetadataBuilder {
      *   <li>{@code 
metadata/identificationInfo/resourceFormat/fileDecompressionTechnique}</li>
      * </ul>
      *
-     * If this method is used together with {@link 
#setPredefinedFormat(String)},
-     * then {@code setPredefinedFormat(…)} should be invoked 
<strong>before</strong> this method.
+     * If this method is used together with {@link #setPredefinedFormat 
setPredefinedFormat(…)},
+     * then the predefined format should be set <strong>before</strong> this 
method.
      *
      * @param value  the compression name, or {@code null} for no-operation.
      *
-     * @see #setPredefinedFormat(String)
      * @see #addFormatName(CharSequence)
      */
     public final void addCompression(final CharSequence value) {
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
index 119e835081..c45855411b 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
@@ -32,7 +32,6 @@ import java.io.BufferedReader;
 import java.io.LineNumberReader;
 import java.io.IOException;
 import java.net.URI;
-import java.nio.charset.Charset;
 import javax.measure.Unit;
 import javax.measure.quantity.Time;
 import org.opengis.util.GenericName;
@@ -71,7 +70,6 @@ import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.metadata.iso.DefaultMetadata;
-import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.setup.OptionKey;
 import org.apache.sis.measure.Units;
 
@@ -140,13 +138,6 @@ final class Store extends URIDataStore implements 
FeatureSet {
      */
     private BufferedReader source;
 
-    /**
-     * The character encoding, or {@code null} if unspecified (in which case 
the platform default is assumed).
-     * Note that the default value is different than the moving feature 
specification, which requires UTF-8.
-     * See "Departures from Moving Features specification" in package javadoc.
-     */
-    private final Charset encoding;
-
     /**
      * The metadata object, or {@code null} if not yet created.
      */
@@ -235,9 +226,9 @@ final class Store extends URIDataStore implements 
FeatureSet {
         source     = (r instanceof BufferedReader) ? (BufferedReader) r : new 
LineNumberReader(r);
         geometries = 
Geometries.factory(connector.getOption(OptionKey.GEOMETRY_LIBRARY));
         dissociate = 
connector.getOption(DataOptionKey.FOLIATION_REPRESENTATION) == 
FoliationRepresentation.FRAGMENTED;
-        GeneralEnvelope envelope    = null;
-        FeatureType     featureType = null;
-        Foliation       foliation   = null;
+        @SuppressWarnings("LocalVariableHidesMemberVariable") GeneralEnvelope 
envelope    = null;
+        @SuppressWarnings("LocalVariableHidesMemberVariable") FeatureType     
featureType = null;
+        @SuppressWarnings("LocalVariableHidesMemberVariable") Foliation       
foliation   = null;
         try {
             final List<String> elements = new ArrayList<>();
             source.mark(StorageConnector.READ_AHEAD_LIMIT);
@@ -296,7 +287,6 @@ final class Store extends URIDataStore implements 
FeatureSet {
         } catch (IllegalArgumentException | DateTimeException e) {
             throw new DataStoreContentException(getLocale(), 
StoreProvider.NAME, super.getDisplayName(), source).initCause(e);
         }
-        this.encoding    = connector.getOption(OptionKey.ENCODING);
         this.envelope    = ImmutableEnvelope.castOrCopy(envelope);
         this.featureType = featureType;
         this.foliation   = foliation;
@@ -349,8 +339,10 @@ final class Store extends URIDataStore implements 
FeatureSet {
      */
     @SuppressWarnings("fallthrough")
     private GeneralEnvelope parseEnvelope(final List<String> elements) throws 
DataStoreException, FactoryException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
+        int spatialDimensionCount = 2;      // Another result of this method 
to be computed as a side-effect.
+
         CoordinateReferenceSystem crs = null;
-        int spatialDimensionCount = 2;
         boolean    isDimExplicit  = false;
         double[]   lowerCorner    = ArraysExt.EMPTY_DOUBLE;
         double[]   upperCorner    = ArraysExt.EMPTY_DOUBLE;
@@ -410,7 +402,8 @@ final class Store extends URIDataStore implements 
FeatureSet {
          *   Assumed never part of the authority code. We need to build the 
temporal component ourselves
          *   in order to set the origin to the start time.
          */
-        final GeneralEnvelope envelope;
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
+        final GeneralEnvelope envelope;     // Value will be assigned to 
`this.envelope` by the caller.
         if (crs != null) {
             int count = 0;
             final CoordinateReferenceSystem[] components = new 
CoordinateReferenceSystem[3];
@@ -632,13 +625,8 @@ final class Store extends URIDataStore implements 
FeatureSet {
         if (metadata == null) {
             final MetadataBuilder builder = new MetadataBuilder();
             final String format = (timeEncoding != null) && hasTrajectories ? 
StoreProvider.MOVING : StoreProvider.NAME;
-            try {
-                builder.setPredefinedFormat(format);
-            } catch (MetadataStoreException e) {
-                builder.addFormatName(format);
-                listeners.warning(Level.FINE, null, e);
-            }
-            builder.addFormatReader(getProvider());
+            builder.setPredefinedFormat(format, listeners, true);
+            builder.addFormatReaderSIS(format);
             builder.addLanguage(Locale.ENGLISH, encoding, 
MetadataBuilder.Scope.ALL);
             builder.addResourceScope(ScopeCode.FEATURE, null);
             builder.addExtent(envelope, listeners);
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
index e74e6765ad..75e4d8c67f 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
@@ -21,7 +21,6 @@ import java.util.Arrays;
 import java.util.Hashtable;
 import java.util.Locale;
 import java.util.Optional;
-import java.util.logging.Level;
 import java.io.IOException;
 import java.io.FileNotFoundException;
 import java.nio.file.NoSuchFileException;
@@ -33,7 +32,6 @@ import java.awt.image.SampleModel;
 import java.awt.image.WritableRaster;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.maintenance.ScopeCode;
-import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridCoverage2D;
@@ -152,13 +150,8 @@ abstract class RasterStore extends PRJDataStore implements 
GridCoverageResource
     final void createMetadata(final String formatName, final String formatKey) 
throws DataStoreException {
         final GridGeometry gridGeometry = getGridGeometry();        // May 
cause parsing of header.
         final MetadataBuilder builder = new MetadataBuilder();
-        try {
-            builder.setPredefinedFormat(formatKey);
-        } catch (MetadataStoreException e) {
-            builder.addFormatName(formatName);
-            listeners.warning(Level.FINE, null, e);
-        }
-        builder.addFormatReader(getProvider());
+        builder.setPredefinedFormat(formatKey, listeners, true);
+        builder.addFormatReaderSIS(provider != null ? provider.getShortName() 
: null);
         builder.addResourceScope(ScopeCode.COVERAGE, null);
         builder.addLanguage(Locale.ENGLISH, encoding, 
MetadataBuilder.Scope.METADATA);
         builder.addSpatialRepresentation(null, gridGeometry, true);
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
index b454c8d45a..692af3ddd9 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
@@ -21,7 +21,6 @@ import java.util.Collection;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Optional;
-import java.util.logging.Level;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.EOFException;
@@ -54,7 +53,6 @@ import org.apache.sis.storage.base.PRJDataStore;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.base.AuxiliaryContent;
 import org.apache.sis.referencing.privy.AffineTransform2D;
-import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.privy.ListOfUnknownSize;
@@ -529,17 +527,14 @@ loop:   for (int convention=0;; convention++) {
             String format = reader().getFormatName();
             for (final String key : KNOWN_FORMATS) {
                 if (key.equalsIgnoreCase(format)) {
-                    try {
-                        builder.setPredefinedFormat(key);
+                    if (builder.setPredefinedFormat(key, listeners, false)) {
                         format = null;
-                    } catch (MetadataStoreException e) {
-                        listeners.warning(Level.FINE, null, e);
                     }
                     break;
                 }
             }
             builder.addFormatName(format);                          // Does 
nothing if `format` is null.
-            builder.addFormatReader(getProvider());
+            builder.addFormatReaderSIS(WorldFileStoreProvider.NAME);
             builder.addResourceScope(ScopeCode.COVERAGE, null);
             builder.addSpatialRepresentation(null, 
getGridGeometry(MAIN_IMAGE), true);
             if (gridGeometry.isDefined(GridGeometry.ENVELOPE)) {
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Version.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Version.java
index deb39d2408..0dfa8fca6c 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Version.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Version.java
@@ -16,8 +16,18 @@
  */
 package org.apache.sis.util;
 
+import java.net.URI;
+import java.net.URL;
+import java.io.File;
 import java.io.Serializable;
+import java.util.Optional;
 import java.util.StringTokenizer;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes;
+import java.util.logging.Logger;
+import org.apache.sis.system.Modules;
+import org.apache.sis.util.logging.Logging;
 import static org.apache.sis.system.Modules.MAJOR_VERSION;
 import static org.apache.sis.system.Modules.MINOR_VERSION;
 
@@ -39,7 +49,7 @@ import static org.apache.sis.system.Modules.MINOR_VERSION;
  * encouraged to make sure that subclasses remain immutable for more 
predictable behavior.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.4
+ * @version 1.5
  * @since   0.3
  */
 public class Version implements CharSequence, Comparable<Version>, 
Serializable {
@@ -74,6 +84,8 @@ public class Version implements CharSequence, 
Comparable<Version>, Serializable
 
     /**
      * The version in string form.
+     *
+     * @see #toString()
      */
     private final String version;
 
@@ -102,6 +114,45 @@ public class Version implements CharSequence, 
Comparable<Version>, Serializable
         this.version = version;
     }
 
+    /**
+     * Returns the version of the library that provides the given class. This 
method reads the
+     * {@code "Implementation-Version"} attribute of the {@code 
META-INF/MANIFEST.MF} file of
+     * the <abbr>JAR</abbr> file that contains the given class.
+     *
+     * @param  member  any public class of the library for which to get the 
version.
+     * @return version declared by the library that provides the given class.
+     *
+     * @see Attributes.Name#IMPLEMENTATION_VERSION
+     * @since 1.5
+     */
+    public static Optional<Version> ofLibrary(final Class<?> member) {
+        URL url = member.getResource(member.getSimpleName() + ".class");
+        if ("jar".equalsIgnoreCase(url.getProtocol())) {
+            String path = url.getPath();
+            if (path != null) {
+                int s = path.indexOf('!');
+                if (s >= 0) {
+                    path = path.substring(0, s);
+                }
+                try (JarFile jar = new JarFile(new File(new URI(path)))) {
+                    final Manifest manifest = jar.getManifest();
+                    if (manifest != null) {
+                        final Attributes attributes = 
manifest.getMainAttributes();
+                        if (attributes != null) {
+                            String version = 
attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+                            if (version != null) {
+                                return Optional.of(new Version(version));
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    
Logging.recoverableException(Logger.getLogger(Modules.UTILITIES), 
Version.class, "ofLibrary", e);
+                }
+            }
+        }
+        return Optional.empty();
+    }
+
     /**
      * Returns an instance for the given integer values.
      * The {@code components} array must contain at least 1 element, where:
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java
index 3afa5e81b8..21595c5d94 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java
@@ -138,6 +138,12 @@ public final class Constants extends Static {
      */
     public static final String GDAL = "GDAL";
 
+    /**
+     * The {@value} code space. Uses upper case "N" in the assumption that 
this name is used
+     * in the beginning of sentences. Otherwise, the <abbr>UCAR</abbr> usage 
is "netCDF".
+     */
+    public static final String NETCDF = "NetCDF";
+
     /**
      * The {@value} code space. The project name is {@code "Proj.4"}, but this 
constant omits
      * the dot because this name is used as a code space and we want to avoid 
risk of confusion.
diff --git 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/VersionTest.java 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/VersionTest.java
index c7ae3783bf..3932b77c10 100644
--- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/VersionTest.java
+++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/VersionTest.java
@@ -140,4 +140,13 @@ public final class VersionTest extends TestCase {
         final Version version = new Version("1.6.b2");
         assertNotSame(version, assertSerializedEquals(version));
     }
+
+    /**
+     * Tests {@link Version#ofLibrary(Class)}.
+     */
+    @Test
+    public void testOfLibrary() {
+        Version version = 
Version.ofLibrary(javax.measure.Unit.class).orElseThrow();
+        assertEquals(Version.valueOf(2, 1, 3), version);
+    }
 }
diff --git a/geoapi/snapshot b/geoapi/snapshot
index 367366e67f..8b993c74d8 160000
--- a/geoapi/snapshot
+++ b/geoapi/snapshot
@@ -1 +1 @@
-Subproject commit 367366e67f7f860e100e0e8dafa3a03f34162e71
+Subproject commit 8b993c74d8f49b215f6b4aa2747e0f362d97d3e3

Reply via email to