/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2007-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.image.io.metadata; import java.text.Format; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.LogRecord; import javax.imageio.ImageReader; import javax.imageio.ImageWriter; import javax.imageio.spi.ImageReaderWriterSpi; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.metadata.IIOMetadataFormat; import javax.imageio.metadata.IIOInvalidTreeException; import org.w3c.dom.Node; import org.opengis.coverage.grid.RectifiedGrid; import org.opengis.metadata.Metadata; import org.opengis.metadata.content.ImageDescription; import org.opengis.metadata.extent.GeographicBoundingBox; import org.opengis.metadata.identification.DataIdentification; import org.opengis.metadata.acquisition.AcquisitionInformation; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.geotoolkit.gui.swing.tree.Trees; import org.apache.sis.util.ArraysExt; import org.apache.sis.measure.NumberRange; import org.geotoolkit.util.logging.LoggedFormat; import org.geotoolkit.image.io.WarningProducer; import org.geotoolkit.internal.image.io.Warnings; import org.apache.sis.measure.RangeFormat; import org.apache.sis.util.resources.IndexedResourceBundle; import org.geotoolkit.resources.Errors; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; /** * Spatial (usually geographic) informations encoded in an image file. This class converts the * {@link IIOMetadataNode} elements and attribute values to ISO 19115-2 metadata objects. * While ISO 19115-2 is the primary standard supported by this class, other standards can * works if they are designed with the same rules than the {@link org.opengis.metadata} * package. * * {@section Reading} * ISO 19115-2 metadata instances are obtained by {@link #getInstanceForType(Class)} (for * a single instance) or {@link #getListForType(Class)} (for a list of metadata instances) * methods. The table below lists some common metadata elements. The "<cite>Format</cite>" * and "<cite>Path to node</cite>" columns give the location of the metadata element in a * tree conform to the <a href="SpatialMetadataFormat.html#default-formats">spatial metadata * format</a> defined in this package. * <p> * <blockquote><table border="1" cellspacing="0"> * <tr bgcolor="lightblue"> * <th> Format </th> * <th> Path to node </th> * <th> Method call </th> * </tr> * <tr> * <td> {@link SpatialMetadataFormat#getStreamInstance(String) Stream} </td> * <td> {@code "DiscoveryMetadata"}</td> * <td> <code>getInstanceForType({@linkplain DataIdentification}.class)</code></td> * </tr> * <tr> * <td> {@link SpatialMetadataFormat#getStreamInstance(String) Stream} </td> * <td> {@code "DiscoveryMetadata/Extent/GeographicElement"}</td> * <td> <code>getInstanceForType({@linkplain GeographicBoundingBox}.class)</code></td> * </tr> * <tr> * <td> {@link SpatialMetadataFormat#getStreamInstance(String) Stream} </td> * <td> {@code "AcquisitionMetadata"}</td> * <td> <code>getInstanceForType({@linkplain AcquisitionInformation}.class)</code></td> * </tr> * <tr> * <td> {@link SpatialMetadataFormat#getImageInstance(String) Image} </td> * <td> {@code "ImageDescription"}</td> * <td> <code>getInstanceForType({@linkplain ImageDescription}.class)</code></td> * </tr> * <tr> * <td> {@link SpatialMetadataFormat#getImageInstance(String) Image} </td> * <td> {@code "ImageDescription/Dimensions"}</td> * <td> <code>getListForType({@linkplain SampleDimension}.class)</code></td> * </tr> * <tr> * <td> {@link SpatialMetadataFormat#getImageInstance(String) Image} </td> * <td> {@code "RectifiedGridDomain"}</td> * <td> <code>getInstanceForType({@linkplain RectifiedGrid}.class)</code></td> * </tr> * </table></blockquote> * <p> * The {@code getInstanceForType(Class)} and {@code getListForType(Class)} methods are * tolerant to metadata located in other paths than the ones documented below, provided * that the underlying metadata {@linkplain #format} declares exactly one element accepting * a {@linkplain IIOMetadataFormat#getObjectClass(String) user object} * {@linkplain Class#isAssignableFrom(Class) assignable} to the given type. * In case of ambiguity, an {@link IllegalArgumentException} is thrown. * * {@section Writing} * Unless this metadata {@linkplain #isReadOnly() is read only}, it is possible to store * the root of a metadata tree using the {@link #mergeTree(String, Node)} method. * * {@section Errors handling} * If some inconsistency are found while reading (for example if the coordinate system * dimension doesn't match the envelope dimension), then the default implementation * {@linkplain #warningOccurred logs a warning}. We do not throw an exception because * minor errors are not uncommon in geographic data, and we want to process the data on * a "<cite>best effort</cite>" basis. However because every warnings are logged through * the {@code warningOccurred} method, subclasses can override this method if they want * to treat some warnings as fatal errors. * * @author Martin Desruisseaux (Geomatys) * @version 3.20 * * @see SpatialMetadataFormat * * @since 3.04 (derived from 2.4) * @module */ public class SpatialMetadata extends IIOMetadata implements WarningProducer { /** * Enumeration of values returned by {@link #getFormatCode(String)}. * * @since 3.20 */ private static final int MAIN=0, ISO=1, FALLBACK=2; /** * An empty {@code SpatialMetadata} with no data and no format. This constant is * an alternative to {@code null} for meaning that no metadata are available. * * @since 3.06 */ public static final SpatialMetadata EMPTY = new SpatialMetadata(); /** * The preferred metadata format, which was given at construction time. * The preferred metadata format is not necessarily the only one. In particular, the * {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#ISO_FORMAT_NAME} format * may also be accepted. * * @see SpatialMetadataFormat#getStreamInstance(String) * @see SpatialMetadataFormat#getImageInstance(String) */ public final IIOMetadataFormat format; /** * The {@link ImageReader} or {@link ImageWriter} that holds the metadata, * or {@code null} if none. */ private final Object owner; /** * The metadata provided by standard {@link ImageReader} instances, on which to fallback * if the user asked for something else than the spatial metadata managed by this class. * This is {@code null} if there is no such fallback. */ final IIOMetadata fallback; /** * The root node to be returned by {@link #getAsTree()}. */ private Node root; /** * The root node to be returned by {@link #getAsTree(String)} for the ISO-19115 format. * * @since 3.20 */ private Node rootISO; /** * The values created by {@link #getInstanceForType(Class)}, cached for reuse. */ private transient Map<Class<?>, Object> instances; /** * The values created by {@link #getListForType(Class)}, cached for reuse. */ private transient Map<Class<?>, List<?>> lists; /** * The standard date format. Will be created only when first needed. * * @see #dateFormat() */ private transient LoggedFormat<Date> dateFormat; /** * The standard range format. Will be created only when first needed. * * @see #rangeFormat() */ private transient LoggedFormat<NumberRange<?>> rangeFormat; /** * The default logging level for the warnings. This is given to the * {@link MetadataNodeParser} objects created for this metadata. */ private Level warningLevel; /** * {@code true} if this {@code SpatialMetadata} is read-only. * The default value is {@code false}. * * @see #isReadOnly() * * @since 3.08 */ private boolean isReadOnly; /** * Creates a metadata with no format. This constructor * is for the {@link #EMPTY} constant only. */ private SpatialMetadata() { format = null; owner = null; fallback = null; isReadOnly = true; } /** * Creates an initially empty metadata instance for the given format. * The {@code format} argument is usually one of the {@link SpatialMetadataFormat} predefined * {@linkplain SpatialMetadataFormat#getStreamInstance(String) stream} or * {@linkplain SpatialMetadataFormat#getImageInstance(String) image} instances, * but other formats are allowed. * * @param format The metadata format. */ public SpatialMetadata(final IIOMetadataFormat format) { this(format, false, null, null, null); } /** * Creates an initially empty metadata instance for the given format and reader. * The {@code format} argument is usually one of the {@link SpatialMetadataFormat} predefined * {@linkplain SpatialMetadataFormat#getStreamInstance(String) stream} or * {@linkplain SpatialMetadataFormat#getImageInstance(String) image} instances, * but other formats are allowed. * <p> * If the {@code fallback} argument is non-null, then any call to a * {@link #getMetadataFormat(String) getMetadataFormat}, {@link #getAsTree(String) getAsTree}, * {@link #mergeTree(String,Node) mergeTree} or {@link #setFromTree(String,Node) setFromTree} * method with an unrecognized format name will be delegated to that fallback. This is useful * when the given {@code ImageReader} is actually a wrapper around an other {@code ImageReader}. * <p> * This constructor does not inherit the metadata formats declared in the * {@linkplain ImageReader#getOriginatingProvider() originating provider}, because this * constructor doesn't specify which one of stream or image metadata should be inherited. * * @param format The metadata format. * @param reader The source image reader, or {@code null} if none. * @param fallback The fallback for any format name different than {@code format.getRootName()}, * or {@code null} if none. */ public SpatialMetadata(final IIOMetadataFormat format, final ImageReader reader, final IIOMetadata fallback) { this(format, false, reader, null, fallback); } /** * Creates an initially empty metadata instance for the * {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} format * and the given reader. In addition to the Geotk * in <a href="SpatialMetadataFormat.html#default-formats">spatial metadata format</a>, * this constructor inherits other stream or image formats defined in the * {@linkplain ImageReader#getOriginatingProvider() originating provider}. * * @param isStreamMetadata {@code true} for <em>stream</em> metadata, * or {@code false} for <em>image</em> metadata. * @param reader The source image reader, or {@code null} if none. * @param fallback The fallback for any format name different than * {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME}, * or {@code null} if none. * * @since 3.20 */ public SpatialMetadata(final boolean isStreamMetadata, final ImageReader reader, final IIOMetadata fallback) { this(isStreamMetadata ? SpatialMetadataFormat.getStreamInstance(SpatialMetadataFormat.GEOTK_FORMAT_NAME) : SpatialMetadataFormat.getImageInstance (SpatialMetadataFormat.GEOTK_FORMAT_NAME), isStreamMetadata, reader, (reader != null) ? reader.getOriginatingProvider() : null, fallback); } /** * Creates an initially empty metadata instance for the given format and writer. * The {@code format} argument is usually one of the {@link SpatialMetadataFormat} predefined * {@linkplain SpatialMetadataFormat#getStreamInstance stream} or * {@linkplain SpatialMetadataFormat#getImageInstance image instances}, * but other formats are allowed. * <p> * If the {@code fallback} argument is non-null, then any call to a * {@link #getMetadataFormat(String) getMetadataFormat}, {@link #getAsTree(String) getAsTree}, * {@link #mergeTree(String,Node) mergeTree} or {@link #setFromTree(String,Node) setFromTree} * method with an unrecognized format name will be delegated to that fallback. This is useful * when the given {@code ImageWriter} is actually a wrapper around an other {@code ImageWriter}. * <p> * This constructor does not inherit the metadata formats declared in the * {@linkplain ImageWriter#getOriginatingProvider() originating provider}, because this * constructor doesn't specify which one of stream or image metadata should be inherited. * * @param format The metadata format. * @param writer The target image writer, or {@code null} if none. * @param fallback The fallback for any format name different than {@code format.getRootName()}, * or {@code null} if none. */ public SpatialMetadata(final IIOMetadataFormat format, final ImageWriter writer, final IIOMetadata fallback) { this(format, false, writer, null, fallback); } /** * Creates an initially empty metadata instance for the * {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} format * and the given writer. In addition to the Geotk * in <a href="SpatialMetadataFormat.html#default-formats">spatial metadata format</a>, * this constructor inherits other stream or image formats defined in the * {@linkplain ImageWriter#getOriginatingProvider() originating provider}. * * @param isStreamMetadata {@code true} for <em>stream</em> metadata, * or {@code false} for <em>image</em> metadata. * @param writer The source image writer, or {@code null} if none. * @param fallback The fallback for any format name different than * {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME}, * or {@code null} if none. * * @since 3.20 */ public SpatialMetadata(final boolean isStreamMetadata, final ImageWriter writer, final IIOMetadata fallback) { this(isStreamMetadata ? SpatialMetadataFormat.getStreamInstance(SpatialMetadataFormat.GEOTK_FORMAT_NAME) : SpatialMetadataFormat.getImageInstance (SpatialMetadataFormat.GEOTK_FORMAT_NAME), isStreamMetadata, writer, (writer != null) ? writer.getOriginatingProvider() : null, fallback); } /** * Creates an initially empty metadata instance for the given format and reader/writer. * The format name of the given fallback are merged with the name of the given format. */ private SpatialMetadata(final IIOMetadataFormat format, final boolean isStreamMetadata, final Object owner, final ImageReaderWriterSpi spi, final IIOMetadata fallback) { ensureNonNull("format", format); this.format = format; this.owner = owner; this.fallback = fallback; if (spi != null) { if (isStreamMetadata) { standardFormatSupported = spi.isStandardStreamMetadataFormatSupported(); nativeMetadataFormatName = spi.getNativeStreamMetadataFormatName(); extraMetadataFormatNames = spi.getExtraStreamMetadataFormatNames(); } else { standardFormatSupported = spi.isStandardImageMetadataFormatSupported(); nativeMetadataFormatName = spi.getNativeImageMetadataFormatName(); extraMetadataFormatNames = spi.getExtraImageMetadataFormatNames(); } } if (fallback != null) { if (!standardFormatSupported) { standardFormatSupported = fallback.isStandardMetadataFormatSupported(); } if (nativeMetadataFormatName == null) { nativeMetadataFormatName = fallback.getNativeMetadataFormatName(); } extraMetadataFormatNames = ArraysExt.concatenate(extraMetadataFormatNames, fallback.getExtraMetadataFormatNames()); extraMetadataFormatNames = ArraysExt.resize(extraMetadataFormatNames, ArraysExt.removeDuplicated(extraMetadataFormatNames)); } final String rootName = format.getRootName(); if (!isSupportedFormat(rootName)) { if (nativeMetadataFormatName == null) { nativeMetadataFormatName = rootName; } else if (extraMetadataFormatNames == null) { extraMetadataFormatNames = new String[] {rootName}; } else { extraMetadataFormatNames = ArraysExt.append(extraMetadataFormatNames, rootName); } } /* * Note: we leave 'nativeMetadataFormatClassName' and 'extraMetadataFormatClassNames' * to null, which is illegal. However the only method to use those informations is * IIOMetadata.getMetadataFormat(String), so this is okay if we overload that method. * * Getting the format class would be costly since there is no IIOMetadata method * giving that information. */ } /** * Returns an instance of the given type extracted from this {@code IIOMetadata}. * This method performs the following steps: * * <ol> * <li><p><b>Invoke <code>{@linkplain MetadataNodeParser#listPaths * MetadataNodeParser.listPaths}({@linkplain #format}, type)</code>:</b><br> * * Search for an element declared in the {@linkplain #format} specified at construction * time which accept a {@linkplain IIOMetadataNode#getUserObject() user object} of the * given type. Exactly one element is expected, otherwise an {@link IllegalArgumentException} * is thrown.</p></li> * * <li><p><b>Create a <code>new {@linkplain MetadataNodeParser#MetadataNodeParser(IIOMetadata, * String, String, String) MetadataNodeParser}(this, {@linkplain #format}.getRootName(), * path, "#auto")</code>:</b><br> * * Create a new metadata accessor for the single path found at the previous step.</p></li> * * <li><p><b>Invoke <code>{@linkplain MetadataNodeParser#getUserObject(Class) * MetadataNodeParser.getUserObject}(type)</code>:</b><br> * * If a user object was explicitly specified, return that object.</p></li> * * <li><p><b>Invoke <code>{@linkplain MetadataNodeParser#newProxyInstance(Class) * MetadataNodeParser.newProxyInstance}(type)</code>:</b><br> * * If no explicit user object was found, create a proxy which will implement the getter * methods by a code that fetch the value from the corresponding attribute.</p></li> * </ol> * * If this {@code SpatialMetadata} does not contain a node for the given type, then this * method returns {@code null}. * * @param <T> The compile-time type specified as the {@code type} argument. * @param type The interface implemented by the instance to fetch. * @return An implementation of the given interface, or {@code null} if none. * @throws IllegalArgumentException If the given type is not a valid interface, * or if no element or more than one element exist for the given type. * * @see MetadataNodeParser#newProxyInstance(Class) * * @since 3.06 */ public <T> T getInstanceForType(final Class<T> type) throws IllegalArgumentException { if (instances == null) { instances = new HashMap<>(); } @SuppressWarnings("unchecked") T object = (T) instances.get(type); if (object == null) { /* * No previous instance in the cache. Before to create a new instance, * check for the special case of CRS objects. This special case will * create "real" Geotk CRS objects, not proxies that use reflection. */ if (CoordinateReferenceSystem.class.isAssignableFrom(type)) { final ReferencingBuilder builder; try { builder = new ReferencingBuilder(new MetadataNodeParser(this, SpatialMetadataFormat.GEOTK_FORMAT_NAME, ReferencingBuilder.PATH, null)); } catch (NoSuchElementException e) { return null; // As of method contract. } object = type.cast(builder.build()); } else { /* * For the general case (typically ISO 19115-2 metadata), create * a proxy which will fetch attribute values using reflections. */ final MetadataNodeParser accessor; try { accessor = new MetadataNodeParser(this, getMetadataFormatName(type), type); } catch (NoSuchElementException e) { return null; // As of method contract. } object = getInstanceForType(type, accessor); } instances.put(type, object); } return object; } /** * Clear all type instances cached. * This method should be called is metadata is updated and we want to force futures * {@link #getInstanceForType(Class)} calls to return new type instances on updated metadata. */ public void clearInstancesCache() { if(instances!=null){ instances.clear(); } } /** * Returns an instance of the given type extracted from this {@code IIOMetadata} at the given * path. This method performs the same work than {@link #getInstanceForType(Class)}, except * that the path is explicitly specified instead than automatically inferred from the type. * <p> * <b>Example:</b> * * {@preformat java * EnvironmentalRecord er = getInstanceForType(EnvironmentalRecord.class, "AcquisitionMetadata/EnvironmentalConditions"); * } * * If this {@code SpatialMetadata} does not contain a node for the given path, then this * method returns {@code null}. * * {@note At the opposite of <code>getInstanceForType(Class)</code>, the current implementation * of this method does not cache the returned proxy. Caching may be implemented in a * future version.} * * @param <T> The compile-time type specified as the {@code type} argument. * @param type The interface implemented by the instance to fetch. * @param path The path where to fetch the metadata. * @return An implementation of the given interface, or {@code null} if none. * @throws IllegalArgumentException If the given type is not a valid interface. * * @since 3.07 */ public <T> T getInstanceForType(final Class<T> type, final String path) throws IllegalArgumentException { // Handle CRS in the same special way than getInstanceForType(Class). final boolean isCRS = CoordinateReferenceSystem.class.isAssignableFrom(type); final MetadataNodeParser accessor; try { accessor = new MetadataNodeParser(this, getMetadataFormatName(type), path, isCRS ? null : "#auto"); } catch (NoSuchElementException e) { return null; // As of method contract. } if (isCRS) { return type.cast(new ReferencingBuilder(accessor).build()); } else { return getInstanceForType(type, accessor); } } /** * Returns the instance for the given type using the given accessor. * <p> * The normal usage is to invoke this method for a singleton. However if the user invoked * this method for a list (he should have invoked {@link #getListForType(Class)} instead), * returns the first element of that list as a convenience, creating an empty one if needed. */ private <T> T getInstanceForType(final Class<T> type, final MetadataNodeParser accessor) { T object; if (accessor.allowsChildren()) { final List<T> list = getListForType(type); if (list.isEmpty()) { return null; } object = list.get(0); } else { // The normal case (singleton). object = accessor.getUserObject(type); if (object == null) { object = accessor.newProxyInstance(type); } } return object; } /** * Returns a list of instances of the given type extracted from this {@code IIOMetadata}. * This method performs the same work than {@link #getInstanceForType(Class)}, but for a * list. * <p> * If this {@code SpatialMetadata} does not contain a node for the given type, then this * method returns {@code null}. * <p> * Note that a {@code null} return value has a different meaning than an empty list: * an empty list means that the node exists but have no children, while a {@code null} * value means that the parent node does not exist. * * @param <T> The compile-time type specified as the {@code type} argument. * @param type The interface implemented by the elements of the list to fetch. * @return A list of implementations of the given interface, or {@code null} if none. * @throws IllegalArgumentException If the given type is not a valid interface, * or if no element or more than one element exist for the given type. * * @see MetadataNodeParser#newProxyList(Class) * * @since 3.06 */ public <T> List<T> getListForType(final Class<T> type) throws IllegalArgumentException { if (lists == null) { lists = new HashMap<>(); } @SuppressWarnings("unchecked") List<T> list = (List<T>) lists.get(type); if (list == null) { final MetadataNodeParser accessor; try { accessor = new MetadataNodeParser(this, getMetadataFormatName(type), type); } catch (NoSuchElementException e) { return null; // As of method contract. } list = accessor.newProxyList(type); lists.put(type, list); } return list; } /** * Returns a list of instances of the given type extracted from this {@code IIOMetadata} at the * given path. This method performs the same work than {@link #getListForType(Class)}, except * that the path and the name of children are explicitly specified instead than automatically * inferred from the type. * <p> * <b>Example:</b> * * {@preformat java * Instrument it = getListForType(Instrument.class, "AcquisitionMetadata/Platform/Instruments", "Instrument"); * } * * If this {@code SpatialMetadata} does not contain a node for the given path, then this * method returns {@code null}. * <p> * Note that a {@code null} return value has a different meaning than an empty list: * an empty list means that the node exists but have no children, while a {@code null} * value means that the parent node does not exist. * * {@note At the opposite of <code>getListForType(Class)</code>, the current implementation * of this method does not cache the returned proxy. Caching may be implemented in a * future version.} * * @param <T> The compile-time type specified as the {@code type} argument. * @param type The interface implemented by the elements of the list to fetch. * @param path The path of the parent node where to fetch the metadata. * @param children The name of children under the parent node. * @return A list of implementations of the given interface, or {@code null} if none. * @throws IllegalArgumentException If the given type is not a valid interface. * * @since 3.07 */ public <T> List<T> getListForType(final Class<T> type, final String path, final String children) throws IllegalArgumentException { final MetadataNodeParser accessor; try { accessor = new MetadataNodeParser(this, getMetadataFormatName(type), path, children); } catch (NoSuchElementException e) { return null; // As of method contract. } return accessor.newProxyList(type); } /** * The preferred metadata format name for {@link #getInstanceForType(Class)} methods. * If the preferred metadata format is not found, then fallback on the native format. * <p> * We don't compute those information at construction time, because the user could * subclass {@code SpatialMetadata} and modify the fields in their own constructor. * * @param type The type of metadata object to create. * @return The preferred metadata format name, or {@code null} if unknown. * * @since 3.20 */ private String getMetadataFormatName(final Class<?> type) { final String preferred, alternate; if (Metadata.class.isAssignableFrom(type)) { preferred = SpatialMetadataFormat.ISO_FORMAT_NAME; alternate = SpatialMetadataFormat.GEOTK_FORMAT_NAME; } else { preferred = SpatialMetadataFormat.GEOTK_FORMAT_NAME; alternate = SpatialMetadataFormat.ISO_FORMAT_NAME; } String fallback = nativeMetadataFormatName; if (!preferred.equalsIgnoreCase(fallback)) { final String[] names = extraMetadataFormatNames; if (names != null) { for (final String name : names) { if (preferred.equalsIgnoreCase(name)) return name; if (alternate.equalsIgnoreCase(name)) fallback = name; } } } return fallback; } /** * Returns {@code true} if the format of the given name is supported. * This method does not verify the standard format name. */ private boolean isSupportedFormat(final String name) { return name.equals(nativeMetadataFormatName) || ArraysExt.contains(extraMetadataFormatNames, name); } /** * Returns a code for the given metadata format name. This method returns: * <p> * <ul> * <li>{@link #MAIN} if we should use the {@linkplain #format} given at construction time;</li> * <li>{@link #ISO} if we should use the ISO 19115-2 format;</li> * <li>{@link #FALLBACK} if we should use the {@linkplain #fallback};</li> * <li>or thrown an exception otherwise.</li> * </ul> */ private int getFormatCode(final String formatName) throws IllegalArgumentException { ensureNonNull("formatName", formatName); if (format == null) { throw new IllegalStateException(getErrorResources().getString(Errors.Keys.UndefinedFormat)); } if (format.getRootName().equalsIgnoreCase(formatName)) { return MAIN; } if (SpatialMetadataFormat.ISO_FORMAT_NAME.equalsIgnoreCase(formatName) && isSupportedFormat(SpatialMetadataFormat.ISO_FORMAT_NAME)) { return ISO; } if (fallback != null) { return FALLBACK; } throw new IllegalArgumentException(getErrorResources().getString( Errors.Keys.IllegalArgument_2, "formatName", formatName)); } /** * Returns an object describing the given metadata format, or {@code null} if no description is * available. The default implementation is as below: * <p> * <ul> * <li>If {@code formatName} is equals, ignoring case, to <code>{@linkplain #format}.getRootName()</code> * where {@code format} is the argument given at construction time, then return that format.</li> * <li>Otherwise if {@code formatName} is equals, ignoring case, to * {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#ISO_FORMAT_NAME}, * then return the ISO-19115 format.</li> * <li>Otherwise if a fallback has been specified at construction time, * then delegate to that fallback.</li> * <li>Otherwise throw an {@code IllegalArgumentException}.</li> * </ul> * * @param formatName The desired metadata format. * @return The desired metadata format, or {@code null} if none. * @throws IllegalArgumentException If the specified format name is not recognized. */ @Override public IIOMetadataFormat getMetadataFormat(final String formatName) throws IllegalArgumentException { switch (getFormatCode(formatName)) { case MAIN: return format; case ISO: return SpatialMetadataFormat.getStreamInstance(SpatialMetadataFormat.ISO_FORMAT_NAME); default: return fallback.getMetadataFormat(formatName); } // We do not invoke super.getMetadataFormat(...) because the 'nativeMetadataFormatClassName' // and 'extraMetadataFormatClassNames' were left to null by the constructors. } /** * Returns the root of a tree of metadata contained within this object according to the * conventions defined by the metadata {@linkplain #format} associated to this instance. */ final Node getAsTree() { if (format == null) { throw new UnsupportedOperationException(); } if (root == null) { root = new IIONode(format.getRootName()); } return root; } /** * Returns the root of a tree of metadata contained within this object * according to the conventions defined by a given metadata format. * * @param formatName the desired metadata format. * @return The node forming the root of metadata tree. * @throws IllegalArgumentException if the format name is {@code null} or is not * one of the names returned by {@link #getMetadataFormatNames()}. */ @Override public Node getAsTree(final String formatName) throws IllegalArgumentException { switch (getFormatCode(formatName)) { case MAIN: return getAsTree(); case ISO: return rootISO; default: return fallback.getAsTree(formatName); } } /** * Alters the internal state of this metadata from a tree whose syntax is defined by * the given metadata format. The semantics of how a tree or subtree may be merged with * another tree are format-specific. It may be simply replacing all existing state with * the contents of the given tree. * * @param formatName The desired metadata format. * @param root An XML DOM Node object forming the root of a tree. * @throws IllegalStateException If this metadata is {@linkplain #isReadOnly() read only}. * @throws IIOInvalidTreeException If the tree cannot be parsed successfully using the * rules of the given format. */ @Override public void mergeTree(final String formatName, final Node root) throws IIOInvalidTreeException { if (isReadOnly()) { throw new IllegalStateException(getErrorResources() .getString(Errors.Keys.UnmodifiableMetadata)); } switch (getFormatCode(formatName)) { case MAIN: this.root = root; break; case ISO: rootISO = root; break; default: fallback.mergeTree(formatName, root); break; } } /** * Returns {@code true} if this object does not allows modification. The default value is * {@code false}. If the read-only state is set to {@code true}, then: * * <ul> * <li><p>Calls to {@link #mergeTree mergeTree}, {@link #setFromTree setFromTree} and * {@link #reset reset} methods will throw an {@link IllegalStateException}, as * required by the standard {@link IIOMetadata} contract.</p></li> * * <li><p>Creation of {@link MetadataNodeAccessor}s will throw a {@link NoSuchElementException} * if the named element does not exist in this {@code SpatialMetadata} object. Note that * if the {@code SpatialMetadata} has not been declared read-only, then the default * {@code MetadataNodeAccessor} behavior is to create any missing nodes.</p></li> * </ul> */ @Override public boolean isReadOnly() { return isReadOnly; } /** * Sets whatever this {@code SpatialMetadata} should be read-only. * * @param ro The new read-only state of this metadata object. * * @since 3.08 */ public void setReadOnly(final boolean ro) { isReadOnly = ro; } /** * Returns the language to use when formatting messages, or {@code null} for the * default. The default implementation delegates to {@link ImageReader#getLocale} or * {@link ImageWriter#getLocale} if possible, or returns {@code null} otherwise. * * @return The locale for formatting messages, or {@code null}. */ @Override public Locale getLocale() { if (owner instanceof ImageReader) { return ((ImageReader) owner).getLocale(); } if (owner instanceof ImageWriter) { return ((ImageWriter) owner).getLocale(); } return null; } /** * Returns the resources for formatting error messages. */ private IndexedResourceBundle getErrorResources() { return Errors.getResources(getLocale()); } /** * Returns the level at which warnings are emitted. The default implementation returns * the last value given to the {@link #setWarningLevel(Level)}, or {@link Level#WARNING} * if the level has not been explicitly defined. * * @return The current level at which warnings are emitted. * * @see MetadataNodeParser#getWarningLevel() * * @since 3.07 */ public Level getWarningLevel() { if (warningLevel == null) { warningLevel = Level.WARNING; } return warningLevel; } /** * Sets the warning level. This logging level is given to all {@link MetadataNodeParser} created * by this {@code SpatialMetadata} instance. The default value is {@link Level#WARNING}. * <p> * Note that in the default implementation, warnings are logged only * if this {@code SpatialMetadata} instance is not associated to an * {@linkplain ImageReader image reader} or {@linkplain ImageWriter writer} having * {@linkplain javax.imageio.event.IIOReadWarningListener warning listeners}. * See {@link #warningOccurred(LogRecord)} for more details. * * @param level The new logging level. * * @see MetadataNodeParser#setWarningLevel(Level) * * @since 3.07 */ public void setWarningLevel(final Level level) { ensureNonNull("level", level); warningLevel = level; if (instances != null) MetadataProxy.setWarningLevel(instances.values(), level); if (lists != null) MetadataProxy.setWarningLevel(lists .values(), level); if (dateFormat != null) dateFormat .setLevel(level); if (rangeFormat != null) rangeFormat.setLevel(level); } /** * Invoked when some inconsistency has been detected in the spatial metadata. The default * implementation delegates to the first of the following choices which is applicable: * <p> * <ul> * <li>{@link org.geotoolkit.image.io.SpatialImageReader#warningOccurred(LogRecord)}</li> * <li>{@link org.geotoolkit.image.io.SpatialImageWriter#warningOccurred(LogRecord)}</li> * <li>Send the record to the {@link #LOGGER "org.geotoolkit.image.io"} logger otherwise.</li> * </ul> * <p> * Subclasses can override this method if more processing is wanted, or for * throwing exception if some warnings should be considered as fatal errors. * * @param record The warning record to log. * @return {@code true} if the message has been sent to at least one warning listener, * or {@code false} if it has been sent to the logging system as a fallback. * * @see MetadataNodeParser#warningOccurred(LogRecord) * @see javax.imageio.event.IIOReadWarningListener */ @Override public boolean warningOccurred(final LogRecord record) { return Warnings.log(owner, record); } /** * A {@link LoggedFormat} which use the {@link SpatialMetadata#getLocale reader locale} * for warnings. */ private final class FormatAdapter<T> extends LoggedFormat<T> { private static final long serialVersionUID = -1108933164506428318L; FormatAdapter(final Format format, final Class<T> type) { super(format, type); } @Override protected Locale getWarningLocale() { return getLocale(); } @Override protected void logWarning(final LogRecord warning) { warningOccurred(warning); } } /** * Wraps the specified format in order to either parse fully a string, or log a warning. * * @param <T> The expected type of parsed values. * @param format The format to use for parsing and formatting. * @param type The expected type of parsed values. * @return A format that logs warnings when it can't parse fully a string. */ protected <T> LoggedFormat<T> createLoggedFormat(final Format format, final Class<T> type) { return new FormatAdapter<>(format, type); } /** * Creates a logged format of the given type. * * @param <T> The expected type of parsed values. * @param type The expected type of parsed values. * @param format The method to logs as the "caller" when the parsing fails. * @return A format that logs warnings when it can't parse fully a string. */ private <T> LoggedFormat<T> createLoggedFormat(final Class<T> type, final String caller) { final LoggedFormat<T> format = createLoggedFormat(createFormat(type), type); format.setLogger("org.geotoolkit.image.io.metadata"); format.setCaller(MetadataNodeParser.class, caller); return format; } /** * Creates the format to use for parsing and formatting dates or number ranges. * * {@note We use the Canada locale because it is a bit closer to ISO standards than * the US locale (e.g. order of elements in a date) while using the English * symbols (e.g. the dot as a decimal separator instead than coma).} * * @param type The expected type of parsed values. * @return The format to use. */ private static Format createFormat(final Class<?> type) { if (Date.class.isAssignableFrom(type)) { final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CANADA); format.setTimeZone(TimeZone.getTimeZone("UTC")); return format; } if (NumberRange.class.isAssignableFrom(type)) { final RangeFormat format = new RangeFormat(Locale.CANADA); format.setElementPattern("0.######", false); return format; } throw new IllegalArgumentException(String.valueOf(type)); } /** * Parses the given string as an object of the given type. This method should never be invoked, * except as a (admitly inefficient) fallback when the {@link IIOMetadata} in use is not an * instance of {@code SpatialMetadata} (this usually don't happen). * * @param type The expected type of the parsed value. * @param text The text to parse * @return The parsed value. * @throws ParseException If the parsing failed. */ static <T> T parse(final Class<T> type, final String text) throws ParseException { return type.cast(createFormat(type).parseObject(text)); } /** * Formats the given object as a string. This method should never be invoked, except as a * (admitly inefficient) fallback when the {@link IIOMetadata} in use is not an instance of * {@code SpatialMetadata} (this usually don't happen). * * @param type The expected type of the value. * @param value The value to format. * @return The formatted value. */ static <T> String format(final Class<T> type, final T value) { return createFormat(type).format(value); } /** * Returns a standard date format to be shared by {@link MetadataNodeParser}. * This method creates a new format only when first invoked, and reuses the * existing instance on subsequent invocations. */ final LoggedFormat<Date> dateFormat() { if (dateFormat == null) { dateFormat = createLoggedFormat(Date.class, "getAttributeAsDate"); } return dateFormat; } /** * Returns a standard range format to be shared by {@link MetadataNodeParser}. * This method creates a new format only when first invoked, and reuses the * existing instance on subsequent invocations. */ @SuppressWarnings({"unchecked","rawtypes"}) final LoggedFormat<NumberRange<?>> rangeFormat() { if (rangeFormat == null) { rangeFormat = (LoggedFormat) createLoggedFormat(NumberRange.class, "getAttributeAsRange"); } return rangeFormat; } /** * Resets all the data stored in this object to default values. * All nodes below the root node are discarded. * * @throws IllegalStateException If this metadata is {@linkplain #isReadOnly() read only}. */ @Override public void reset() { if (isReadOnly()) { throw new IllegalStateException(getErrorResources() .getString(Errors.Keys.UnmodifiableMetadata)); } root = null; if (fallback != null) { fallback.reset(); } } /** * Returns a string representation of this metadata, mostly for debugging purpose. * The default implementation formats the metadata as a tree similar to the one * formatted by {@link MetadataNodeParser#toString()}. */ @Override public String toString() { if (format == null) { return "EMPTY"; } return Trees.toString(Trees.xmlToSwing(getAsTree(format.getRootName()))); } }