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; }