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 d8b9343  Move some metadata fields in a separated 
`ImageMetadataBuilder` class. The goal is to make their life cycle more 
visible, especially `XMLMetadata` which causes confusing metadata tree if not 
merged last.
d8b9343 is described below

commit d8b93438c9fa5b52dff5c6e09e7237250ce21970
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Jan 28 11:29:48 2022 +0100

    Move some metadata fields in a separated `ImageMetadataBuilder` class.
    The goal is to make their life cycle more visible, especially `XMLMetadata` 
which causes confusing metadata tree if not merged last.
---
 .../org/apache/sis/internal/metadata/Merger.java   |   5 +-
 .../org/apache/sis/storage/geotiff/CRSBuilder.java |   2 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    | 126 ++---------
 .../sis/storage/geotiff/ImageMetadataBuilder.java  | 234 +++++++++++++++++++++
 .../apache/sis/storage/geotiff/NativeMetadata.java |   2 +-
 .../apache/sis/storage/geotiff/XMLMetadata.java    |  44 +++-
 .../sis/storage/geotiff/XMLMetadataTest.java       |   4 +-
 .../sis/internal/storage/MetadataBuilder.java      |  49 +++--
 8 files changed, 322 insertions(+), 144 deletions(-)

diff --git 
a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java 
b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java
index f881c5d..852a542 100644
--- 
a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java
+++ 
b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java
@@ -126,9 +126,8 @@ public class Merger {
      * Implementation of {@link #copy(Object, ModifiableMetadata)} method,
      * to be invoked recursively for all child properties to merge.
      *
-     * @param  dryRun  {@code true} for executing the merge operation in "dry 
run" mode instead of performing the
-     *                 actual merge. This mode is used for verifying if there 
is a merge conflict before to perform
-     *                 the actual operation.
+     * @param  dryRun  {@code true} for simulating the merge operation instead 
of performing the actual merge.
+     *                 Used for verifying if there is a merge conflict before 
to perform the actual operation.
      * @return {@code true} if the merge operation is valid, or {@code false} 
if the given arguments are valid
      *         metadata but the merge operation can nevertheless not be 
executed because it could cause data lost.
      */
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
index 87efd7b..af17277 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
@@ -1197,7 +1197,7 @@ final class CRSBuilder extends 
ReferencingFactoryContainer {
                 String name = getAsString(GeoKeys.PCSCitation);
                 if (name == null) {
                     name = getAsString(GeoKeys.Citation);
-                    // Note that Citation has been removed from the map, so it 
will not be used by 'complete(MetadataBuilder).
+                    // Note that Citation has been removed from the map, so it 
will not be used by `complete(MetadataBuilder)`.
                 }
                 final Unit<Length>  linearUnit  = 
createUnit(GeoKeys.LinearUnits,  GeoKeys.LinearUnitSize, Length.class, 
Units.METRE);
                 final Unit<Angle>   angularUnit = 
createUnit(GeoKeys.AngularUnits, GeoKeys.AngularUnitSize, Angle.class, 
Units.DEGREE);
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 ba9bd62..e88f5e9 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
@@ -29,8 +29,6 @@ import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
 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;
@@ -54,11 +52,9 @@ import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.SampleDimension;
 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;
-import org.apache.sis.measure.Units;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.image.DataType;
 
@@ -119,7 +115,7 @@ final class ImageFileDirectory extends DataCube {
     /**
      * Builder for the metadata. This field is reset to {@code null} when not 
needed anymore.
      */
-    private MetadataBuilder metadata;
+    private ImageMetadataBuilder metadata;
 
     /**
      * {@code true} if this {@code ImageFileDirectory} has not yet read all 
deferred entries.
@@ -336,19 +332,6 @@ final class ImageFileDirectory extends DataCube {
     private Vector colorMap;
 
     /**
-     * The size of the dithering or halftoning matrix used to create a 
dithered or halftoned bilevel file.
-     * This field should be present only if {@code Threshholding} tag is 2 (an 
ordered dither or halftone
-     * technique has been applied to the image data). Special values:
-     *
-     * <ul>
-     *   <li>-1 means that {@code Threshholding} is 1 or unspecified.</li>
-     *   <li>-2 means that {@code Threshholding} is 2 but the matrix size has 
not yet been specified.</li>
-     *   <li>-3 means that {@code Threshholding} is 3 (randomized process such 
as error diffusion).</li>
-     * </ul>
-     */
-    private short cellWidth = -1, cellHeight = -1;
-
-    /**
      * The minimum or maximum sample value found in the image, with one value 
per band.
      * May be a vector of length 1 if the same single value applies to all 
bands.
      */
@@ -361,20 +344,6 @@ final class ImageFileDirectory extends DataCube {
     private boolean isMinSpecified, isMaxSpecified;
 
     /**
-     * The number of pixels per {@link #resolutionUnit} in the {@link 
#imageWidth} and the {@link #imageHeight}
-     * directions, or {@link Double#NaN} is unspecified. Since ISO 19115 does 
not have separated resolution fields
-     * for image width and height, Apache SIS stores only the maximal value.
-     */
-    private double resolution = Double.NaN;
-
-    /**
-     * The unit of measurement for the {@linkplain #resolution} value, or 
{@code null} if none.
-     * A null value is used for images that may have a non-square aspect 
ratio, but no meaningful
-     * absolute dimensions. Default value for TIFF files is inch.
-     */
-    private Unit<Length> resolutionUnit = Units.INCH;
-
-    /**
      * The "no data" or background pixel value, or NaN if undefined.
      *
      * @see #getFillValue(boolean)
@@ -468,7 +437,7 @@ final class ImageFileDirectory extends DataCube {
     ImageFileDirectory(final Reader reader, final int index) {
         super(reader);
         this.index = index;
-        metadata = new MetadataBuilder();
+        metadata = new ImageMetadataBuilder();
     }
 
     /**
@@ -511,8 +480,8 @@ final class ImageFileDirectory extends DataCube {
     /**
      * Adds the value read from the current position in the given stream for 
the entry identified
      * by the given GeoTIFF tag. This method may store the value either in a 
field of this class,
-     * or directly in the {@link MetadataBuilder}. However in the later case, 
this method should
-     * not write anything under the {@code "metadata/contentInfo"} node.
+     * or directly in the {@link ImageMetadataBuilder}. However in the later 
case, this method
+     * should not write anything under the {@code "metadata/contentInfo"} node.
      *
      * @param  tag    the GeoTIFF tag to decode.
      * @param  type   the GeoTIFF type of the value to read.
@@ -1035,10 +1004,7 @@ final class ImageFileDirectory extends DataCube {
              */
             case Tags.XResolution:
             case Tags.YResolution: {
-                final double r = type.readDouble(input(), count);
-                if (Double.isNaN(resolution) || r > resolution) {
-                    resolution = r;
-                }
+                metadata.setResolution(type.readDouble(input(), count));
                 break;
             }
             /*
@@ -1049,14 +1015,8 @@ final class ImageFileDirectory extends DataCube {
              *   3 = Centimeter.
              */
             case Tags.ResolutionUnit: {
-                final int unit = type.readInt(input(), count);
-                switch (unit) {
-                    case 1:  resolutionUnit = null;             break;
-                    case 2:  resolutionUnit = Units.INCH;       break;
-                    case 3:  resolutionUnit = Units.CENTIMETRE; break;
-                    default: return unit;                   // Cause a warning 
to be reported by the caller.
-                }
-                break;
+                return metadata.setResolutionUnit(type.readInt(input(), 
count));
+                // Non-null return value cause a warning to be reported by the 
caller.
             }
             /*
              * For black and white TIFF files that represent shades of gray, 
the technique used to convert
@@ -1067,26 +1027,16 @@ final class ImageFileDirectory extends DataCube {
              *   3 = A randomized process such as error diffusion has been 
applied to the image data.
              */
             case Tags.Threshholding: {
-                final short value = type.readShort(input(), count);
-                switch (value) {
-                    case 1:  break;
-                    case 2:  if (cellWidth >= 0 || cellHeight >= 0) return 
null; else break;
-                    case 3:  break;
-                    default: return value;                  // Cause a warning 
to be reported by the caller.
-                }
-                cellWidth = cellHeight = (short) -value;
-                break;
+                return metadata.setThreshholding(type.readShort(input(), 
count));
+                // Non-null return value cause a warning to be reported by the 
caller.
             }
             /*
              * The width and height of the dithering or halftoning matrix used 
to create
              * a dithered or halftoned bilevel file. Meaningful only if 
Threshholding = 2.
              */
-            case Tags.CellWidth: {
-                cellWidth = type.readShort(input(), count);
-                break;
-            }
+            case Tags.CellWidth:
             case Tags.CellLength: {
-                cellHeight = type.readShort(input(), count);
+                metadata.setCellSize(type.readShort(input(), count), tag == 
Tags.CellWidth);
                 break;
             }
 
@@ -1120,8 +1070,7 @@ final class ImageFileDirectory extends DataCube {
 
             case Tags.GEO_METADATA:
             case Tags.GDAL_METADATA: {
-                final XMLMetadata parser = new XMLMetadata(reader, type, 
count, tag == Tags.GDAL_METADATA);
-                parser.appendTo(metadata);
+                metadata.addXML(new XMLMetadata(reader, type, count, tag));
                 break;
             }
             case Tags.GDAL_NODATA: {
@@ -1356,7 +1305,7 @@ final class ImageFileDirectory extends DataCube {
      */
     @Override
     protected Metadata createMetadata() throws DataStoreException {
-        final MetadataBuilder metadata = this.metadata;
+        final ImageMetadataBuilder metadata = this.metadata;
         if (metadata == null) {
             /*
              * We enter in this block only if an exception occurred during the 
first attempt to build metadata.
@@ -1365,18 +1314,6 @@ final class ImageFileDirectory extends DataCube {
             return super.createMetadata();
         }
         this.metadata = null;     // Clear now in case an exception happens.
-        getIdentifier().ifPresent((id) -> metadata.addTitle(id.toString()));
-        /*
-         * 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(CharSequences.upperCaseToSentence(compression.name()));
-        }
         /*
          * Add information about sample dimensions.
          *
@@ -1394,42 +1331,6 @@ 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));
-        }
-        /*
-         * Cell size is relevant only if the Threshholding TIFF tag value is 
2. By convention in
-         * this implementation class, other Threshholding values are stored as 
negative cell sizes:
-         *
-         *   -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: {
-                // Nothing to report.
-                break;
-            }
-            case -3: {
-                
metadata.addProcessDescription(Resources.formatInternational(Resources.Keys.RandomizedProcessApplied));
-                break;
-            }
-            default: {
-                metadata.addProcessDescription(Resources.formatInternational(
-                            Resources.Keys.DitheringOrHalftoningApplied_2,
-                            (cellWidth  >= 0) ? cellWidth  : '?',
-                            (cellHeight >= 0) ? cellHeight : '?'));
-                break;
-            }
-        }
-        /*
          * Add Coordinate Reference System built from GeoTIFF tags.
          * Note that the CRS may not exist.
          *
@@ -1447,6 +1348,7 @@ final class ImageFileDirectory extends DataCube {
         /*
          * End of metadata construction from TIFF tags.
          */
+        metadata.finish(this);
         final DefaultMetadata md = metadata.build(false);
         if (isIndexValid) {
             final Metadata c = reader.store.customizer.customize(index, md);
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageMetadataBuilder.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageMetadataBuilder.java
new file mode 100644
index 0000000..22db2a8
--- /dev/null
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageMetadataBuilder.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.geotiff;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import org.apache.sis.internal.geotiff.Resources;
+import org.apache.sis.internal.geotiff.Compression;
+import org.apache.sis.internal.storage.MetadataBuilder;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.measure.Units;
+
+
+/**
+ * A temporary object for building the metadata for a single GeoTIFF image.
+ * Fields in the class are used only for information purposes; they do not
+ * have incidence on the way Apache SIS will handle the GeoTIFF image.
+ *
+ * <div class="note"><b>Note:</b>
+ * if those fields become useful to {@link ImageFileDirectory} in a future 
version,
+ * we can move them into that class. Otherwise keeping those fields here allow 
to
+ * discard them (which save a little bit of space) when no longer needed.</div>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class ImageMetadataBuilder extends MetadataBuilder {
+    /**
+     * The number of pixels per {@link #resolutionUnit} in the image width and 
Height directions,
+     * or {@link Double#NaN} is unspecified. Since ISO 19115 does not have 
separated resolution
+     * fields for image width and height, Apache SIS stores only the maximal 
value.
+     */
+    private double resolution = Double.NaN;
+
+    /**
+     * The unit of measurement for the {@linkplain #resolution} value, or 
{@code null} if none.
+     * A null value is used for images that may have a non-square aspect 
ratio, but no meaningful
+     * absolute dimensions. Default value for TIFF files is inch.
+     */
+    private Unit<Length> resolutionUnit = Units.INCH;
+
+    /**
+     * The size of the dithering or halftoning matrix used to create a 
dithered or halftoned bilevel file.
+     * This field should be present only if {@code Threshholding} tag is 2 (an 
ordered dither or halftone
+     * technique has been applied to the image data). Special values:
+     *
+     * <ul>
+     *   <li>-1 means that {@code Threshholding} is 1 or unspecified.</li>
+     *   <li>-2 means that {@code Threshholding} is 2 but the matrix size has 
not yet been specified.</li>
+     *   <li>-3 means that {@code Threshholding} is 3 (randomized process such 
as error diffusion).</li>
+     * </ul>
+     */
+    private short cellWidth = -1, cellHeight = -1;
+
+    /**
+     * Metadata specified in {@code GEO_METADATA} or {@code GDAL_METADATA} 
tags, or {@code null} if none.
+     */
+    private XMLMetadata complement;
+
+    /**
+     * Creates an initially empty metadata builder.
+     */
+    ImageMetadataBuilder() {
+    }
+
+    /**
+     * Encodes the value of Threshholding TIFF tag into the {@link #cellWidth} 
and {@link #cellHeight} fields.
+     * Recognized values are:
+     *
+     * <ul>
+     *   <li>1 = No dithering or halftoning has been applied to the image 
data.</li>
+     *   <li>2 = An ordered dither or halftone technique has been applied to 
the image data.</li>
+     *   <li>3 = A randomized process such as error diffusion has been applied 
to the image data.</li>
+     * </ul>
+     *
+     * @param  value  the threshholding value.
+     * @return {@code null} on success, or the given value if not recognized.
+     */
+    @SuppressWarnings("fallthrough")
+    Integer setThreshholding(final int value) {
+        switch (value) {
+            default: return value;                              // Cause a 
warning to be reported by the caller.
+            case 2:  if ((cellWidth & cellHeight) >= 0) break;  // Exit if at 
least one value is positive, else fallthrough.
+            case 1:  // Fall through
+            case 3:  cellWidth = cellHeight = (short) -value; break;
+        }
+        return null;
+    }
+
+    /**
+     * Sets the width or height of the dithering or halftoning matrix used to 
create
+     * a dithered or halftoned bilevel file. Meaningful only if Threshholding 
= 2.
+     *
+     * @param  size  the new size.
+     * @param  w     {@code true} for setting cell width, or {@code false} for 
setting cell height.
+     */
+    void setCellSize(final short size, final boolean w) {
+        if (w) cellWidth = size;
+        else  cellHeight = size;
+    }
+
+    /**
+     * Sets the resolution to the maximal of current value and given value.
+     */
+    void setResolution(final double r) {
+        if (Double.isNaN(resolution) || r > resolution) {
+            resolution = r;
+        }
+    }
+
+    /**
+     * Sets the unit of measurement for {@code XResolution} and {@code 
YResolution}.
+     * Recognized values are:
+     *
+     * <ul>
+     *   <li>1 = None. Used for images that may have a non-square aspect 
ratio.</li>
+     *   <li>2 = Inch (default).</li>
+     *   <li>3 = Centimeter.</li>
+     * </ul>
+     *
+     * @param  value  the threshholding value.
+     * @return {@code null} on success, or the given value if not recognized.
+     */
+    Integer setResolutionUnit(final int unit) {
+        switch (unit) {
+            case 1:  resolutionUnit = null;             break;
+            case 2:  resolutionUnit = Units.INCH;       break;
+            case 3:  resolutionUnit = Units.CENTIMETRE; break;
+            default: return unit;     // Cause a warning to be reported by the 
caller.
+        }
+        return null;
+    }
+
+    /**
+     * Adds metadata in XML format. Those metadata are defined
+     * in {@code GEO_METADATA} or {@code GDAL_METADATA} tags.
+     */
+    void addXML(final XMLMetadata xml) {
+        if (complement == null) {
+            complement = xml;
+        } else {
+            xml.appendTo(complement);
+        }
+    }
+
+    /**
+     * Completes the metadata with the information stored in the fields of the 
IFD.
+     * This method is invoked only if the user requested the ISO 19115 
metadata.
+     * It should be invoked last, after all other metadata have been set.
+     *
+     * @throws DataStoreException if an error occurred while reading metadata 
from the data store.
+     */
+    void finish(final ImageFileDirectory image) throws DataStoreException {
+        image.getIdentifier().ifPresent((id) -> addTitle(id.toString()));
+        /*
+         * Add information about the file format.
+         *
+         * Destination: metadata/identificationInfo/resourceFormat
+         */
+        final GeoTiffStore store = image.reader.store;
+        if (store.hidden) {
+            store.setFormatInfo(this);       // Should be before 
`addCompression(…)`.
+        }
+        final Compression compression = image.getCompression();
+        if (compression != null) {
+            
addCompression(CharSequences.upperCaseToSentence(compression.name()));
+        }
+        /*
+         * 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) {
+            
addResolution(resolutionUnit.getConverterTo(Units.METRE).convert(resolution));
+        }
+        /*
+         * Cell size is relevant only if the Threshholding TIFF tag value is 
2. By convention in
+         * this implementation class, other Threshholding values are stored as 
negative cell sizes:
+         *
+         *   -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
+         */
+        final int cellWidth  = this.cellWidth;
+        final int cellHeight = this.cellHeight;
+        switch (Math.min(cellWidth, cellHeight)) {
+            case -1: {
+                // Nothing to report.
+                break;
+            }
+            case -3: {
+                
addProcessDescription(Resources.formatInternational(Resources.Keys.RandomizedProcessApplied));
+                break;
+            }
+            default: {
+                addProcessDescription(Resources.formatInternational(
+                            Resources.Keys.DitheringOrHalftoningApplied_2,
+                            (cellWidth  >= 0) ? cellWidth  : '?',
+                            (cellHeight >= 0) ? cellHeight : '?'));
+                break;
+            }
+        }
+        /*
+         * If there is XML metadata, append them last in order
+         * to allow them to be merged with existing metadata.
+         */
+        while (complement != null) try {
+            complement = complement.appendTo(this);
+        } catch (Exception ex) {
+            
image.warning(image.reader.errors().getString(Errors.Keys.CanNotSetPropertyValue_1,
 complement.tag()), ex);
+        }
+    }
+}
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/NativeMetadata.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/NativeMetadata.java
index 594a051..ac1a53c 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/NativeMetadata.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/NativeMetadata.java
@@ -177,7 +177,7 @@ final class NativeMetadata extends GeoKeysLoader {
                             }
                             case Tags.GDAL_METADATA:
                             case Tags.GEO_METADATA: {
-                                children = new XMLMetadata(reader, type, 
count, tag == Tags.GDAL_METADATA);
+                                children = new XMLMetadata(reader, type, 
count, tag);
                                 if (children.isEmpty()) {
                                     // Fallback on showing array of numerical 
values.
                                     value = type.readVector(input, count);
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/XMLMetadata.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/XMLMetadata.java
index c8b444f..4c9b3ac 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/XMLMetadata.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/XMLMetadata.java
@@ -112,12 +112,19 @@ final class XMLMetadata implements Filter {
     private final boolean isGDAL;
 
     /**
+     * The next metadata in a list of linked metadata. Should always be {@code 
null},
+     * but we nevertheless define this field in case a file defines more than 
one
+     * {@code GEO_METADATA} or {@code GDAL_METADATA} tags.
+     */
+    private XMLMetadata next;
+
+    /**
      * Creates a new instance with the given XML. Used for testing purposes.
      */
     XMLMetadata(final String xml, final boolean isGDAL) {
         this.isGDAL = isGDAL;
-        string = xml;
-        listeners = null;
+        this.string = xml;
+        listeners   = null;
     }
 
     /**
@@ -126,10 +133,10 @@ final class XMLMetadata implements Filter {
      * @param  reader  the TIFF reader.
      * @param  type    type of the metadata tag to read.
      * @param  count   number of bytes or characters in the value to read.
-     * @param  isGDAL  {@code true} if the XML is GDAL metadata.
+     * @param  tag     the tag where the metadata was stored.
      */
-    XMLMetadata(final Reader reader, final Type type, final long count, final 
boolean isGDAL) throws IOException {
-        this.isGDAL = isGDAL;
+    XMLMetadata(final Reader reader, final Type type, final long count, final 
short tag) throws IOException {
+        isGDAL = (tag == Tags.GDAL_METADATA);
         listeners = reader.store.listeners();
         switch (type) {
             case ASCII: {
@@ -154,6 +161,29 @@ final class XMLMetadata implements Filter {
     }
 
     /**
+     * Appends this metadata at the end of a linked list starting with the 
given element.
+     * This method is inefficient because it iterates over all elements for 
reaching the tail,
+     * but it should not be an issue because this method is invoked only in 
the unlikely case
+     * where a file would define more than one {@code *_METADATA} tag.
+     *
+     * @param  head  first element of the linked list where to append this 
metadata.
+     */
+    final void appendTo(XMLMetadata head) {
+        while (head.next != null) {
+            head = head.next;
+        }
+        head.next = this;
+    }
+
+    /**
+     * Returns the name of the tag from which the XML has been read.
+     * This is used for error messages.
+     */
+    String tag() {
+        return Tags.name(isGDAL ? Tags.GDAL_METADATA : Tags.GEO_METADATA);
+    }
+
+    /**
      * Returns {@code true} if the XML document could not be read.
      */
     public boolean isEmpty() {
@@ -328,8 +358,9 @@ final class XMLMetadata implements Filter {
      * @param  metadata  the builder where to append the content of this 
{@code XMLMetadata}.
      * @throws XMLStreamException if an error occurred while parsing the XML.
      * @throws JAXBException if an error occurred while parsing the XML.
+     * @return the next metadata in a linked list of metadata, or {@code null} 
if none.
      */
-    public void appendTo(final MetadataBuilder metadata) throws 
XMLStreamException, JAXBException {
+    public XMLMetadata appendTo(final MetadataBuilder metadata) throws 
XMLStreamException, JAXBException {
         final XMLEventReader reader = toXML();
         if (reader != null) {
             if (isGDAL) {
@@ -361,6 +392,7 @@ final class XMLMetadata implements Filter {
             }
             reader.close();     // No need to close the underlying input 
stream.
         }
+        return next;
     }
 
     /**
diff --git 
a/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/XMLMetadataTest.java
 
b/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/XMLMetadataTest.java
index f59eb03..0ebe0ca 100644
--- 
a/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/XMLMetadataTest.java
+++ 
b/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/XMLMetadataTest.java
@@ -125,7 +125,7 @@ public final strictfp class XMLMetadataTest extends 
TestCase {
     public void testMetadataGDAL() throws Exception {
         XMLMetadata xml = new XMLMetadata(GDAL_METADATA, true);
         MetadataBuilder builder = new MetadataBuilder();
-        xml.appendTo(builder);
+        assertNull(xml.appendTo(builder));
         DefaultMetadata metadata = builder.build(false);
         assertMultilinesEquals(
                 "Metadata\n" +
@@ -146,7 +146,7 @@ public final strictfp class XMLMetadataTest extends 
TestCase {
     public void testGeoMetadata() throws Exception {
         XMLMetadata xml = new XMLMetadata(GEO_METADATA, false);
         MetadataBuilder builder = new MetadataBuilder();
-        xml.appendTo(builder);
+        assertNull(xml.appendTo(builder));
         DefaultMetadata metadata = builder.build(false);
         assertMultilinesEquals(
                 "Metadata\n" +
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 ede11a6..c679471 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
@@ -3207,6 +3207,10 @@ parse:      for (int i = 0; i < length;) {
      * The given source should be an instance of {@link Metadata},
      * but some types of metadata components are accepted as well.
      *
+     * <p>This method should be invoked last, just before the call to {@link 
#build(boolean)}.
+     * Any identification information, responsible party, extent, coverage 
description, <i>etc.</i>
+     * added after this method call will be stored in new metadata object (not 
merged).</p>
+     *
      * @param  source  the source metadata to merge. Will never be modified.
      * @param  locale  the locale to use for error message in exceptions, or 
{@code null} for the default locale.
      * @return {@code true} if the given source has been merged,
@@ -3217,6 +3221,7 @@ parse:      for (int i = 0; i < length;) {
      * @see Merger
      */
     public boolean mergeMetadata(final Object source, final Locale locale) {
+        flush();
         final ModifiableMetadata target;
              if (source instanceof Metadata)                    target = 
metadata();
         else if (source instanceof DataIdentification)          target = 
identification();
@@ -3246,14 +3251,11 @@ parse:      for (int i = 0; i < length;) {
     }
 
     /**
-     * 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.
-     *
-     * @param  freeze  {@code true} if this method should set the returned 
metadata to
-     *                 {@link DefaultMetadata.State#FINAL}, or {@code false} 
for leaving the metadata editable.
-     * @return the metadata, or {@code null} if none.
+     * Writes all pending metadata objects into the {@link DefaultMetadata} 
root class.
+     * Then all {@link #identification}, {@link #gridRepresentation}, 
<i>etc.</i> fields
+     * except {@link #metadata} are set to {@code null}.
      */
-    public final DefaultMetadata build(final boolean freeze) {
+    private void flush() {
         newIdentification();
         newGridRepresentation(GridType.UNSPECIFIED);
         newFeatureTypes();
@@ -3261,19 +3263,28 @@ parse:      for (int i = 0; i < length;) {
         newAcquisition();
         newDistribution();
         newLineage();
-        final DefaultMetadata md = metadata;
-        metadata = null;
-        if (md != null) {
-            if (standardISO != 0) {
-                List<Citation> c = Citations.ISO_19115;
-                if (standardISO == 1) {
-                    c = Collections.singletonList(c.get(0));
-                }
-                md.setMetadataStandards(c);
-            }
-            if (freeze) {
-                md.transitionTo(DefaultMetadata.State.FINAL);
+    }
+
+    /**
+     * Returns the metadata, optionally as an unmodifiable object.
+     * If {@code freeze} is {@code true}, then the returned metadata instance 
can not be modified.
+     *
+     * @param  freeze  {@code true} if this method should set the returned 
metadata to
+     *                 {@link DefaultMetadata.State#FINAL}, or {@code false} 
for leaving the metadata editable.
+     * @return the metadata, never {@code null}.
+     */
+    public final DefaultMetadata build(final boolean freeze) {
+        flush();
+        final DefaultMetadata md = metadata();
+        if (standardISO != 0) {
+            List<Citation> c = Citations.ISO_19115;
+            if (standardISO == 1) {
+                c = Collections.singletonList(c.get(0));
             }
+            md.setMetadataStandards(c);
+        }
+        if (freeze) {
+            md.transitionTo(DefaultMetadata.State.FINAL);
         }
         return md;
     }

Reply via email to