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(); } /**