blob: 4012cb7ea9fdd217f949013a62af7b402513d0ad [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.internal.jaxb.referencing;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.lang.reflect.Array;
import javax.xml.bind.annotation.XmlElementRef;
import org.opengis.util.GenericName;
import org.opengis.metadata.Identifier;
import org.opengis.referencing.ReferenceIdentifier;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.ParameterNotFoundException;
import org.apache.sis.parameter.AbstractParameterDescriptor;
import org.apache.sis.parameter.DefaultParameterDescriptor;
import org.apache.sis.parameter.DefaultParameterDescriptorGroup;
import org.apache.sis.parameter.DefaultParameterValueGroup;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.referencing.NamedIdentifier;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.util.CorruptedObjectException;
import org.apache.sis.internal.util.CollectionsExt;
import org.apache.sis.internal.jaxb.gco.PropertyType;
import org.apache.sis.internal.jaxb.Context;
/**
* JAXB adapter mapping implementing class to the GeoAPI interface. See
* package documentation for more information about JAXB and interface.
*
* <p>This class provides additional {@code merge(…)} methods for building a unique descriptor
* instance when the same descriptor is declared in more than one place in the GML document.
* Some examples of duplications are:</p>
*
* <ul>
* <li>The descriptors listed under the {@code <gml:group>} element, which duplicate the descriptors listed
* under each {@code <gml:parameterValue>} element.</li>
* <li>The descriptors declared in each parameter value of a {@code SingleOperation}, which duplicate the
* descriptors declared in the associated {@code OperationMethod}.</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.6
* @since 0.6
* @module
*/
public final class CC_GeneralOperationParameter extends PropertyType<CC_GeneralOperationParameter, GeneralParameterDescriptor> {
/**
* The default value of {@code minimumOccurs} and {@code maximumOccurs} if the XML element is not provided.
*/
public static final short DEFAULT_OCCURRENCE = 1;
/**
* The properties to ignore in the descriptor parsed from GML when this descriptor is merged with a
* pre-defined descriptor. Remarks:
*
* <ul>
* <li>We ignore the name because the comparisons shall be performed by the caller with
* {@link IdentifiedObjects#isHeuristicMatchForName} or something equivalent.</li>
* <li>We ignore aliases and identifiers because they are collections, which require
* handling in a special way.</li>
* </ul>
*/
private static final String[] IGNORE_DURING_MERGE = {
GeneralParameterDescriptor.NAME_KEY,
GeneralParameterDescriptor.ALIAS_KEY,
GeneralParameterDescriptor.IDENTIFIERS_KEY
};
/**
* Empty constructor for JAXB only.
*/
public CC_GeneralOperationParameter() {
}
/**
* Returns the GeoAPI interface which is bound by this adapter.
* This method is indirectly invoked by the private constructor
* below, so it shall not depend on the state of this object.
*
* @return {@code GeneralParameterDescriptor.class}
*/
@Override
protected Class<GeneralParameterDescriptor> getBoundType() {
return GeneralParameterDescriptor.class;
}
/**
* Constructor for the {@link #wrap} method only.
*/
private CC_GeneralOperationParameter(final GeneralParameterDescriptor parameter) {
super(parameter);
}
/**
* Invoked by {@link PropertyType} at marshalling time for wrapping the given value in a
* {@code <gml:OperationParameter>} or {@code <gml:OperationParameterGroup>} XML element.
*
* @param parameter the element to marshal.
* @return a {@code PropertyType} wrapping the given the element.
*/
@Override
protected CC_GeneralOperationParameter wrap(final GeneralParameterDescriptor parameter) {
return new CC_GeneralOperationParameter(parameter);
}
/**
* Invoked by JAXB at marshalling time for getting the actual element to write
* inside the {@code <gml:parameter>} XML element.
* This is the value or a copy of the value given in argument to the {@code wrap} method.
*
* @return the element to be marshalled.
*
* @see CC_GeneralParameterValue#getElement()
*/
@XmlElementRef
public AbstractParameterDescriptor getElement() {
final GeneralParameterDescriptor metadata = this.metadata;
if (metadata instanceof AbstractParameterDescriptor) {
return (AbstractParameterDescriptor) metadata;
}
if (metadata instanceof ParameterDescriptor) {
return DefaultParameterDescriptor.castOrCopy((ParameterDescriptor<?>) metadata);
}
if (metadata instanceof ParameterDescriptorGroup) {
return DefaultParameterDescriptorGroup.castOrCopy((ParameterDescriptorGroup) metadata);
}
return null; // Unknown types are currently not marshalled (we may revisit that in a future SIS version).
}
/**
* Invoked by JAXB at unmarshalling time for storing the result temporarily.
*
* @param parameter the unmarshalled element.
*/
public void setElement(final AbstractParameterDescriptor parameter) {
metadata = parameter;
}
/**
* Verifies that the given descriptor is non-null and contains at least a name.
* This method is used after unmarshalling.
*/
static boolean isValid(final GeneralParameterDescriptor descriptor) {
return descriptor != null && descriptor.getName() != null;
}
/**
* Returns {@code true} if the given descriptor is restricted to a constant value.
* This constraint exists in some pre-defined map projections.
*
* <div class="note"><b>Example:</b>
* the <cite>"Latitude of natural origin"</cite> parameter of <cite>"Mercator (1SP)"</cite> projection
* is provided for completeness, but should never be different than zero in this particular projection
* (otherwise it would be a <cite>"Mercator (variant C)"</cite> projection). But if this parameter is
* nevertheless provided, the SIS implementation will use it. From this point of view, SIS is tolerant
* to non-zero value.
*
* <p>If the GML document declares explicitly a restricted parameter, maybe it intends to use it with
* a non-zero value. Consequently the {@code merge(…)} method will not propagate this restriction.</p>
* </div>
*/
private static boolean isRestricted(final ParameterDescriptor<?> descriptor) {
final Comparable<?> min = descriptor.getMinimumValue();
if (min instanceof Number) {
final Comparable<?> max = descriptor.getMaximumValue();
if (max instanceof Number) {
// Compare as 'double' because we want (-0 == +0) to be true.
return ((Number) min).doubleValue() == ((Number) max).doubleValue();
}
}
return false;
}
/**
* Returns a descriptor with the same properties than the {@code provided} one, but completed with information
* not found in GML. Those missing information are given by the {@code complete} descriptor, which may come from
* two sources:
*
* <ul>
* <li>The descriptor for a {@code <gml:ParameterValue>} element. Those descriptors are more complete than the
* ones provided by {@code <gml:OperationParameter>} elements alone because the parameter value allows SIS
* to infer the {@code valueClass}.</li>
* <li>A pre-defined parameter descriptor from the {@link org.apache.sis.internal.referencing.provider} package.</li>
* </ul>
*
* @param provided the descriptor unmarshalled from the GML document.
* @param complete the descriptor to use for completing missing information.
* @return the descriptor to use. May be one of the arguments given to this method, or a new instance.
*
* @see <a href="http://issues.apache.org/jira/browse/SIS-290">SIS-290</a>
*/
static GeneralParameterDescriptor merge(final GeneralParameterDescriptor provided,
final GeneralParameterDescriptor complete)
{
if (provided == complete) {
return complete;
}
final boolean isGroup;
if (provided instanceof ParameterDescriptor<?> && complete instanceof ParameterDescriptor<?>) {
isGroup = false; // This is by far the most usual case.
} else if (provided instanceof ParameterDescriptorGroup && complete instanceof ParameterDescriptorGroup) {
isGroup = true;
} else {
/*
* Mismatched or unknown type. It should not happen with descriptors parsed by JAXB and with
* pre-defined descriptors provided by SIS. But it could happen with a pre-defined descriptor
* found in a user-provided OperationMethod with malformed parameters.
* Return the descriptor found in the GML document as-is.
*/
return provided;
}
final int minimumOccurs = provided.getMinimumOccurs();
final int maximumOccurs = provided.getMaximumOccurs();
final Map<String,?> expected = IdentifiedObjects.getProperties(complete);
final Map<String,?> actual = IdentifiedObjects.getProperties(provided, IGNORE_DURING_MERGE);
final boolean canSubstitute = complete.getMinimumOccurs() == minimumOccurs
&& complete.getMaximumOccurs() == maximumOccurs
&& expected.entrySet().containsAll(actual.entrySet())
&& containsAll(complete.getAlias(), provided.getAlias())
&& containsAll(complete.getIdentifiers(), provided.getIdentifiers());
if (canSubstitute && !isGroup) {
/*
* The pre-defined or ParameterValue descriptor contains at least all the information found
* in the descriptor parsed from the GML document. We can use the existing instance directly,
* assuming that the additional properties are acceptable.
*
* We make an exception to the above rule if the existing instance put a possibly too strong
* restriction on the parameter values. See 'isRestricted(…)' for more information.
*/
if (!isRestricted((ParameterDescriptor<?>) complete)) {
return complete;
}
}
/*
* Collect the properties specified in the GML document and complete with the properties provided
* by the 'complete' descriptor. If the descriptor is a group, then this 'replacement' method will
* be invoked recursively for each parameter in the group.
*/
final Map<String,Object> merged = new HashMap<>(expected);
merged.putAll(actual); // May overwrite pre-defined properties.
mergeArrays(GeneralParameterDescriptor.ALIAS_KEY, GenericName.class, provided.getAlias(), merged, complete.getName());
mergeArrays(GeneralParameterDescriptor.IDENTIFIERS_KEY, ReferenceIdentifier.class, provided.getIdentifiers(), merged, null);
if (isGroup) {
final List<GeneralParameterDescriptor> descriptors = ((ParameterDescriptorGroup) provided).descriptors();
return merge(DefaultParameterValueGroup.class, merged, merged, minimumOccurs, maximumOccurs,
descriptors.toArray(new GeneralParameterDescriptor[descriptors.size()]),
(ParameterDescriptorGroup) complete, canSubstitute);
} else {
return create(merged, (ParameterDescriptor<?>) provided, (ParameterDescriptor<?>) complete);
}
}
/**
* Returns a descriptor with the given properties, completed with information not found in GML.
* Those extra information are given by the {@code complete} descriptor.
*
* @param caller the public source class to report if a log message need to be emitted.
* @param properties properties as declared in the GML document, to be used if {@code complete} is incompatible.
* @param merged more complete properties, to be used if {@code complete} is compatible.
* @param minimumOccurs value to assign to {@link DefaultParameterDescriptorGroup#getMinimumOccurs()}.
* @param maximumOccurs value to assign to {@link DefaultParameterDescriptorGroup#getMaximumOccurs()}.
* @param provided parameter descriptors declared in the GML document. This array will be overwritten.
* @param complete more complete parameter descriptors.
* @param canSubstitute {@code true} if this method is allowed to return {@code complete}.
* @return the parameter descriptor group to use (may be the {@code complete} instance).
*
* @see <a href="http://issues.apache.org/jira/browse/SIS-290">SIS-290</a>
*/
static ParameterDescriptorGroup merge(final Class<?> caller,
final Map<String,?> properties,
final Map<String,?> merged,
final int minimumOccurs,
final int maximumOccurs,
final GeneralParameterDescriptor[] provided,
final ParameterDescriptorGroup complete,
boolean canSubstitute)
{
boolean isCompatible = true;
final Set<GeneralParameterDescriptor> included = new HashSet<>(Containers.hashMapCapacity(provided.length));
for (int i=0; i<provided.length; i++) {
final GeneralParameterDescriptor p = provided[i];
try {
/*
* Replace the descriptors provided in the GML document by descriptors from the 'complete' instance,
* if possible. Keep trace of the complete descriptors that we found in this process.
*/
GeneralParameterDescriptor predefined = complete.descriptor(p.getName().getCode());
if (predefined != null) { // Safety in case 'complete' is a user's implementation.
canSubstitute &= (provided[i] = merge(p, predefined)) == predefined;
if (!included.add(predefined)) {
throw new CorruptedObjectException(predefined); // Broken hashCode/equals, or object mutated.
}
continue;
}
} catch (ParameterNotFoundException e) {
/*
* Log at Level.WARNING for the first parameter (canSubstitute == true) and at Level.FINE
* for all other (canSubstitute == false). We do not use CC_GeneralOperationParameter as
* the source class because this is an internal class. We rather use the first public class
* in the caller hierarchy, which is either DefaultParameterValueGroup or DefaultOperationMethod.
*/
Context.warningOccured(Context.current(), caller,
(caller == DefaultParameterValueGroup.class) ? "setValues" : "setDescriptors", e, canSubstitute);
}
/*
* If a parameter was not found in the 'complete' descriptor, we will not be able to use that descriptor.
* But we may still be able to use its properties (name, alias, identifier) provided that the parameter
* not found was optional.
*/
isCompatible &= p.getMinimumOccurs() == 0;
canSubstitute = false;
}
if (isCompatible) {
/*
* At this point, we determined that all mandatory parameters in the GML document exist in the 'complete'
* descriptor. However the converse is not necessarily true. Verify that all parameters missing in the GML
* document were optional.
*/
for (final GeneralParameterDescriptor descriptor : complete.descriptors()) {
if (!included.contains(descriptor) && descriptor.getMinimumOccurs() != 0
&& !CC_OperationMethod.isImplicitParameter(descriptor))
{
canSubstitute = false;
isCompatible = false;
break;
}
}
}
if (canSubstitute) {
return complete;
} else {
return new DefaultParameterDescriptorGroup(isCompatible ? merged : properties,
minimumOccurs, maximumOccurs, provided);
}
}
/**
* Creates a new descriptor with the same properties than the {@code provided} one, but completed with
* information not found in GML. Those extra information are given by the {@code complete} descriptor.
*
* <p>It is the caller's responsibility to construct the {@code merged} properties as a merge of the properties
* of the two given descriptors. This can be done with the help of {@link #mergeArrays(String, Class, Collection,
* Map, Identifier)} among others.</p>
*/
private static <T> ParameterDescriptor<T> create(final Map<String,?> merged,
final ParameterDescriptor<?> provided,
final ParameterDescriptor<T> complete)
{
final Class<T> valueClass = complete.getValueClass();
return new DefaultParameterDescriptor<>(merged,
provided.getMinimumOccurs(),
provided.getMaximumOccurs(),
// Values below this point are not provided in GML documents,
// so they must be inferred from the pre-defined descriptor.
valueClass,
Parameters.getValueDomain(complete),
CollectionsExt.toArray(complete.getValidValues(), valueClass),
complete.getDefaultValue());
}
/**
* Returns {@code true} if the {@code complete} collection contains all elements in the {@code provided}
* collection, where each element have been converted to the canonical {@link NamedIdentifier} implementation
* for comparison purpose.
*
* @param <T> the type of elements in the collection.
* @param complete the collection which is expected to contains all elements.
* @param provided the collection which may be a subset of {@code complete}.
* @return {@code true} if {@code complete} contains all {@code provided} elements.
*/
private static <T> boolean containsAll(final Collection<T> complete, final Collection<T> provided) {
if (!provided.isEmpty()) {
final int size = complete.size();
if (size == 0) {
return false;
}
final Set<NamedIdentifier> c = new HashSet<>(Containers.hashMapCapacity(size));
for (final T e : complete) {
c.add(toNamedIdentifier(e));
}
for (final T e : provided) {
if (!c.contains(toNamedIdentifier(e))) {
return false;
}
}
}
return true;
}
/**
* Merges the property of type {@code Collection} identified by the given key.
* This is used when we can not just substitute one collection by the other.
*
* @param <T> the type of elements in the array or collection.
* @param key the key where to fetch or store the array in the {@code merged} map.
* @param componentType the type of elements in the array or collection.
* @param provided the elements unmarshalled from the XML document.
* @param merged the map used for completing missing information.
*/
@SuppressWarnings("unchecked")
private static <T> void mergeArrays(final String key, final Class<T> componentType,
Collection<T> provided, final Map<String,Object> merged, final Identifier remove)
{
if (!provided.isEmpty()) {
T[] complete = (T[]) merged.get(key);
if (complete != null) {
/*
* Add the 'provided' values before 'complete' for two reasons:
* 1) Use the same insertion order than the declaration order in the GML file.
* 2) Replace 'provided' instances by 'complete' instances, since the later
* are sometime pre-defined instances defined as static final constants.
*/
final Map<NamedIdentifier,T> c = new LinkedHashMap<>();
for (final T e : provided) c.put(toNamedIdentifier(e), e);
for (final T e : complete) c.put(toNamedIdentifier(e), e);
c.remove(toNamedIdentifier(remove));
provided = c.values();
}
complete = provided.toArray((T[]) Array.newInstance(componentType, provided.size()));
merged.put(key, complete);
}
}
/**
* Given an {@link Identifier} or {@link GenericName} instance, returns that instance as a {@link NamedIdentifier}
* implementation. The intent is to allow {@code Object.equals(Object)} and hash code to correctly recognize two
* names or identifiers as equal even if they are of different implementations.
*
* <p>Note that {@link NamedIdentifier} is the type of unmarshalled names, aliases and identifiers.
* So this method should not create any new object in a majority of cases.</p>
*/
private static NamedIdentifier toNamedIdentifier(final Object name) {
if (name == null || name.getClass() == NamedIdentifier.class) {
return (NamedIdentifier) name;
} else if (name instanceof Identifier) {
return new NamedIdentifier((Identifier) name);
} else {
return new NamedIdentifier((GenericName) name);
}
}
}