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

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

commit 235399435ff6e0dd8adf2ab65f02be90ed893aee
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Sep 3 17:26:35 2021 +0200

    Revisit the way GeoTIFF metadata are built:
    - Group more information on an image-by-image basis.
    - Build `DataStore` metadata by merging images metadata.
    
    Also add a `DataStore` constructor for specifying whether the GeoTIFF store
    is a component of a larger store. It will be used by the Landsat store.
---
 .../earthobservation/LandsatReaderTest.java        |   4 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  74 +++++++++---
 .../sis/storage/geotiff/GridGeometryBuilder.java   |   7 ++
 .../sis/storage/geotiff/ImageFileDirectory.java    | 131 +++++++++++++--------
 .../org/apache/sis/storage/geotiff/Reader.java     |  11 +-
 .../sis/storage/netcdf/MetadataReaderTest.java     |  16 ++-
 .../apache/sis/storage/netcdf/NetcdfStoreTest.java |   4 +-
 .../sis/internal/storage/AbstractResource.java     |  23 +++-
 .../sis/internal/storage/MetadataBuilder.java      | 123 ++++++++++++++++++-
 .../apache/sis/internal/storage/folder/Store.java  |   2 +-
 .../java/org/apache/sis/storage/DataStore.java     |  38 +++++-
 11 files changed, 335 insertions(+), 98 deletions(-)

diff --git 
a/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java
 
