/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.constellation.json.metadata;
import java.util.Date;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
import java.util.logging.Logger;
import org.apache.sis.internal.jaxb.metadata.replace.ReferenceSystemMetadata;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.Metadata;
import org.opengis.temporal.Instant;
import org.opengis.temporal.Period;
import org.opengis.temporal.TemporalPrimitive;
import org.opengis.referencing.ReferenceSystem;
import org.apache.sis.metadata.MetadataStandard;
import org.apache.sis.metadata.KeyNamePolicy;
import org.apache.sis.metadata.TypeValuePolicy;
import org.apache.sis.metadata.ValueExistencePolicy;
import org.apache.sis.util.logging.Logging;
/**
* Creates the tree of value to writes as a JSON file.
*
* <h3>Design problem:</h3>
* Templates may have two or more nodes with the same path. For example we may have:
*
* <blockquote><pre>
* {
* "block":{
* "multiplicity":1,
* "path":null
* "children":[{
* "field":{
* "multiplicity":1,
* "path":"identificationInfo.descriptiveKeywords.keyword"
* }
* },{
* ...etc...
* },{
* "field":{
* "multiplicity":1,
* "path":"identificationInfo.topicCategory"
* }
* }
* ]}
* },{
* "block":{
* "multiplicity":60,
* "path":"identificationInfo.descriptiveKeywords",
* "children":[{
* "field":{
* "multiplicity":60,
* "path":"identificationInfo.descriptiveKeywords.keyword"
* }
* },{
* ...etc...
* }
* ]}
* }
* </pre></blockquote>
*
* In the above example, the same path ("identificationInfo.descriptiveKeywords.keyword") is repeated twice.
* The first block will show only the first occurrence (because of "multiplicity":1) while the second block
* will show all occurrences. It may be desirable to omit from the second block all elements already shown
* in the first block. However this objective raises some tricky issues:
*
* 1) Shall we omit only the first "keyword", or the first "descriptiveKeywords" (thus loosing any keywords
* after the first one in the first "descriptiveKeywords"), or the first "identificationInfo" instance?
* Omitting only the first "keyword" would probably be confusing for the user. Omitting the first block
* "descriptiveKeyword" may be closer to our intend, but there is nothing in the above template telling
* us that. This is because the first block contains an element ("topicCategory") which is normally not
* part of descriptive keywords, so that block is a mix of information from different places.
*
* 2) Omitting elements requires that we take trace of remaining elements after we have show some of them.
* We can do that with the TemplateApplication.remainingValues hash map. This map requires distinct keys
* for the same path applied on different instances of a metadata value. For example the two following
* paths are distinct:
*
* - identificationInfo[0].descriptiveKeywords[0].keyword
* - identificationInfo[0].descriptiveKeywords[1].keyword
*
* This is handled by the NumerotedPath class. When an element has been shown, we need to check if the
* parent element became empty. This can be handled by a 'prune' operation applied after we created the
* tree (in comparison, in the simpler version it is possible to prune on-the-fly, without a need for a
* post-operation). Consequently handling of such special cases make the code much more complex.
*
*
* For avoiding the complexity of the above, we currently do not try to auto-detect the properties to omit.
* instead, we provide an explicit {@link TemplateNode#ignore} attribute. However this is not a satisfying
* solution.
*
* @author Martin Desruisseaux (Geomatys)
*/
final class TemplateApplicator {
public static final Logger LOGGER = Logging.getLogger("org.constellation.json.metadata");
/**
* A path to be handled in a special way.
*/
private static final String[] REFERENCE_SYSTEM_CODE = {"referenceSystemIdentifier", "code"};
private static final String[] REFERENCE_SYSTEM_CODESPACE = {"referenceSystemIdentifier", "codeSpace"};
private static final String[] REFERENCE_SYSTEM_VERSION = {"referenceSystemIdentifier", "version"};
/**
* {@code true} for omitting empty nodes.
*/
private final boolean prune;
/**
* The 1-based index of each elements in the path, or 0 for values that are not collections.
* Those indices will be incremented as we iterate in the metadata tree.
*/
private final int[] indices;
/**
* A temporary list used when building a list of nodes for all values at a path.
*/
private final List<ValueNode> nodes;
/**
* Creates a new writer.
*
* @param prune {@code true} for omitting empty nodes.
* @param maxDepth The maximal length of {@link TemplateNode#path}.
*/
TemplateApplicator(final boolean prune, final int maxDepth) {
this.prune = prune;
this.indices = new int[maxDepth];
this.nodes = new ArrayList<>();
}
/**
* Builds a tree of values for the given node and all its children nodes.
*
* @param metadata The metadata from which to get the values.
* @return The roots of tree nodes created by this method (not necessarily the root of the whole tree to be
* written), or {@code null} if none. The array may contain null elements, which shall be ignored.
*/
final ValueNode[] createValueTree(final TemplateNode template, final Object metadata) throws ParseException {
return createValueTree(template, null, metadata, 0);
}
/**
* Builds a tree of values for the given node and all its children nodes.
* The given {@code metadata} argument shall be one of the following:
*
* <ul>
* <li>The metadata instance.</li>
* <li>If the metadata instance is unknown, then the base {@link Class} of expected metadata instances.
* There is not risk of confusion between {@code Class} instances and metadata instances because
* {@code Class} can not implement a GeoAPI interface.</li>
* <li>If even the base {@code Class} is unknown, then {@code null}.</li>
* </ul>
*
* <p>This method invokes itself recursively.</p>
*
* @param sibling The node just before this one, or {@code null} if none.
* @param metadata The metadata from which to get the values, or the expected base {@code Class}, or {@code null}.
* @param pathOffset Index of the first {@link #path} element to use.
* @return The roots of tree nodes created by this method (not necessarily the root of the whole tree to be
* written), or {@code null} if none. The array may contain null elements, which shall be ignored.
*/
private ValueNode[] createValueTree(final TemplateNode template, ValueNode sibling,
final Object metadata, int pathOffset) throws ParseException
{
if (template.path == null) {
/*
* If this node does not declare any path, we can not get a metadata value for this node.
* However maybe some chidren have a path allowing them to fetch metadata values.
*/
sibling = null;
ValueNode node = null;
for (final TemplateNode child : template.children) {
final ValueNode[] tree = createValueTree(child, sibling, metadata, pathOffset);
node = addTo(template, node, tree);
sibling = last(tree);
}
return (node != null) ? new ValueNode[] {node} : null;
}
/*
* If this node declares a path, then get the values for this node. The values may be other
* metadata objects, in which case we will need to invoke this method recursively for them.
*/
nodes.clear();
final boolean isMetadataInstance = (metadata != null) && !(metadata instanceof Class<?>);
if (isMetadataInstance) {
try {
getValues(template, metadata, pathOffset);
} catch (ClassCastException e) {
final StringBuilder buffer = new StringBuilder("Illegal path: ");
template.appendPath(pathOffset, buffer);
throw new ParseException(buffer.append('.').toString(), e);
}
filterIgnoredValues(template);
}
/*
* If there is no value and the user asked us to prune empty nodes,
* returns 'null' immediately (avoid the creation of an array).
*/
if (nodes.isEmpty()) {
if (prune) {
return null;
}
/*
* If there is no value, write anyway if the user asked us to not prune empty nodes.
* We will format the default values, which may be {@code null}. But before to create
* new node for default values, we will need to add the "[1]" indice in the JSON path
* when the expected type is a collection.
*/
final int incrementFrom;
final int pathEnd = template.path.length;
if (sibling != null && Arrays.equals(template.path, sibling.template.path)) {
System.arraycopy(sibling.indices, pathOffset, indices, pathOffset, pathEnd);
incrementFrom = pathEnd - 1; // 'do … while' below will increment only the last indice.
} else {
Arrays.fill(indices, pathOffset, pathEnd, 0); // Initialize to "not a collection".
incrementFrom = pathOffset;
}
Class<?> type = null;
if (metadata != null) {
type = isMetadataInstance ? metadata.getClass() : (Class<?>) metadata; // See method javadoc.
do {
if (pathOffset >= template.path.length) {
LOGGER.severe(Arrays.toString(template.path)); // debug
}
final String identifier = template.path[pathOffset];
final Class<?> propertyType;
try {
propertyType = getType(template, type, TypeValuePolicy.PROPERTY_TYPE, identifier);
} catch (ClassCastException e) {
break; // Same than for unknown properties (see following block).
}
if (propertyType == null) {
/*
* May happen with non-standard or unsupported properties. We can not continue further.
* The 'Arrays.fill(…)' above had set remaining indices to 0, which means that we treat
* unknown properties as singletons.
*/
break;
}
if (Collection.class.isAssignableFrom(propertyType)) {
if (pathOffset >= incrementFrom) {
indices[pathOffset]++;
}
type = getType(template, type, TypeValuePolicy.ELEMENT_TYPE, identifier);
} else {
type = propertyType; // Should be equivalent to a call to 'getType(…)', but much cheaper.
}
} while (++pathOffset < pathEnd);
}
if (template.isField()) {
nodes.add(new ValueNode(template, indices, template.defaultValue));
// To be formatted below like ordinary values.
} else {
sibling = null;
ValueNode node = null;
for (final TemplateNode child : template.children) {
final ValueNode[] tree = createValueTree(child, sibling, type, pathEnd);
node = addTo(template, node, tree);
sibling = last(tree);
}
return new ValueNode[] {node}; // Unlikely to be null, but still allowed.
}
}
/*
* If we have a value and this node is a field, returns the value.
* Otherwise delegate to the child nodes for creating sub-trees.
* Note that we need to copy the nodes in an array before to invoke
* the 'createValueTree' method recursively.
*/
final ValueNode[] na = nodes.toArray(new ValueNode[nodes.size()]);
if (!template.isField()) {
pathOffset = template.path.length;
for (int i=0; i<na.length; i++) {
sibling = null;
ValueNode node = na[i];
System.arraycopy(node.indices, 0, indices, 0, node.indices.length);
for (final TemplateNode child : template.children) {
final ValueNode[] tree = createValueTree(child, sibling, node.value, pathOffset);
node = addTo(template, node, tree);
sibling = last(tree);
}
na[i] = node; // As a matter of principle, but the reference should be the same.
}
}
return na;
}
/**
* Returns the last element of the given tree, or {@code null} if none.
*/
private static ValueNode last(final ValueNode[] tree) {
if (tree != null) {
for (int i=tree.length; --i>=0;) {
final ValueNode node = tree[i];
if (node != null) return node;
}
}
return null;
}
/**
* Fetches all occurrences of metadata values at the path given by {@link TemplateNode#path}.
* This method searches only the metadata values for the given {@code TemplateNode} - it does
* not perform any search for children {@code TemplateNode}s.
* The values are added to the {@link #nodes} list.
*
* <p>This method invokes itself recursively.</p>
*
* @param metadata The metadata from where to get the values.
* @param pathOffset Index of the first {@code path} element to use.
* @throws ClassCastException if {@code metadata} is not an instance of the expected standard.
*/
private void getValues(final TemplateNode template, Object metadata, int pathOffset) throws ParseException, ClassCastException {
Objects.requireNonNull(template.path);
Objects.requireNonNull(metadata);
Object value;
do {
if (pathOffset >= template.path.length) {
final StringBuilder paths = new StringBuilder();
for (String p : template.path) {
paths.append('\n').append(p);
}
throw new ParseException("Path offset out of band :" + paths);
}
final String identifier = template.path[pathOffset];
if ((identifier.equals("referenceSystemInfo") || identifier.equals("referenceSystemIdentifier")) && template.endsWith(REFERENCE_SYSTEM_CODE) && (metadata instanceof Metadata || metadata instanceof ReferenceSystem)) {
value = referenceSystemCode(metadata); // Special case.
pathOffset = template.path.length;
} else if ((identifier.equals("referenceSystemInfo") || identifier.equals("referenceSystemIdentifier")) && template.endsWith(REFERENCE_SYSTEM_CODESPACE) && (metadata instanceof Metadata || metadata instanceof ReferenceSystem)) {
value = referenceSystemCodeSpace(metadata); // Special case.
pathOffset = template.path.length;
} else if ((identifier.equals("referenceSystemInfo") || identifier.equals("referenceSystemIdentifier")) && template.endsWith(REFERENCE_SYSTEM_VERSION) && (metadata instanceof Metadata || metadata instanceof ReferenceSystem)) {
value = referenceSystemVersion(metadata); // Special case.
pathOffset = template.path.length;
} else if (metadata instanceof TemporalPrimitive) {
value = extent((TemporalPrimitive) metadata, identifier); // Special case.
} else {
value = template.standard.asValueMap(metadata, KeyNamePolicy.UML_IDENTIFIER, ValueExistencePolicy.NON_EMPTY).get(identifier);
}
if (value == null) {
return;
}
/*
* Verify if the value is a collection. We do not rely on (value instanceof Collection)
* only because it may not be reliable if the value implements more than one interface.
* Instead, we rely on the method contract.
*/
if (value instanceof Collection<?> && isCollection(template.standard, metadata, identifier)) {
final boolean isField = (++pathOffset >= template.path.length);
final Object[] values = ((Collection<?>) value).toArray();
for (int i=0; i<values.length; i++) {
indices[pathOffset - 1] = i + 1;
value = values[i];
if (isField) {
nodes.add(new ValueNode(template, indices, value));
} else if (value != null) {
getValues(template, value, pathOffset);
}
}
return;
}
/*
* The value is not a collection. Continue the loop for each components in the path. For example
* if the path is "identificationInfo.extent.geographicElement.southBoundLatitude", then the loop
* would be executed for "identificationInfo", then "extent", etc. if all components were singleton.
*/
indices[pathOffset] = 0; // 0 means "not a collection".
metadata = value;
} while (++pathOffset < template.path.length);
nodes.add(new ValueNode(template, indices, value));
}
/**
* Given a newly computed set of {@link #nodes}, separates the value to use for the given {@code TemplateNode}.
*
* <p>The current version only ensures that the number of elements is not greater than {@link TemplateNode#maxOccurs}.
* However if we want to apply a more sophisticated filter in a future version, it could be applied here.</p>
*/
private void filterIgnoredValues(final TemplateNode template) {
final int size = nodes.size();
final int maxOccurs = template.maxOccurs;
if (size > maxOccurs) {
nodes.subList(maxOccurs, size).clear();
}
final NumerotedPath[] ignore = template.ignore;
if (ignore != null) {
final Iterator<ValueNode> it = nodes.iterator();
while (it.hasNext()) {
final ValueNode node = it.next();
for (final NumerotedPath p : ignore) {
if (node.pathEquals(p)) {
it.remove();
break;
}
}
}
}
}
/**
* Adds the non-null children to the given parent.
* The parent will be created when first needed.
*
* @param parent The parent, or {@code null} if not yet created.
* @param children The children to add to the parent, or {@code null}.
* @return The parent, newly created if the given {@code parent} was null.
*/
private ValueNode addTo(final TemplateNode template, ValueNode parent, final ValueNode[] children) {
if (children != null) {
for (final ValueNode child : children) {
if (child != null) {
if (parent == null) {
parent = new ValueNode(template, indices, null);
}
parent.add(child);
}
}
}
return parent;
}
/**
* Returns the type of a metadata property.
*/
private static Class<?> getType(final TemplateNode template, final Class<?> type, final TypeValuePolicy policy, final String identifier) {
return template.standard.asTypeMap(type, KeyNamePolicy.UML_IDENTIFIER, policy).get(identifier);
}
/**
* Returns {@code true} if the given property of the given metadata is a collection according the method contract.
* We do not rely only on {@code (value instanceof Collection)} because it may not be reliable if the value
* implements more than one interface.
*/
static boolean isCollection(final MetadataStandard standard, final Object metadata, final CharSequence identifier) {
return Collection.class.isAssignableFrom(standard.asTypeMap(metadata.getClass(),
KeyNamePolicy.UML_IDENTIFIER, TypeValuePolicy.PROPERTY_TYPE).get(identifier));
}
/**
* Special case for {@link #REFERENCE_SYSTEM_CODE}.
*/
private static String referenceSystemCode(final Object obj) {
if (obj instanceof Metadata) {
final Metadata metadata = (Metadata) obj;
for (final ReferenceSystem r : metadata.getReferenceSystemInfo()) {
String code = referenceSystemCode(r);
if (code != null) return code;
}
} else if (obj instanceof ReferenceSystem) {
return referenceSystemCode((ReferenceSystem)obj);
}
return null;
}
private static String referenceSystemCode(final ReferenceSystem r) {
if (r instanceof ReferenceSystemMetadata) {
if(r.getName() != null) {
final String code = r.getName().getCode();
if (code != null) return code;
}
} else {
for (final Identifier id : r.getIdentifiers()) {
final String code = id.getCode();
if (code != null) return code;
}
}
return null;
}
/**
* Special case for {@link #REFERENCE_SYSTEM_CODESPACE}.
*/
private static String referenceSystemCodeSpace(final Object obj) {
if (obj instanceof Metadata) {
final Metadata metadata = (Metadata) obj;
for (final ReferenceSystem r : metadata.getReferenceSystemInfo()) {
String code = referenceSystemCodeSpace(r);
if (code != null) return code;
}
} else if (obj instanceof ReferenceSystem) {
return referenceSystemCodeSpace((ReferenceSystem)obj);
}
return null;
}
private static String referenceSystemCodeSpace(final ReferenceSystem r) {
if (r instanceof ReferenceSystemMetadata) {
if(r.getName() != null) {
final String codespace = r.getName().getCodeSpace();
if (codespace != null) return codespace;
}
} else {
for (final Identifier id : r.getIdentifiers()) {
final String codespace = id.getCodeSpace();
if (codespace != null) return codespace;
}
}
return null;
}
/**
* Special case for {@link #REFERENCE_SYSTEM_VERSION}.
*/
private static String referenceSystemVersion(final Object obj) {
if (obj instanceof Metadata) {
final Metadata metadata = (Metadata) obj;
for (final ReferenceSystem r : metadata.getReferenceSystemInfo()) {
String code = referenceSystemVersion(r);
if (code != null) return code;
}
} else if (obj instanceof ReferenceSystem) {
return referenceSystemVersion((ReferenceSystem)obj);
}
return null;
}
private static String referenceSystemVersion(final ReferenceSystem r) {
if (r instanceof ReferenceSystemMetadata) {
if(r.getName() != null) {
final String version = r.getName().getVersion();
if (version != null) return version;
}
} else {
for (final Identifier id : r.getIdentifiers()) {
final String version = id.getVersion();
if (version != null) return version;
}
}
return null;
}
/**
* Special case for extent information.
*/
private static Date extent(final TemporalPrimitive metadata, final String identifier) throws ParseException {
final Instant instant;
if (metadata instanceof Period) {
switch (identifier) {
case "beginPosition" : instant = ((Period) metadata).getBeginning(); break;
case "endPosition" : instant = ((Period) metadata).getEnding(); break;
default : throw new ParseException("Unsupported extent property: " + identifier);
}
} else if (metadata instanceof Instant) {
instant = (Instant) metadata;
} else {
throw new ParseException("Unsupported extent: " + metadata);
}
return (instant != null) ? instant.getDate() : null;
}
}