/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-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.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Collection;
import java.util.Collections;
import java.nio.charset.Charset;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import org.opengis.util.CodeList;
import org.opengis.util.RecordType;
import org.opengis.util.Record;
// We use a lot of different metadata interfaces in this class.
// It is a bit too tedious to declare all of them.
import org.opengis.metadata.*;
import org.opengis.metadata.extent.*;
import org.opengis.metadata.spatial.*;
import org.opengis.metadata.quality.*;
import org.opengis.metadata.lineage.*;
import org.opengis.metadata.content.*;
import org.opengis.metadata.citation.*;
import org.opengis.metadata.constraint.*;
import org.opengis.metadata.acquisition.*;
import org.opengis.metadata.maintenance.*;
import org.opengis.metadata.distribution.*;
import org.opengis.metadata.identification.*;
import org.opengis.metadata.maintenance.Scope;
import org.opengis.parameter.*;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.crs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.operation.*;
import org.opengis.annotation.Obligation;
import org.opengis.coverage.grid.GridCell;
import org.opengis.coverage.grid.GridPoint;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.coverage.grid.RectifiedGrid;
import org.opengis.coverage.grid.GridCoordinates;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.primitive.Point;
import org.opengis.temporal.Duration;
import org.opengis.util.ControlledVocabulary;
import org.opengis.util.InternationalString;
import org.opengis.util.GenericName;
import org.geotoolkit.lang.Builder;
import org.apache.sis.util.Classes;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.iso.Types;
import org.apache.sis.measure.Angle;
import org.apache.sis.metadata.KeyNamePolicy;
import org.apache.sis.metadata.TypeValuePolicy;
import org.apache.sis.metadata.MetadataStandard;
import org.apache.sis.metadata.UnmodifiableMetadataException;
import org.geotoolkit.metadata.ValueRestriction;
import org.geotoolkit.resources.Errors;
import static javax.imageio.metadata.IIOMetadataFormat.*;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.toElementName;
import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.NAME_POLICY;
import static org.geotoolkit.internal.image.io.GridDomainAccessor.ARRAY_ATTRIBUTE_NAME;
/**
* Creates new {@linkplain SpatialMetadataFormat spatial metadata format} instances. This class
* infers the tree structure from metadata objects defined by some {@linkplain MetadataStandard
* metadata standard}, typically ISO 19115-2. New metadata elements are declared by calls to the
* generic {@link #addTree addTree} method, or one of its specialized variants like
* {@link #addTreeForImage(String)}.
* <p>
* This builder is used for creating the default
* <a href="SpatialMetadataFormat.html#default-formats">trees documented here</a>.
* Users can leverage this builder for creating trees based on interfaces from other
* standards.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.20
*
* @since 3.20 (derived from 3.05)
* @module
*/
public class SpatialMetadataFormatBuilder extends Builder<SpatialMetadataFormat> {
/**
* The instance being built.
*/
private final SpatialMetadataFormat metadata;
/**
* Sets to {@code true} after {@link #build()} has been invoked,
* in order to ensure that we do not modify a published metadata.
*/
private boolean done;
/**
* The substitution map, or {@code null} if none. The content is provided by the
* user and id not modified by the {@code addTree} methods (but this is modified
* by the more specialized {@code addTreeForXXX} methods). See the public javadoc
* in {@link #substitutions()} for an explanation of this map content.
*
* @see #substitutions()
*/
private Map<Class<?>,Class<?>> substitutions;
/**
* The attribute types to exclude. On first {@link #addTreeRecursively} invocation, this
* is the set of {@link #substitutions} keys having a null value. On recursive invocations,
* this set is modified in order to avoid infinite recursivity
* (e.g. {@code Identifier.getAuthority().getIdentifiers()}).
* <p>
* New elements in this set are added before entering the {@code addTreeRecursively} method,
* and removed after exiting.
*/
private final Set<Class<?>> excludes = new HashSet<>();
/**
* Set of types that we plan to complete manually later. Special value:
* <p>
* <ul>
* <li>{@code null} for declaring all types as incomplete.</li>
* <li>{@link Collections#emptySet()} for declaring all types as complete.</li>
* </ul>
* <p>
* When computing the node child policy ({@code CHILD_POLICY_CHOICE}, <i>etc.</i>) from
* the child restrictions, the children types enumerated in the {@link #excludes} set will
* be ignored only if the given {@code type} is not incomplete. Not ignoring an "excluded"
* type may lead to relaxed child policy. This is needed when a "excluded" type is actually
* going to be added manually by the caller after the {@code addTree} method call.
*/
private Set<Class<?>> incompletes = Collections.emptySet();
/**
* The name of nodes added in previous iterations. The existing nodes will be added to the new
* parent using the {@link #addChildElement(String, String)} instead than creating the tree
* again. This {@link #addTreeRecursively} method will add new elements in this map, but never
* remove existing elements. The class value is used for checking purpose only.
*/
private final Map<String,Class<?>> existings = new HashMap<>();
/**
* Creates a builder for a metadata format of the given name.
* The metadata format will be an instance of {@link SpatialMetadataFormat}.
*
* @param rootName the name of the root element.
*/
public SpatialMetadataFormatBuilder(final String rootName) {
metadata = new SpatialMetadataFormat(rootName);
}
/**
* Creates a builder for a metadata format of the given type. The given implementation
* class must have an accessible no-argument constructor.
*
* {@note We do not provide a constructor accepting directly an instance, because it would
* provide a way to alter an already published <code>SpatialMetadataFormat</code> instance.}
*
* @param type The type of the metadata format to instantiate.
* @throws IllegalAccessException If the class or its no-argument constructor is not accessible.
* @throws InstantiationException If the instantiation fails.
*/
public SpatialMetadataFormatBuilder(final Class<? extends SpatialMetadataFormat> type)
throws InstantiationException, IllegalAccessException
{
metadata = type.newInstance();
}
/**
* The map of children types to substitute by other types. This map is initially empty.
* Users can add or remove entries in this map before to invoke any {@code addTree} method.
* <p>
* If this map is non empty, then every occurrence of a class in the set of keys is replaced
* by the associated class in the collection of values. The purpose of this map is to:
*
* <ul>
* <li><p>Replace a base class by some specialized subclass. Since {@code IIOMetadata} is
* about grided data (not generic {@code Feature}s), the exact subtype is often known at
* compile time, and we want the additional attributes to be declared unconditionally.
* Example:</p>
*
* <blockquote><pre>substitutions().put({@linkplain RangeDimension}.class, {@linkplain Band}.class);</pre></blockquote></li>
*
* <li><p>Exclude a particular class by setting the replacement to {@code null}. This is used
* for excluding large tree of metadata which may not be applicable. Example:</p>
*
* <blockquote><pre>substitutions().put({@linkplain Objective}.class, null);</pre></blockquote></li>
*
* <li><p>Replace an element class (including the whole tree behind it) by a single attribute.
* This simplification is especially useful for {@code Citation} because they typically appear
* in many different places with the same name ("<cite>citation</cite>"), while Image I/O does
* not allow many elements to have the same name (actually this is not strictly forbidden, but
* the getter methods return information only about the first occurrence of a given name).
* Converting an element to an attribute allow it to appear with the same name under different
* nodes, and can make the tree considerably simpler (at the cost of losing all the sub-tree
* below the converted element). Example:</p>
*
* <blockquote><pre>substitutions().put({@linkplain Citation}.class, String.class);</pre></blockquote></li>
*
* <li><p>Replace a collection by a singleton, by setting the source type to an array and the
* target type to the element of that array. This is useful when a collection seems an overkill
* for the specific case of stream or image metadata. Example:</p>
*
* <blockquote><pre>substitutions().put({@linkplain Identification}[].class, {@linkplain Identification}.class);</pre></blockquote></li>
* </ul>
*
* @return The substitution map (never {@code null}).
*/
public Map<Class<?>,Class<?>> substitutions() {
if (substitutions == null) {
substitutions = new HashMap<>();
}
return substitutions;
}
/**
* Verifies that the metadata instance has not yet been published.
*/
private void ensureModifiable() {
if (done) {
throw new UnmodifiableMetadataException(Errors.format(
Errors.Keys.UnmodifiableObject_1, metadata.getClass()));
}
}
/**
* Adds a new optional element or attribute of the given type as a child of the root. This method
* performs the same work than {@link #addTree(MetadataStandard, Class, String, String, boolean)},
* except that the element is added at the root and the name is inferred from the given type
* for convenience.
*
* @param standard The metadata standard of the element or attribute to be added.
* @param type The type of the element or attribute to be added.
*/
public void addTree(final MetadataStandard standard, final Class<?> type) {
ensureNonNull("standard", standard);
ensureNonNull("type", type);
addTree(standard, type, type.getSimpleName(), metadata.getRootName(), false);
}
/**
* Adds a new element or attribute of the given type and name as a child of the given node.
* <p>
* <ul>
* <li>If the given type is a metadata, then it is
* {@linkplain IIOMetadataFormatImpl#addElement(String,String,int) added as an element}
* and all its children are added recursively.</li>
* <li>Otherwise the type is
* {@linkplain IIOMetadataFormatImpl#addAttribute(String,String,int,boolean,String) added as an attribute}.</li>
* </ul>
*
* {@section Element type}
* This method expects a {@code type} argument, which can be a {@link CodeList} subclass,
* one of the interfaces member of the given metadata {@code standard}, or a simple JSE
* type (boolean, number of {@link String}). Do <strong>not</strong> specify collection
* types, since the type of collection elements can not be inferred easily. To specify
* a multi-occurrence, use the array type instead (e.g. {@code CoordinateSystemAxis[].class}).
*
* {@section Substitution map}
* The {@linkplain #substitutions() substitution map} applies only to children (if any),
* not to the type given directly to this method.
*
* @param standard The metadata standard of the element or attribute to be added.
* @param type The type of the element or attribute to be added (see javadoc).
* @param elementName The name of the element or attribute node to be added.
* @param parentName The name of the parent node to where to add the child.
* @param mandatory {@code true} if the element should be mandatory, or {@code false}
* if optional.
*/
public void addTree(final MetadataStandard standard, Class<?> type,
final String elementName, final String parentName, final boolean mandatory)
{
ensureNonNull("standard", standard);
ensureNonNull("type", type);
ensureNonNull("elementName", elementName);
ensureNonNull("parentName", parentName);
ensureModifiable();
excludes.clear();
if (substitutions != null) {
for (final Map.Entry<Class<?>,Class<?>> entry : substitutions.entrySet()) {
if (entry.getValue() == null) {
excludes.add(entry.getKey());
}
}
}
/*
* If the given type is an arrray, handle as a multi-occurrence (i.e. we will add
* a "Elements" parent node, and declare in that parent a single "Element" child
* which can be repeated many time).
*/
int max = 1;
String identifier = null;
if (type.isArray()) {
type = type.getComponentType();
max = Integer.MAX_VALUE;
try {
identifier = standard.getInterface(type).getSimpleName();
} catch (ClassCastException e) {
// Not an implementation of the expected standard.
// It may be an "ordinary" object from the JDK.
identifier = elementName;
}
}
existings.clear();
addTreeRecursively(standard, type, identifier, elementName, parentName, mandatory ? 1 : 0, max, null);
incompletes = Collections.emptySet(); // Not a public API for now.
}
/**
* Implementation of {@link #addTree}. This method invokes itself recursively.
*
* @param standard
* The metadata standard of the element or attribute to be added. This standard will
* be constant for the whole tree added by this method call (including children).
* @param type
* The type of the element or attribute to be added. May be a {@link CodeList}, an
* interface of the given {@code standard}, or a simple JSE object like a boolean,
* a number or a {@link String}. This type shall <strong>not</strong> be an array
* or a collection. To add a multi-occurrence, specify the <em>element</em> type
* with a {@code maxOccurrence} parameter greater than 1.
* @param identifier
* On first invocation, the simple Java name of the standard interface implemented
* by the given {@code type} (this is usually the UML identifier without the OGC/ISO
* two-letters prefix). On recursive invocations, the UML identifier of the Java method
* for the element to be added. If non-null, this argument is usually the same than
* {@code elementName} but in singular form. If {@code null}, a singular name will be
* derived from the {@code elementName} string if needed.
* @param elementName
* The name of the element or attribute node to be added. This is the name of the
* parent node, except that the first letter may be changed to upper-case. If the
* node accepts multi-occurrence ({@code maxOccurrence > 1}), a compound child node
* will be added with the {@code identifier} name and {@code CHILD_POLICY_REPEAT}.
* @param parentName
* The name of the parent node where to add the child. This is the name given in
* calls to {@link IIOMetadataFormatImpl} methods for specifying where to add the node.
* @param minOccurrence
* Minimal occurrence of the element or attribute in the parent node. If 0, the
* element is considered optional. If different than zero, it is considered
* mandatory.
* @param maxOccurrence
* Maximal occurrence of the element or attribute in the parent node. If greater
* than 1, the node will be added with {@link #CHILD_POLICY_REPEAT}.
* @param restriction
* The restriction on the valid values, or {@code null} if none. This is used for
* determining the minimal and maximal values of attributes, and the child policy.
* @return
* The {@code elementName}, or a modified version of it if that method
* modified the case, or {@code null} if it has not been added.
*/
private String addTreeRecursively(
final MetadataStandard standard,
Class<?> type,
String identifier, // May replace first letter by upper-case.
String elementName, // Replaced by component name on multi-occurrence
String parentName, // Replaced by element name on multi-occurrence
final int minOccurrence,
final int maxOccurrence,
final ValueRestriction restriction)
{
if (maxOccurrence == 0) {
return null;
}
/*
* CodeList ⇒ Attribute VALUE_ENUMERATION
*
* The enums are the code list elements. There is no default value.
*/
if (ControlledVocabulary.class.isAssignableFrom(type)) {
@SuppressWarnings("unchecked")
final Class<ControlledVocabulary> codeType = (Class<ControlledVocabulary>) type;
metadata.addEnumeration(parentName, elementName, (minOccurrence != 0), getCodeList(codeType));
return elementName;
}
/*
* JSE type ⇒ Attribute VALUE_ARBITRARY | VALUE_LIST | VALUE_ENUMERATION
*
* If the element is not an other object from the same metadata standard, handles it as
* an attribute. Everything which can not be handled by one of the DATATYPE_* constants
* is handled as a String.
*/
if (!standard.isMetadata(type)) {
String containerName = elementName;
if (maxOccurrence != 1) {
/*
* Collection ⇒ Attribute VALUE_LIST
*
* In most case, we are adding a list of String or double values. But in a
* few cases we add a list of double[] arrays (e.g. "offsetVectors"), in
* which cases we need to insert a compound element in the tree.
*/
final Class<?> componentType = type.getComponentType();
if (componentType != null) {
// The container for the repeated elements (CHILD_POLICY_REPEAT)
containerName = elementName = toElementName(elementName);
final String componentName = toComponentName(elementName, identifier, true);
metadata.addListWrapper(standard, parentName, elementName, componentName,
componentType, minOccurrence, maxOccurrence);
// The attribute of kind VALUE_LIST.
parentName = componentName;
elementName = ARRAY_ATTRIBUTE_NAME;
type = componentType;
}
metadata.addAttribute(parentName, elementName, typeOf(type), minOccurrence, maxOccurrence, null);
} else {
/*
* Boolean ⇒ Attribute VALUE_ENUMERATION
* Number ⇒ Attribute VALUE_RANGE[_?_INCLUSIVE]
* Object ⇒ Attribute VALUE_ARBITRARY
*/
metadata.addAttribute(parentName, elementName, typeOf(type), minOccurrence,
maxOccurrence, (restriction != null) ? restriction.range : null);
}
return containerName;
}
/*
* Collection of Metadata ⇒ Element CHILD_POLICY_REPEAT
*
* The 'elementName' is inferred from the method name and is typically in plural
* form (at least in GeoAPI interfaces). We add a node for 'elementName', which
* can contain many occurrences of the actual metadata structure. The new node is
* set as the parent of the actual metadata structure. The name of that metadata
* structure is set to the UML identifier, which is typically the same name than
* 'elementName' except that it is in singular form.
*/
elementName = toElementName(elementName);
final String containerName = elementName;
if (maxOccurrence != 1) {
final Class<?> existingType = existings.get(elementName);
if (existingType != null) {
if (!existingType.equals(type)) {
throw new IllegalArgumentException(elementName); // TODO: better error message
}
metadata.addExistingElement(elementName, parentName);
return null;
}
metadata.addElement(standard, null, elementName, parentName, CHILD_POLICY_REPEAT, minOccurrence, maxOccurrence);
existings.put(elementName, type);
parentName = elementName;
identifier = toElementName(identifier);
elementName = toComponentName(elementName, identifier, false);
}
/*
* Metadata singleton ⇒ Element CHILD_POLICY_SOME|ALL|CHOICE|EMPTY
*
* If every childs have the same obligation,
* then we will apply the following mapping:
*
* MANDATORY ⇒ CHILD_POLICY_ALL
* CONDITIONAL ⇒ CHILD_POLICY_CHOICE (this is assuming that XOR is the condition)
* FORBIDDEN ⇒ CHILD_POLICY_EMPTY
*
* Otherwise the policy is CHILD_POLICY_SOME.
*/
boolean hasChilds = false;
Obligation obligation = Obligation.FORBIDDEN; // If there is no child.
final Map<String,String> methods, identifiers;
final Map<String, ExtendedElementInformation> restrictions;
final Map<String,Class<?>> propertyTypes, elementTypes;
methods = standard.asNameMap (type, NAME_POLICY, KeyNamePolicy. METHOD_NAME);
identifiers = standard.asNameMap (type, NAME_POLICY, KeyNamePolicy. UML_IDENTIFIER);
propertyTypes = standard.asTypeMap (type, NAME_POLICY, TypeValuePolicy.PROPERTY_TYPE);
elementTypes = standard.asTypeMap (type, NAME_POLICY, TypeValuePolicy.ELEMENT_TYPE);
restrictions = standard.asInformationMap(type, NAME_POLICY);
final boolean isComplete = (incompletes != null) && !incompletes.contains(type);
for (final Map.Entry<String,Class<?>> entry : elementTypes.entrySet()) {
final Class<?> candidate = entry.getValue();
if (isComplete && excludes.contains(candidate)) {
// If the caller does not plan to complete manually the tree after this method,
// then the excluded types should not be considered when determining the child
// policy. Note that a null 'incomplete' map means that every types are incomplete.
continue;
}
if (standard.isMetadata(candidate) && !ControlledVocabulary.class.isAssignableFrom(candidate)) {
final ValueRestriction vr = ValueRestriction.create(restrictions.get(entry.getKey()));
if (vr != null) {
final Obligation c = vr.obligation;
if (c != null) {
if (!hasChilds) {
hasChilds = true;
obligation = c;
continue;
}
if (c == obligation) {
continue;
}
}
}
// Found an obligation which is unknown or different than the previous ones.
obligation = null;
hasChilds = true;
break;
}
}
if (obligation == null) {
// The obligation is not the same for every child.
obligation = Obligation.OPTIONAL;
}
final int childPolicy;
switch (obligation) {
case MANDATORY: childPolicy = CHILD_POLICY_ALL; break;
case CONDITIONAL: childPolicy = CHILD_POLICY_CHOICE; break;
case FORBIDDEN: childPolicy = CHILD_POLICY_EMPTY; break;
default: childPolicy = CHILD_POLICY_SOME; break;
}
/*
* At this point we have determined the child policy to apply to the new node.
* Now add the child elements. The loop below invokes this method recursively
* for each attribute of the metadata object that we are adding.
*/
final Class<?> existingType = existings.get(elementName);
if (existingType != null) {
if (!existingType.equals(type)) {
throw new IllegalArgumentException(elementName); // TODO: better error message
}
metadata.addExistingElement(elementName, parentName);
return null;
}
metadata.addElement(standard, type, elementName, parentName, childPolicy, 0, 1);
for (final Map.Entry<String,Class<?>> entry : propertyTypes.entrySet()) {
String childName = entry.getKey();
final ValueRestriction vr = ValueRestriction.create(restrictions.get(childName));
int min = 0, max = 1;
if (vr != null && vr.obligation != null) {
switch (vr.obligation) {
case MANDATORY: min = 1; break;
case FORBIDDEN: max = 0; break;
}
}
Class<?> childType = entry.getValue();
if (Collection.class.isAssignableFrom(childType)) {
// Replace the collection type by the type of elements in that collection.
childType = elementTypes.get(childName);
if (childType == null) {
/*
* We have been unable to find the element type.
* Silently ignore.
*/
continue;
}
max = Integer.MAX_VALUE;
}
/*
* If the caller specified a substitution map, then we perform two checks:
*
* 1) If we have a collection (max > 1), then check if the caller wants to
* replace the collection (identified by an array type) by a singleton.
*
* 2) Then check if we want to replace the element type by an other element
* type. It could be a new array type.
*/
if (substitutions != null) {
Class<?> replacement = null;
if (max > 1) { // Collection case.
replacement = substitutions.get(Classes.changeArrayDimension(childType, 1));
if (replacement != null) {
childType = replacement; // Typically, the replacement type is the same.
childName = identifiers.get(childName); // Replace plural by singular form.
max = 1;
}
}
replacement = substitutions.get(childType);
if (replacement != null) {
childType = replacement;
replacement = childType.getComponentType();
if (replacement != null) {
max = Integer.MAX_VALUE;
childType = replacement;
}
}
}
/*
* We now have all the properties for the child that we want to add. Invoke this method
* recursively for proceding to the addition, with guard against infinite recursivity.
*/
if (excludes.add(childType)) {
childName = addTreeRecursively(standard, childType, identifiers.get(childName),
childName, elementName, min, max, vr);
if (!excludes.remove(childType)) {
throw new AssertionError(childType);
}
if (childName != null) {
metadata.mapName(elementName, methods.get(entry.getKey()), childName);
}
}
}
return containerName;
}
/**
* Returns one of the {@code DATATYPE_*} constant for the given class. If no constant
* matches, then returns {@code DATATYPE_STRING} on the assumption that all attributes
* have a sensible {@link Object#toString()} implementation.
*
* @param type The class for which the {@code DATATYPE_*} constant is desired.
* @return The {@code DATATYPE_*} constant for the given class.
*/
private static int typeOf(Class<?> type) {
type = Numbers.primitiveToWrapper(type);
if (Number.class.isAssignableFrom(type)) {
if (Numbers.isInteger(type)) {
return DATATYPE_INTEGER;
}
if (Float.class.isAssignableFrom(type)) {
return DATATYPE_FLOAT;
}
return DATATYPE_DOUBLE;
}
if (Boolean.class.isAssignableFrom(type)) {
return DATATYPE_BOOLEAN;
}
if (Angle.class.isAssignableFrom(type)) {
return DATATYPE_DOUBLE;
}
return DATATYPE_STRING;
}
/**
* Returns the list of UML identifiers for the given code list type.
* If a code has no UML identifier, then the programmatic name is used as a fallback.
*
* @param codeType The type of code list.
* @return The list of UML identifiers or programmatic names for the given
* code list, or an empty array if none.
*
* @since 3.03
*/
private static String[] identifiers(final Class<? extends ControlledVocabulary> codeType) {
final ControlledVocabulary[] codes = Types.getCodeValues(codeType);
final String[] ids = new String[codes.length];
for (int i=0; i<codes.length; i++) {
ids[i] = Types.getCodeName(codes[i]);
}
return ids;
}
/**
* Returns the code list identifiers, with some changes for code inherited from
* legacy specifications.
*/
private static String[] getCodeList(final Class<? extends ControlledVocabulary> codeType) {
String[] identifiers = identifiers(codeType);
if (codeType == AxisDirection.class) {
for (int i=0; i<identifiers.length; i++) {
// Replace "CS_AxisOrientationEnum.CS_AO_Other" by something more readable.
if (identifiers[i].endsWith("Other")) {
identifiers[i] = "other";
}
}
}
return identifiers;
}
/**
* Returns the name of an entry in a collection.
*
* @param elementName The Java-Beans name of the collection. This is usually plural.
* @param identifier The UML identifier of the same element than above.
* This is usually singular. It may be {@code null}
* @param attribute {@code true} if the {@code elementName} is actually for an attribute.
* @return The name of an entry in the collection.
*/
private static String toComponentName(final String elementName, final String identifier,
final boolean attribute)
{
if (identifier != null && !identifier.equalsIgnoreCase(elementName)) {
return identifier;
}
if (attribute && elementName.endsWith("s")) {
/*
* Try to make singular assuming an English speeling (we are already making the same
* assumption when adding the "Entry" suffix below). We do that only for attributes,
* not for elements, because elements may be complex structures in which the plural
* form is intentional.
*
* Examples:
* - "DescriptiveKeywords" is an element with "Keywords" (and others) attributes.
* We don't want to make it singular, because it can contains many keywords.
* - "offsetVectors" is an attribute of type List<double[]>, which is converted
* by this class as an "offserVectors" element with "offsetVector" childs.
*/
return elementName.substring(0, elementName.length() - 1);
}
// This is used only as a fallback.
return (identifier != null ? identifier : elementName) + "Entry";
}
/**
* Adds the tree structure for an ISO-19115 metadata object.
* <b>Warning:</b> this tree is big and is supported only for a few plugins like
* {@link org.geotoolkit.image.io.plugin.NetcdfImageReader}.
*
* @param addToElement The name of the element where to add the tree,
* or {@code null} for adding the tree at the root.
*
* @see SpatialMetadataFormat#ISO_FORMAT_NAME
*
* @since 3.20
*/
protected void addTreeForISO19115(String addToElement) {
ensureModifiable();
if (addToElement == null) {
addToElement = metadata.getRootName();
}
final Map<Class<?>,Class<?>> substitutions = substitutions();
// TODO: need to rename the nodes below.
substitutions.put(ProcessStepReport.class, null); // "Reports" name clash with quality.Element
substitutions.put(Source.class, null); // "Sources" name clash with ResponsibleParty.
substitutions.put(Responsibility.class, null);
substitutions.put(Party.class, null);
addTree(MetadataStandard.ISO_19115, Metadata.class, "Metadata", addToElement, false);
}
/**
* Adds the tree structure for <cite>stream</cite> metadata. The default implementation
* adds the tree structure documented in the "<cite>Stream metadata</cite>" column of the
* <code><a href="SpatialMetadataFormat.html#default-formats">SpatialMetadataFormat</a></code>
* javadoc.
*
* @param addToElement The name of the element where to add the tree,
* or {@code null} for adding the tree at the root.
*
* @see SpatialMetadataFormat#getStreamInstance(String)
*/
public void addTreeForStream(String addToElement) {
ensureModifiable();
if (addToElement == null) {
addToElement = metadata.getRootName();
}
final Map<Class<?>,Class<?>> substitutions = substitutions();
/*
* Metadata excluded because they are redundant with standard API.
*/
substitutions.put(Format.class, null); // Redundant with ImageReaderWriterSpi.
substitutions.put(Locale.class, null); // Specified in ImageReader.getLocale().
substitutions.put(Charset.class, null); // Fixed to Unicode in java.lang.String.
substitutions.put(BrowseGraphic.class, null); // Redundant with Image I/O Thumbnails.
substitutions.put(SpatialRepresentationType.class, null); // Fixed to "grid" for Image I/O.
/*
* Metadata excluded because we are not interested in (at this time). Their
* inclusion introduce large sub-trees that would need to be simplified. We
* may revisit some of those exclusion in a future version, when we get more
* experience about what are needed.
*/
substitutions.put(Usage.class, null); // MD_DataIdentification.resourceSpecificUsage
substitutions.put(Responsibility.class, null); // MD_DataIdentification.pointOfContact
substitutions.put(ResponsibleParty.class, null);
substitutions.put(Constraints.class, null); // MD_DataIdentification.resourceConstraints
substitutions.put(MaintenanceInformation.class, null); // MD_DataIdentification.resourceMaintenance
substitutions.put(AssociatedResource.class, null); // MD_DataIdentification.associatedResource
substitutions.put(AggregateInformation.class, null); // MD_DataIdentification.aggregationInfo
substitutions.put(Plan.class, null); // MI_AcquisitionInformation.acquisitionPlan
substitutions.put(Objective.class, null); // MI_AcquisitionInformation.objective
substitutions.put(Operation.class, null); // MI_AcquisitionInformation.operation
substitutions.put(Requirement.class, null); // MI_AcquisitionInformation.acquisitionRequirement
substitutions.put(Scope.class, null); // DQ_DataQuality.scope
substitutions.put(Lineage.class, null); // DQ_DataQuality.lineage
substitutions.put(Result.class, null); // DQ_DataQuality.report.result
/*
* Metadata excluded because not yet implemented.
*/
substitutions.put(Duration.class, null); // MD_DataIdentification.temporalResolution
substitutions.put(TemporalExtent.class, null);
/*
* Metadata simplification, where elements are replaced by attributes. The simplification
* is especially important for Citation because they appear in many different places with
* the same name ("citation"), while Image I/O does not allow many element nodes to have
* the same name (this is not strictly forbidden, but the getter methods return information
* only about the first occurrence of the given name. Note however that having the same name
* under different element node is not an issue for attributes). In addition, the Citation
* sub-tree is very large and we don't want to allow the tree to growth that big.
*/
substitutions.put(Citation.class, String.class);
substitutions.put(Citation[].class, String.class);
substitutions.put(Identifier.class, String.class);
/*
* Metadata excluded because they introduce circularity or because
* they appear more than once (we shall not declare two nodes with
* the same name in Image I/O). Some will be added by hand later.
*/
substitutions.put(Instrument.class, null); // MI_AcquisitionInformation.instrument
/*
* Collections replaced by singletons, because only one
* instance is enough for the purpose of stream metadata.
*/
substitutions.put(Extent[].class, Extent.class); // MD_DataIdentification.extent
substitutions.put(GeographicExtent[].class, GeographicExtent.class); // MD_DataIdentification.extent.geographicElement
substitutions.put(VerticalExtent[].class, VerticalExtent.class); // MD_DataIdentification.extent.verticalElement
substitutions.put(Resolution[].class, Resolution.class); // MD_DataIdentification.spatialResolution
substitutions.put(Platform[].class, Platform.class); // MI_AcquisitionInformation.platform
substitutions.put(Element[].class, Element.class); // DQ_DataQuality.report
substitutions.put(Date[].class, Date.class); // DQ_DataQuality.report.dateTime
/*
* Since this set of metadata is about gridded data,
* replace the generic interfaces by specialized ones.
*/
substitutions.put(Identification.class, DataIdentification.class);
substitutions.put(SpatialRepresentation.class, GridSpatialRepresentation.class);
substitutions.put(GeographicExtent.class, GeographicBoundingBox.class);
/*
* Build the tree.
*/
final MetadataStandard standard = MetadataStandard.ISO_19115;
addTree(standard, DataIdentification.class, "DiscoveryMetadata", addToElement, false);
addTree(standard, AcquisitionInformation.class, "AcquisitionMetadata", addToElement, false);
addTree(standard, DataQuality.class, "QualityMetadata", addToElement, false);
metadata.removeAttribute("EquivalentScale", "doubleValue");
/*
* Add by hand a node in the place where it would have been added if we didn't
* excluded it. We do this addition because Instruments appear in two places,
* while we want only the occurrence that appear under the "Platform" node.
*/
substitutions.put(Platform.class, null);
substitutions.remove(Identifier.class); // Allow full expansion.
addTree(standard, Instrument[].class, "Instruments", "Platform", false);
metadata.mapName("Instruments", "getCitations", "citation");
}
/**
* Adds the tree structure for <cite>image</cite> metadata. The default implementation
* adds the tree structure documented in the "<cite>Image metadata</cite>" column of the
* <a href="SpatialMetadataFormat.html#default-formats">class javadoc</a>.
* <p>
* The <cite>Coordinate Reference System</cite> branch is not included by this method.
* For including CRS information, the {@link #addTreeForCRS(String)} method shall be
* invoked explicitly.
*
* @param addToElement The name of the element where to add the tree,
* or {@code null} for adding the tree at the root.
*
* @see SpatialMetadataFormat#getImageInstance(String)
*/
public void addTreeForImage(String addToElement) {
ensureModifiable();
if (addToElement == null) {
addToElement = metadata.getRootName();
}
final Map<Class<?>,Class<?>> substitutions = substitutions();
substitutions.put(Citation.class, String.class); // MD_ImageDescription.xxxCode
substitutions.put(RecordType.class, null); // MD_CoverageDescription.attributeDescription
substitutions.put(RangeDimension.class, Band.class); // MD_CoverageDescription.dimension
substitutions.put(AttributeGroup.class, null); // MD_CoverageDescription.attributeGroup
/*
* Adds the "ImageDescription" node derived from ISO 19115.
* The 'fillSampleValues' attribute is a Geotk extension.
*/
MetadataStandard standard = MetadataStandard.ISO_19115;
addTree(standard, ImageDescription.class, "ImageDescription", addToElement, false);
metadata.addEnumeration("ImageDescription", "contentType", false, "image", "thematicClassification", "physicalMeasurement");
metadata.addElement(standard, Band.class, "Dimensions", "ImageDescription", CHILD_POLICY_REPEAT, 0, Integer.MAX_VALUE);
substitutions.put(Record.class, null); // MD_SampleDimension.otherProperty
substitutions.put(Identifier.class, null); // MD_RangeDimension.names
substitutions.put(InternationalString.class, null); // MD_RangeDimension.description
addTree(standard, Band.class, "Dimension", "Dimensions", false);
substitutions.remove(InternationalString.class);
substitutions.remove(Identifier.class);
substitutions.remove(Record.class);
metadata.addAttribute("Dimension", "validSampleValues", DATATYPE_STRING, 0, 1, null);
metadata.addAttribute("Dimension", "fillSampleValues", DATATYPE_DOUBLE, 0, Integer.MAX_VALUE, null);
metadata.addAttribute("Dimension", "descriptor", DATATYPE_STRING, 0, 1, null);
metadata.addObjectValue("Dimension", SampleDimension.class); // Replace Band.class.
/*
* Adds the "SpatialRepresentation" node derived from ISO 19115.
* We omit the information about spatial-temporal axis properties (the Dimension object)
* because it is redundant with the information provided in the CRS and offset vectors.
*/
substitutions.put(Dimension.class, null); // GridSpatialRepresentation.axisDimensionProperties
substitutions.put(Point.class, double[].class); // MD_Georectified.centerPoint
substitutions.put(GCP.class, null); // MD_Georectified.checkPoint
substitutions.put(Boolean.TYPE, null); // MD_Georectified.checkPointAvailability
substitutions.put(InternationalString.class, null); // MD_Georectified.various descriptions...
addTree(standard, Georectified.class, "SpatialRepresentation", addToElement, false);
metadata.removeAttribute("SpatialRepresentation", "cornerPoints");
/*
* Adds the "RectifiedGridDomain" node derived from ISO 19123.
*/
substitutions.put(String.class, null); // CV_Grid.axisNames
substitutions.put(GridCell.class, null); // CV_Grid.cell
substitutions.put(GridPoint.class, null); // CV_Grid.intersection
substitutions.put(GridEnvelope.class, null); // CV_Grid.extent (will be added later)
substitutions.put(GridCoordinates.class, int[].class); // CV_GridEnvelope.low/high
substitutions.put(DirectPosition.class, double[].class); // CV_RectifiedGrid.origin
substitutions.put(CoordinateReferenceSystem.class, null);
standard = MetadataStandard.ISO_19123;
incompletes = null; // Will consider every RectifiedGridDomain nodes as incomplete.
addTree(standard, RectifiedGrid.class, "RectifiedGridDomain", addToElement, false);
/*
* Following is part of ISO 19123 and "GML in JPEG 2000" specifications,
* but under different names. We use the "GML in JPEG 2000" names.
*/
addTree(standard, GridEnvelope.class, "Limits", "RectifiedGridDomain", false);
metadata.removeAttribute("Limits", "dimension"); // Geotk extension not in ISO 19123.
metadata.removeAttribute("RectifiedGridDomain", "dimension"); // Redundant with the one in SpatialRepresentation.
/*
* There is no public API for this functionality at this time...
*/
metadata.mapName("RectifiedGridDomain", "getExtent", "Limits");
}
/**
* Adds the tree structure for a <cite>Coordinate Reference System</cite> object.
*
* @param addToElement The name of the element where to add the tree,
* or {@code null} for adding the tree at the root.
*/
protected void addTreeForCRS(String addToElement) {
ensureModifiable();
if (addToElement == null) {
addToElement = metadata.getRootName();
}
final Map<Class<?>,Class<?>> substitutions = substitutions();
/*
* Metadata excluded in order to keep the CRS node relatively simple.
*/
substitutions.put(Identifier.class, null); // IO_IdentifiedObject.identifiers
substitutions.put(GenericName.class, null); // IO_IdentifiedObject.alias
substitutions.put(String.class, null); // IO_IdentifiedObject.toWKT
substitutions.put(Extent.class, null); // RS_ReferenceSystem.domainOfValidity
substitutions.put(InternationalString.class, null); // SC_CRS.scope
substitutions.put(Date.class, null); // CD_Datum.realizationEpoch
substitutions.put(Boolean.TYPE, null); // CD_Ellipsoid.isIvfDefinitive
/*
* Assume that the CRS will be geodetic CRS.
* After the tree has been added, we will generalize the declared types.
*/
substitutions.put(Datum.class, GeodeticDatum.class);
MetadataStandard standard = MetadataStandard.ISO_19111;
incompletes = new HashSet<>(4);
incompletes.add(CoordinateReferenceSystem.class);
incompletes.add(CoordinateSystem.class);
incompletes.add(GeodeticDatum.class);
addTree(standard, SingleCRS.class, "CoordinateReferenceSystem", addToElement, false);
metadata.addObjectValue("CoordinateReferenceSystem", CoordinateReferenceSystem.class);
metadata.addObjectValue("Datum", Datum.class);
/*
* We need to add the axes explicitly, because the method signature is
* CoordinateSystem.getAxis(int) which is not recognized by our reflection API.
*/
addTree(standard, CoordinateSystemAxis[].class, "Axes", "CoordinateSystem", true);
/*
* Add conversion parameters. Note that the operation method will be replaced by a String
* later. We can not replace it by a String now since Strings are excluded because of the
* toWKT() method.
*/
substitutions.put(MathTransform.class, null);
substitutions.put(OperationMethod.class, null);
substitutions.put(PositionalAccuracy.class, null);
substitutions.put(CoordinateReferenceSystem.class, null);
substitutions.put(ParameterValueGroup.class, null);
incompletes = null; // Will consider every Conversion nodes as incomplete.
addTree(standard, Conversion.class, "Conversion", "CoordinateReferenceSystem", false);
metadata.addAttribute("Conversion", "method", DATATYPE_STRING, 1, 1, null);
/*
* Adds the parameter manually, because they are not in the ISO 19111 package
* (so the MetadataStandard.ISO_19111 doesn't known them) and because we want
* a simple (name, value) pair, instead than the parameter descriptor.
*/
metadata.addElement(null, ParameterValueGroup.class, "Parameters", "Conversion", CHILD_POLICY_REPEAT, 0, Integer.MAX_VALUE);
metadata.addElement(null, ParameterValue.class, "ParameterValue", "Parameters", CHILD_POLICY_EMPTY, 0, 0);
metadata.addAttribute("ParameterValue", "name", DATATYPE_STRING, 1, 1, null);
metadata.addAttribute("ParameterValue", "value", DATATYPE_DOUBLE, 1, 1, null);
}
/**
* Returns the metadata format instance which has been build.
* This builder can not be used anymore after this method has been invoked.
*
* @return The metadata format instance.
*/
@Override
public SpatialMetadataFormat build() {
done = true;
return metadata;
}
}