b/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java
index f080a2c..23f067a 100644
--- 
a/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java
+++ 
b/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java
@@ -86,7 +86,7 @@ public class LandsatReaderTest extends TestCase {
         verifier.addPropertyToIgnore(Metadata.class, "metadataStandard");      
     // Because hard-coded in SIS.
         verifier.addPropertyToIgnore(Metadata.class, "referenceSystemInfo");   
     // Very verbose and depends on EPSG connection.
         verifier.addMetadataToVerify(actual);
-        verifier.assertMetadataEquals(
+        verifier.addExpectedValues(
             "defaultLocale+otherLocale[0]",                                    
                      "en",
             "metadataIdentifier.code",                                         
                      "LandsatTest",
             "metadataScope[0].resourceScope",                                  
                      ScopeCode.COVERAGE,
@@ -251,5 +251,7 @@ public class LandsatReaderTest extends TestCase {
             "spatialRepresentationInfo[1].checkPointAvailability",             
      false,
 
             "resourceLineage[0].source[0].description", "Pseudo GLS");
+
+        verifier.assertMetadataEquals();
     }
 }
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 98dd0ed..d42ec30 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.storage.geotiff;
 
-import java.util.Locale;
 import java.util.List;
 import java.util.Optional;
 import java.util.logging.LogRecord;
@@ -29,7 +28,6 @@ import java.nio.file.StandardOpenOption;
 import org.opengis.util.NameSpace;
 import org.opengis.util.NameFactory;
 import org.opengis.util.GenericName;
-import org.opengis.util.FactoryException;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.maintenance.ScopeCode;
 import org.opengis.parameter.ParameterValueGroup;
@@ -37,9 +35,9 @@ import org.apache.sis.setup.OptionKey;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.UnsupportedStorageException;
 import org.apache.sis.storage.DataStoreClosedException;
 import org.apache.sis.storage.IllegalNameException;
@@ -120,6 +118,12 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
     private List<GridCoverageResource> components;
 
     /**
+     * Whether this {@code GeotiffStore} will be hidden. If {@code true}, then 
some metadata that would
+     * normally be provided in this {@code GeoTiffStore} will be provided by 
individual components instead.
+     */
+    final boolean hidden;
+
+    /**
      * Creates a new GeoTIFF store from the given file, URL or stream object.
      * This constructor invokes {@link 
StorageConnector#closeAllExcept(Object)},
      * keeping open only the needed resource.
@@ -129,7 +133,37 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
      * @throws DataStoreException if an error occurred while opening the 
GeoTIFF file.
      */
     public GeoTiffStore(final GeoTiffStoreProvider provider, final 
StorageConnector connector) throws DataStoreException {
-        super(provider, connector);
+        this(null, provider, connector, false);
+    }
+
+    /**
+     * Creates a new GeoTIFF store as a component of a larger data store.
+     *
+     * <div class="note"><b>Example:</b>
+     * A Landsat data set is a collection of files in a directory or ZIP file,
+     * which includes more than 10 GeoTIFF files (one image per band or 
product for a scene).
+     * {@link org.apache.sis.storage.earthobservation.LandsatStore} is a data 
store opening the Landsat
+     * metadata file as the main file, then opening each band/product using a 
GeoTIFF data store.
+     * Those bands/products are components of the Landsat data store.</div>
+     *
+     * If the {@code hidden} parameter is {@code true}, some metadata that 
would normally be provided
+     * in this {@code GeoTiffStore} will be provided by individual components 
instead.
+     *
+     * @param  parent     the parent that contains this new GeoTIFF store 
component, or {@code null} if none.
+     * @param  provider   the factory that created this {@code DataStore} 
instance, or {@code null} if unspecified.
+     * @param  connector  information about the storage (URL, stream, 
<i>etc</i>).
+     * @param  hidden     {@code true} if this GeoTIFF store will not be 
directly accessible from the parent.
+     *                    It is the case if the parent store will expose only 
some {@linkplain #components()
+     *                    components} instead of the GeoTIFF store itself.
+     * @throws DataStoreException if an error occurred while opening the 
GeoTIFF file.
+     *
+     * @since 1.1
+     */
+    public GeoTiffStore(final DataStore parent, final DataStoreProvider 
provider, final StorageConnector connector,
+                        final boolean hidden) throws DataStoreException
+    {
+        super(parent, provider, connector, hidden);
+        this.hidden = hidden;
         final Charset encoding = connector.getOption(OptionKey.ENCODING);
         this.encoding = (encoding != null) ? encoding : 
StandardCharsets.US_ASCII;
         final ChannelDataInput input = 
connector.getStorageAs(ChannelDataInput.class);
@@ -193,6 +227,20 @@ 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.setFormat(Constants.GEOTIFF);
+        } catch (MetadataStoreException e) {
+            builder.addFormatName(Constants.GEOTIFF);
+            listeners.warning(e);
+        }
+        builder.addEncoding(encoding, MetadataBuilder.Scope.METADATA);
+        builder.addResourceScope(ScopeCode.COVERAGE, null);
+    }
+
+    /**
      * Returns information about the dataset as a whole. The returned metadata 
object can contain information
      * such as the spatiotemporal extent of the dataset, contact information 
about the creator or distributor,
      * data quality, usage constraints and more.
@@ -204,26 +252,18 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
     public synchronized Metadata getMetadata() throws DataStoreException {
         if (metadata == null) {
             final Reader reader = reader();
-            final MetadataBuilder builder = reader.metadata;
-            try {
-                builder.setFormat(Constants.GEOTIFF);
-            } catch (MetadataStoreException e) {
-                builder.addFormatName(Constants.GEOTIFF);
-                listeners.warning(e);
-            }
-            builder.addEncoding(encoding, MetadataBuilder.Scope.METADATA);
-            builder.addResourceScope(ScopeCode.COVERAGE, null);
-            final Locale locale = getLocale();
+            final MetadataBuilder builder = new MetadataBuilder();
+            setFormatInfo(builder);
             int n = 0;
             try {
                 ImageFileDirectory dir;
                 while ((dir = reader.getImageFileDirectory(n++)) != null) {
-                    dir.completeMetadata(builder, locale);
+                    builder.addFromComponent(dir.getMetadata());
                 }
             } catch (IOException e) {
                 throw errorIO(e);
-            } catch (FactoryException | ArithmeticException e) {
-                throw new DataStoreContentException(getLocale(), 
Constants.GEOTIFF, reader.input.filename, null).initCause(e);
+            } catch (ArithmeticException e) {
+                listeners.warning(e);
             }
             /*
              * Add the filename as an identifier only if the input was 
something convertible to URI (URL, File or Path),
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
index 3ca072d..5c516df 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
@@ -362,6 +362,13 @@ final class GridGeometryBuilder {
      *
      * This method invokes {@link 
MetadataBuilder#newGridRepresentation(MetadataBuilder.GridType)}
      * with the appropriate {@code GEORECTIFIED} or {@code GEOREFERENCEABLE} 
type.
+     * Storage locations are:
+     *
+     * <ul>
+     *   <li>{@code metadata/spatialRepresentationInfo/*}</li>
+     *   <li>{@code metadata/identificationInfo/spatialRepresentationType}</li>
+     *   <li>{@code metadata/referenceSystemInfo}</li>
+     * </ul>
      *
      * @param  metadata  the helper class where to write metadata values.
      * @throws NumberFormatException if a numeric value was stored as a string 
and can not be parsed.
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index a6685c0..c73899c 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -20,7 +20,6 @@ import java.io.IOException;
 import java.text.ParseException;
 import java.util.List;
 import java.util.Arrays;
-import java.util.Locale;
 import java.util.Optional;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
@@ -32,10 +31,12 @@ import java.awt.image.SinglePixelPackedSampleModel;
 import java.awt.image.RasterFormatException;
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
+import org.opengis.metadata.Metadata;
 import org.opengis.metadata.citation.DateType;
 import org.opengis.util.FactoryException;
 import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.internal.geotiff.Resources;
 import org.apache.sis.internal.geotiff.Predictor;
 import org.apache.sis.internal.geotiff.Compression;
@@ -52,6 +53,7 @@ import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.math.Vector;
@@ -101,12 +103,22 @@ final class ImageFileDirectory extends DataCube {
     private final GenericName identifier;
 
     /**
+     * Builder for the metadata. This field is reset to {@code null} when not 
needed anymore.
+     */
+    private MetadataBuilder metadata;
+
+    /**
      * {@code true} if this {@code ImageFileDirectory} has not yet read all 
deferred entries.
      * When this flag is {@code true}, the {@code ImageFileDirectory} is not 
yet ready for use.
      */
     boolean hasDeferredEntries;
 
     /**
+     * {@code true} if {@link #validateMandatoryTags()} has already been 
invoked.
+     */
+    private boolean isValidated;
+
+    /**
      * The size of the image described by this FID, or -1 if the information 
has not been found.
      * The image may be much bigger than the memory capacity, in which case 
the image shall be tiled.
      *
@@ -418,6 +430,7 @@ final class ImageFileDirectory extends DataCube {
     ImageFileDirectory(final Reader reader, final int index) {
         super(reader);
         identifier = 
reader.nameFactory.createLocalName(reader.store.identifier, 
String.valueOf(index + 1));
+        metadata = new MetadataBuilder();
     }
 
     /**
@@ -839,19 +852,23 @@ final class ImageFileDirectory extends DataCube {
 
             /*
              * The name of the document from which this image was scanned.
+             *
+             * Destination: metadata/identificationInfo/citation/series/name
              */
             case Tags.DocumentName: {
                 for (final String value : type.readString(input(), count, 
encoding())) {
-                    reader.metadata.addSeries(value);
+                    metadata.addSeries(value);
                 }
                 break;
             }
             /*
              * The name of the page from which this image was scanned.
+             *
+             * Destination: metadata/identificationInfo/citation/series/page
              */
             case Tags.PageName: {
                 for (final String value : type.readString(input(), count, 
encoding())) {
-                    reader.metadata.addPage(value);
+                    metadata.addPage(value);
                 }
                 break;
             }
@@ -859,6 +876,8 @@ final class ImageFileDirectory extends DataCube {
              * The page number of the page from which this image was scanned.
              * Should be a vector of length 2 containing the page number and
              * the total number of pages (with 0 meaning unavailable).
+             *
+             * Destination: metadata/identificationInfo/citation/series/page
              */
             case Tags.PageNumber: {
                 final Vector v = type.readVector(input(), count);
@@ -868,64 +887,76 @@ final class ImageFileDirectory extends DataCube {
                     case 1:  p = v.intValue(0);
                     case 0:  break;
                 }
-                reader.metadata.addPage(p, n);
+                metadata.addPage(p, n);
                 break;
             }
             /*
              * A string that describes the subject of the image.
              * For example, a user may wish to attach a comment such as "1988 
company picnic" to an image.
+             *
+             * Destination: metadata/identificationInfo/citation/title
              */
             case Tags.ImageDescription: {
                 for (final String value : type.readString(input(), count, 
encoding())) {
-                    reader.metadata.addTitle(value);
+                    metadata.addTitle(value);
                 }
                 break;
             }
             /*
              * Person who created the image. Some older TIFF files used this 
tag for storing
              * Copyright information, but Apache SIS does not support this 
legacy practice.
+             *
+             * Destination: metadata/identificationInfo/citation/party/name
              */
             case Tags.Artist: {
                 for (final String value : type.readString(input(), count, 
encoding())) {
-                    reader.metadata.addAuthor(value);
+                    metadata.addAuthor(value);
                 }
                 break;
             }
             /*
              * Copyright notice of the person or organization that claims the 
copyright to the image.
              * Example: “Copyright, John Smith, 1992. All rights reserved.”
+             *
+             * Destination: metadata/identificationInfo/resourceConstraint
              */
             case Tags.Copyright: {
                 for (final String value : type.readString(input(), count, 
encoding())) {
-                    reader.metadata.parseLegalNotice(value);
+                    metadata.parseLegalNotice(value);
                 }
                 break;
             }
             /*
              * Date and time of image creation. The format is: "YYYY:MM:DD 
HH:MM:SS" with 24-hour clock.
+             *
+             * Destination: metadata/identificationInfo/citation/date
              */
             case Tags.DateTime: {
                 for (final String value : type.readString(input(), count, 
encoding())) {
-                    
reader.metadata.addCitationDate(reader.getDateFormat().parse(value),
+                    
metadata.addCitationDate(reader.getDateFormat().parse(value),
                             DateType.CREATION, MetadataBuilder.Scope.RESOURCE);
                 }
                 break;
             }
             /*
              * The computer and/or operating system in use at the time of 
image creation.
+             *
+             * Destination: 
metadata/resourceLineage/processStep/processingInformation/procedureDescription
              */
             case Tags.HostComputer: {
                 for (final String value : type.readString(input(), count, 
encoding())) {
-                    reader.metadata.addHostComputer(value);
+                    metadata.addHostComputer(value);
                 }
                 break;
             }
             /*
              * Name and version number of the software package(s) used to 
create the image.
+             *
+             * Destination: 
metadata/resourceLineage/processStep/processingInformation/softwareReference/title
              */
             case Tags.Software: {
                 for (final String value : type.readString(input(), count, 
encoding())) {
-                    reader.metadata.addSoftwareReference(value);
+                    metadata.addSoftwareReference(value);
                 }
                 break;
             }
@@ -941,10 +972,12 @@ final class ImageFileDirectory extends DataCube {
             /*
              * The model name or number of the scanner, video digitizer, or 
other type of equipment used to
              * generate the image.
+             *
+             * Destination: 
metadata/acquisitionInformation/platform/instrument/identifier
              */
             case Tags.Model: {
                 for (final String value : type.readString(input(), count, 
encoding())) {
-                    reader.metadata.addInstrument(null, value);
+                    metadata.addInstrument(null, value);
                 }
                 break;
             }
@@ -1135,6 +1168,7 @@ final class ImageFileDirectory extends DataCube {
      * @throws DataStoreContentException if a mandatory tag is missing and can 
not be inferred.
      */
     final void validateMandatoryTags() throws DataStoreContentException {
+        if (isValidated) return;
         if (imageWidth  < 0) throw missingTag(Tags.ImageWidth);
         if (imageHeight < 0) throw missingTag(Tags.ImageLength);
         final short offsetsTag, byteCountsTag;
@@ -1256,25 +1290,33 @@ final class ImageFileDirectory extends DataCube {
         if (referencing != null && !referencing.validateMandatoryTags()) {
             throw missingTag(Tags.ModelTiePoints);
         }
+        isValidated = true;
     }
 
     /**
-     * Completes the metadata with the information stored in the field of this 
IFD.
+     * Builds the metadata with the information stored in the fields of this 
IFD.
      * This method is invoked only if the user requested the ISO 19115 
metadata.
-     * This method creates a new {@code "metadata/contentInfo"} node for this 
image.
-     * Information not under the {@code "metadata/contentInfo"} node will be 
merged
-     * with the current content of the given {@code MetadataBuilder}.
      *
-     * @param   metadata  where to write metadata information. Caller should 
have already invoked
-     *                    {@link MetadataBuilder#setFormat(String)} before 
{@code completeMetadata(…)} calls.
+     * @throws DataStoreException if an error occurred while reading metadata 
from the data store.
      */
-    final void completeMetadata(final MetadataBuilder metadata, final Locale 
locale)
-            throws DataStoreContentException, FactoryException
-    {
-        metadata.newCoverage(false);
+    @Override
+    protected Metadata createMetadata() throws DataStoreException {
+        /*
+         * Add information about the file format.
+         *
+         * Destination: metadata/identificationInfo/resourceFormat
+         */
+        if (reader.store.hidden) {
+            reader.store.setFormatInfo(metadata);       // Should be before 
`addCompression(…)`.
+        }
         if (compression != null) {
-            metadata.addCompression(compression.name().toLowerCase(locale));
+            
metadata.addCompression(CharSequences.upperCaseToSentence(compression.name()));
         }
+        /*
+         * Add information about sample dimensions.
+         *
+         * Destination: metadata/contentInfo/attributeGroup/attribute
+         */
         for (int band = 0; band < samplesPerPixel;) {
             metadata.newSampleDimension();
             metadata.setBitPerSample(bitsPerSample);
@@ -1285,6 +1327,8 @@ final class ImageFileDirectory extends DataCube {
         /*
          * Add the resolution into the metadata. Our current ISO 19115 
implementation restricts
          * the resolution unit to metres, but it may be relaxed in a future 
SIS version.
+         *
+         * Destination: metadata/identificationInfo/spatialResolution/distance
          */
         if (!Double.isNaN(resolution) && resolutionUnit != null) {
             
metadata.addResolution(resolutionUnit.getConverterTo(Units.METRE).convert(resolution));
@@ -1296,6 +1340,8 @@ final class ImageFileDirectory extends DataCube {
          *   -1 means that Threshholding is 1 or unspecified.
          *   -2 means that Threshholding is 2 but the matrix size has not yet 
been specified.
          *   -3 means that Threshholding is 3 (randomized process such as 
error diffusion).
+         *
+         * Destination: metadata/resourceLineage/processStep/description
          */
         switch (Math.min(cellWidth, cellHeight)) {
             case -1: {
@@ -1315,36 +1361,23 @@ final class ImageFileDirectory extends DataCube {
             }
         }
         /*
-         * Add Coordinate Reference System built from GeoTIFF tags.  Note that 
the CRS may not exist,
-         * in which case the CRS builder returns null. This is safe since all 
MetadataBuilder methods
-         * ignore null values (a design choice because this pattern come very 
often).
+         * Add Coordinate Reference System built from GeoTIFF tags. Note that 
the CRS may not exist.
+         *
+         * Destination: metadata/spatialRepresentationInfo and others.
          */
         if (referencing != null) {
-            getGridGeometry();                  // For calculation of 
gridGeometry if not already done.
-            referencing.completeMetadata(metadata);
+            final GridGeometry gridGeometry = getGridGeometry();
+            if (gridGeometry.isDefined(GridGeometry.ENVELOPE)) try {
+                metadata.addExtent(gridGeometry.getEnvelope());
+            } catch (TransformException e) {
+                warning(e);
+            }
+            referencing.completeMetadata(metadata);         // Must be after 
`getGridGeometry()`.
         }
-    }
-
-    /**
-     * Invoked the first time that {@link #getMetadata()} is invoked.
-     *
-     * @param  metadata  the builder where to set metadata properties.
-     * @throws DataStoreException if an error occurred while reading metadata 
from the data store.
-     */
-    @Override
-    protected void createMetadata(final MetadataBuilder metadata) throws 
DataStoreException {
-        super.createMetadata(metadata);
-        /*
-         * TODO:
-         *   - Modify ImageFileDirectory.completeMetadata(…) with the addition 
of a boolean telling that
-         *     that we invoke this method for a single image instead than the 
whole image. Use that flag
-         *     for skipping MetadataBuilder calls writing in 
metadata/identificationInfo/resourceFormat.
-         *   - Invoke ImageFileDirectory.completeMetadata(…) if not already 
done and cache in a field.
-         *   - Add a metadata utility method taking two Metadata in argument, 
search for properties that
-         *     are equal and replace them by the same instance.
-         *   - Invoke that method from here if GeoTiffStore already has a 
metadata, or conversely from
-         *     GeoTiffStore if ImageResource already has a metadata.
-         */
+        metadata.addTitleOrIdentifier(identifier.toString(), 
MetadataBuilder.Scope.RESOURCE);
+        final Metadata md = metadata.build(true);
+        metadata = null;
+        return md;
     }
 
     /**
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
index e6f90be..1bf97eb 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
@@ -29,7 +29,6 @@ import org.opengis.util.NameFactory;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
-import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.geotiff.Resources;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.util.resources.Errors;
@@ -121,11 +120,6 @@ final class Reader extends GeoTIFF {
     private boolean deferredNeedsSort;
 
     /**
-     * Builder for the metadata.
-     */
-    final MetadataBuilder metadata;
-
-    /**
      * The factory to use for creating image identifiers.
      */
     final NameFactory nameFactory;
@@ -141,7 +135,6 @@ final class Reader extends GeoTIFF {
         super(store);
         this.input       = input;
         this.origin      = input.getStreamPosition();
-        this.metadata    = new MetadataBuilder();
         this.doneIFD     = new HashSet<>();
         this.nameFactory = DefaultFactories.forBuildin(NameFactory.class);
         /*
@@ -174,7 +167,7 @@ final class Reader extends GeoTIFF {
                         intSizeExpansion = (byte) (powerOf2 - 2);
                         if (intSizeExpansion == 1) {
                             /*
-                             * Above 'intSizeExpension' calculation was a 
little bit useless since we accept only
+                             * Above `intSizeExpension` calculation was a 
little bit useless since we accept only
                              * one result in the end, but we did that generic 
computation anyway for keeping the
                              * code almost ready if the BigTIFF specification 
adds support for 16 bytes pointer.
                              */
@@ -275,7 +268,7 @@ final class Reader extends GeoTIFF {
                 final long size  = (type != null) ? 
Math.multiplyExact(type.size, count) : 0;
                 if (size <= offsetSize) {
                     /*
-                     * If the value can fit inside the number of bytes given 
by 'offsetSize', then the value is
+                     * If the value can fit inside the number of bytes given 
by `offsetSize`, then the value is
                      * stored directly at that location. This is the most 
common way TIFF tag values are stored.
                      */
                     final long position = input.getStreamPosition();
diff --git 
a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
 
b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
index 779a47c..333f091 100644
--- 
a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
+++ 
b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
@@ -44,7 +44,7 @@ import static org.apache.sis.test.TestUtilities.date;
  * for reading netCDF attributes.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.3
  * @module
  */
@@ -75,7 +75,7 @@ public final strictfp class MetadataReaderTest extends 
TestCase {
         try (Decoder input = 
ChannelDecoderTest.createChannelDecoder(TestData.NETCDF_2D_GEOGRAPHIC)) {
             metadata = new MetadataReader(input).read();
         }
-        compareToExpected(metadata);
+        compareToExpected(metadata).assertMetadataEquals();
     }
 
     /**
@@ -91,19 +91,21 @@ public final strictfp class MetadataReaderTest extends 
TestCase {
         try (Decoder input = createDecoder(TestData.NETCDF_2D_GEOGRAPHIC)) {
             metadata = new MetadataReader(input).read();
         }
-        compareToExpected(metadata);
+        final ContentVerifier verifier = compareToExpected(metadata);
+        
verifier.addExpectedValue("identificationInfo[0].resourceFormat[0].formatSpecificationCitation.alternateTitle[1]",
 "NetCDF-3/CDM");
+        verifier.assertMetadataEquals();
     }
 
     /**
-     * Compares the string representation of the given metadata object with 
the expected one.
+     * Creates comparator for the string representation of the given metadata 
object with the expected one.
      * The given metadata shall have been created from the {@link 
TestData#NETCDF_2D_GEOGRAPHIC} dataset.
      */
-    static void compareToExpected(final Metadata actual) {
+    static ContentVerifier compareToExpected(final Metadata actual) {
         final ContentVerifier verifier = new ContentVerifier();
         verifier.addPropertyToIgnore(Metadata.class, "metadataStandard");
         verifier.addPropertyToIgnore(Metadata.class, "referenceSystemInfo");
         verifier.addMetadataToVerify(actual);
-        verifier.assertMetadataEquals(
+        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",
@@ -156,5 +158,7 @@ public final strictfp class MetadataReaderTest extends 
TestCase {
             "contentInfo[0].attributeGroup[0].attribute[0].units",             
        "°C",
 
             "resourceLineage[0].statement", "Decimated and modified by GeoAPI 
for inclusion in conformance test suite.");
+
+        return verifier;
     }
 }
diff --git 
a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/NetcdfStoreTest.java
 
b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/NetcdfStoreTest.java
index 2b4a6dc..e64cb7e 100644
--- 
a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/NetcdfStoreTest.java
+++ 
b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/NetcdfStoreTest.java
@@ -32,7 +32,7 @@ import static org.opengis.test.Assert.*;
  * Tests {@link NetcdfStore}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.3
  * @module
  */
@@ -63,7 +63,7 @@ public final strictfp class NetcdfStoreTest extends TestCase {
             metadata = store.getMetadata();
             assertSame("Should be cached.", metadata, store.getMetadata());
         }
-        MetadataReaderTest.compareToExpected(metadata);
+        MetadataReaderTest.compareToExpected(metadata).assertMetadataEquals();
     }
 
     /**
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
index 83ba7bf..13bac60 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
@@ -113,16 +113,29 @@ public class AbstractResource extends StoreListeners 
implements Resource {
     @Override
     public final synchronized Metadata getMetadata() throws DataStoreException 
{
         if (metadata == null) {
-            final MetadataBuilder builder = new MetadataBuilder();
-            createMetadata(builder);
-            metadata = builder.build(true);
+            metadata = createMetadata();
         }
         return metadata;
     }
 
     /**
-     * Invoked the first time that {@link #getMetadata()} is invoked. The 
default implementation populates
-     * metadata based on information provided by {@link #getIdentifier()} and 
{@link #getEnvelope()}.
+     * Invoked in a synchronized block the first time that {@link 
#getMetadata()} is invoked.
+     * The default implementation delegates to {@link 
#createMetadata(MetadataBuilder)}.
+     * Subclasses can override if they want to use a different kind of builder.
+     *
+     * @return the newly created metadata.
+     * @throws DataStoreException if an error occurred while reading metadata 
from the data store.
+     */
+    protected Metadata createMetadata() throws DataStoreException {
+        final MetadataBuilder builder = new MetadataBuilder();
+        createMetadata(builder);
+        return builder.build(true);
+    }
+
+    /**
+     * Invoked by the default implementation of {@link #createMetadata()}.
+     * The default implementation populates metadata based on information
+     * provided by {@link #getIdentifier()} and {@link #getEnvelope()}.
      * Subclasses should override if they can provide more information.
      *
      * @param  metadata  the builder where to set metadata properties.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
index 6e3a924..4812e3c 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
@@ -44,15 +44,20 @@ import org.opengis.metadata.citation.DateType;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.citation.CitationDate;
 import org.opengis.metadata.citation.OnLineFunction;
+import org.opengis.metadata.citation.OnlineResource;
 import org.opengis.metadata.identification.Identification;
+import org.opengis.metadata.extent.Extent;
 import org.opengis.metadata.spatial.GCP;
 import org.opengis.metadata.spatial.Dimension;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.metadata.spatial.CellGeometry;
 import org.opengis.metadata.spatial.PixelOrientation;
 import org.opengis.metadata.spatial.GeolocationInformation;
+import org.opengis.metadata.spatial.SpatialRepresentation;
 import org.opengis.metadata.spatial.SpatialRepresentationType;
+import org.opengis.metadata.constraint.Constraints;
 import org.opengis.metadata.constraint.Restriction;
+import org.opengis.metadata.content.ContentInformation;
 import org.opengis.metadata.content.CoverageContentType;
 import org.opengis.metadata.content.TransferFunctionType;
 import org.opengis.metadata.maintenance.ScopeCode;
@@ -60,8 +65,12 @@ import org.opengis.metadata.acquisition.Context;
 import org.opengis.metadata.acquisition.OperationType;
 import org.opengis.metadata.identification.Progress;
 import org.opengis.metadata.identification.KeywordType;
+import org.opengis.metadata.identification.Resolution;
 import org.opengis.metadata.identification.TopicCategory;
+import org.opengis.metadata.distribution.Distribution;
+import org.opengis.metadata.distribution.Distributor;
 import org.opengis.metadata.distribution.Format;
+import org.opengis.metadata.lineage.Lineage;
 import org.opengis.metadata.quality.Element;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.ReferenceSystem;
@@ -140,7 +149,9 @@ import org.apache.sis.measure.Units;
 import static 
org.apache.sis.internal.util.StandardDateFormat.MILLISECONDS_PER_DAY;
 
 // Branch-dependent imports
+import org.opengis.temporal.Duration;
 import org.opengis.feature.FeatureType;
+import org.opengis.metadata.acquisition.AcquisitionInformation;
 import org.opengis.metadata.citation.Responsibility;
 
 
@@ -544,8 +555,9 @@ public class MetadataBuilder {
     private DefaultFormat format() {
         DefaultFormat df = DefaultFormat.castOrCopy(format);
         if (df == null) {
-            format = df = new DefaultFormat();
+            df = new DefaultFormat();
         }
+        format = df;
         return df;
     }
 
@@ -2001,7 +2013,7 @@ parse:      for (int i = 0; i < length;) {
      * Adds and populates a "spatial representation info" node using the given 
grid geometry.
      * This method invokes implicitly {@link 
#newGridRepresentation(GridType)}, unless this
      * method returns {@code false} in which case nothing has been done.
-     * Storage location is:
+     * Storage locations are:
      *
      * <ul>
      *   <li>{@code 
metadata/spatialRepresentationInfo/transformationDimensionDescription}</li>
@@ -2397,7 +2409,11 @@ parse:      for (int i = 0; i < length;) {
     /**
      * Sets the number that uniquely identifies instances of bands of 
wavelengths on which a sensor operates.
      * This is a convenience method for {@link #setBandIdentifier(MemberName)} 
when the band is specified only
-     * by a number.
+     * by a number. Storage location is:
+     *
+     * <ul>
+     *   <li>{@code 
metadata/contentInfo/attributeGroup/attribute/sequenceIdentifier}</li>
+     * </ul>
      *
      * @param  sequenceIdentifier  the band number, or 0 or negative if none.
      */
@@ -3115,6 +3131,107 @@ parse:      for (int i = 0; i < length;) {
     }
 
     /**
+     * Appends information from the metadata of a component.
+     * This is an helper method for building the metadata of an aggregate.
+     * Aggregate metadata should be set before to invoke this method, in 
particular:
+     *
+     * <ul>
+     *   <li>The aggregated resource {@linkplain #addTitle title}.</li>
+     *   <li>The {@linkplain #addFormatName format} (may not be the same than 
component format).</li>
+     * </ul>
+     *
+     * This method applies the following heuristic rules (may change in any 
future version).
+     * Those rules assume that the component metadata was built with {@code 
MetadataBuilder}
+     * (this assumption determines which metadata elements are inspected).
+     *
+     * <ul>
+     *   <li>Content information is added verbatim. There is usually one 
instance per component.</li>
+     *   <li>Extents are added as one {@link Extent} per component, but 
without duplicated values.</li>
+     *   <li>All Coordinate Reference System information are added without 
duplicated values.</li>
+     *   <li>Some citation information are merged in a single citation.
+     *       The following information are ignored because considered too 
specific to the component:<ul>
+     *         <li>titles</li>
+     *         <li>identifiers</li>
+     *         <li>series (includes page numbers).</li>
+     *       </ul></li>
+     *   <li>{@linkplain #addCompression Compression} are added (without 
duplicated value) but not the
+     *       other format information (because the aggregate is assumed to 
have its own format name).</li>
+     *   <li>Distributor names, but not the other distribution information 
because the aggregated resource
+     *       may not be distributed in the same way then the components.</li>
+     * </ul>
+     *
+     * @param  component  the component from which to append metadata.
+     */
+    public final void addFromComponent(final Metadata component) {
+        /*
+         * Note: this method contains many loops like below:
+         *
+         *     for (Foo r : info.getFoos()) {
+         *         addIfNotPresent(bla().getFoos(), r);
+         *     }
+         *
+         * We could easily factor out the above pattern in a method, but we 
don't do that because
+         * it would invoke `bla().getFoos()` before the loop. We want that 
call to happen only if
+         * the collection contains at least one element. Usually there is only 
0 or 1 element.
+         */
+        for (final Identification info : component.getIdentificationInfo()) {
+            final Citation c = info.getCitation();
+            if (c != null) {
+                // Title, identifiers and series are assumed to not apply (see 
Javadoc).
+                final DefaultCitation citation = citation();
+                for (Responsibility r : c.getCitedResponsibleParties()) {
+                    addIfNotPresent(citation.getCitedResponsibleParties(), r);
+                }
+                for (OnlineResource r : c.getOnlineResources()) {
+                    addIfNotPresent(citation.getOnlineResources(), r);
+                }
+                
citation.getPresentationForms().addAll(c.getPresentationForms());
+            }
+            final DefaultDataIdentification identification = identification();
+            for (Extent e : info.getExtents()) {
+                addIfNotPresent(identification.getExtents(), e);
+            }
+            for (Resolution r : info.getSpatialResolutions()) {
+                addIfNotPresent(identification.getSpatialResolutions(), r);
+            }
+            for (Duration r : info.getTemporalResolutions()) {
+                addIfNotPresent(identification.getTemporalResolutions(), r);
+            }
+            for (Format r : info.getResourceFormats()) {
+                addCompression(r.getFileDecompressionTechnique());
+                // Ignore format name (see Javadoc).
+            }
+            for (Constraints r : info.getResourceConstraints()) {
+                addIfNotPresent(identification.getResourceConstraints(), r);
+            }
+            
identification.getTopicCategories().addAll(info.getTopicCategories());
+            
identification.getSpatialRepresentationTypes().addAll(info.getSpatialRepresentationTypes());
+        }
+        final DefaultMetadata metadata = metadata();
+        for (ContentInformation info : component.getContentInfo()) {
+            addIfNotPresent(metadata.getContentInfo(), info);
+        }
+        for (final ReferenceSystem crs : component.getReferenceSystemInfo()) {
+            addReferenceSystem(crs);
+        }
+        for (SpatialRepresentation info : 
component.getSpatialRepresentationInfo()) {
+            addIfNotPresent(metadata.getSpatialRepresentationInfo(), info);
+        }
+        for (AcquisitionInformation info : 
component.getAcquisitionInformation()) {
+            addIfNotPresent(metadata.getAcquisitionInformation(), info);
+        }
+        for (Distribution info : component.getDistributionInfo()) {
+            // See Javadoc about why we copy only the distributors.
+            for (Distributor r : info.getDistributors()) {
+                addIfNotPresent(distribution().getDistributors(), r);
+            }
+        }
+        for (Lineage info : component.getResourceLineages()) {
+            addIfNotPresent(metadata.getResourceLineages(), info);
+        }
+    }
+
+    /**
      * Returns the metadata (optionally as an unmodifiable object), or {@code 
null} if none.
      * If {@code freeze} is {@code true}, then the returned metadata instance 
can not be modified.
      *
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
index 386444a..fab7ea4 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
@@ -188,7 +188,7 @@ class Store extends DataStore implements StoreResource, 
Aggregate, DirectoryStre
      * @throws DataStoreException if an error occurred while opening the 
stream.
      */
     private Store(final Store parent, final StorageConnector connector, final 
NameFactory nameFactory) throws DataStoreException {
-        super(parent, connector);
+        super(parent, parent.getProvider(), connector, false);
         originator        = parent;
         location          = connector.getStorageAs(Path.class);
         locale            = connector.getOption(OptionKey.LOCALE);
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
index 5487b25..ff26ba6 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
@@ -150,21 +150,49 @@ public abstract class DataStore implements Resource, 
Localized, AutoCloseable {
      * @throws DataStoreException if an error occurred while creating the data 
store for the given storage.
      *
      * @since 0.8
+     *
+     * @deprecated Replaced by {@link #DataStore(DataStore, DataStoreProvider, 
StorageConnector, boolean)}.
      */
+    @Deprecated
     protected DataStore(final DataStore parent, final StorageConnector 
connector) throws DataStoreException {
+        this(parent, (parent != null) ? parent.provider : null, connector, 
false);
+    }
+
+    /**
+     * Creates a new instance as a child of another data store instance.
+     * Events will be sent not only to {@linkplain #addListener registered 
listeners} of this {@code DataStore},
+     * but also to listeners of the {@code parent} data store. Each listener 
will be notified only once, even if
+     * the same listener is registered in the two places.
+     *
+     * @param  parent     the parent that contains this new {@code DataStore} 
component, or {@code null} if none.
+     * @param  provider   the factory that created this {@code DataStore} 
instance, or {@code null} if unspecified.
+     * @param  connector  information about the storage (URL, stream, reader 
instance, <i>etc</i>).
+     * @param  hidden     {@code true} if this store will not be directly 
accessible from the parent.
+     *                    It is the case if this store is an {@link Aggregate} 
and the parent store will
+     *                    expose only some components of the aggregate instead 
of the aggregate itself.
+     * @throws DataStoreException if an error occurred while creating the data 
store for the given storage.
+     *
+     * @since 1.1
+     */
+    protected DataStore(final DataStore parent, final DataStoreProvider 
provider, final StorageConnector connector,
+                        final boolean hidden) throws DataStoreException
+    {
         ArgumentChecks.ensureNonNull("connector", connector);
+        this.provider = provider;
+        name = connector.getStorageName();
         final StoreListeners forwardTo;
         if (parent != null) {
+            locale = parent.locale;
+            if (hidden) {
+                listeners = parent.listeners;
+                return;
+            }
             forwardTo = parent.listeners;
-            provider  = parent.provider;
-            locale    = parent.locale;
         } else {
-            forwardTo = null;
-            provider  = null;
             locale    = Locale.getDefault(Locale.Category.DISPLAY);
+            forwardTo = null;
         }
         listeners = new StoreListeners(forwardTo, this);
-        name = connector.getStorageName();
     }
 
     /**

Reply via email to