blob: 16cfb6334b5126b67d7cd26141f5242576b1d9ee [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.metadata;
import java.util.Locale;
import java.util.Collection;
import java.util.Collections;
import java.lang.reflect.Method;
import org.opengis.annotation.UML;
import org.opengis.metadata.Datatype;
import org.opengis.metadata.Obligation;
import org.opengis.metadata.citation.Citation;
import org.opengis.metadata.ExtendedElementInformation;
import org.opengis.metadata.citation.ResponsibleParty;
import org.opengis.util.CodeList;
import org.opengis.util.InternationalString;
import org.apache.sis.internal.simple.SimpleIdentifier;
import org.apache.sis.internal.system.Modules;
import org.apache.sis.measure.ValueRange;
import org.apache.sis.util.iso.Types;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.collection.CheckedContainer;
import org.apache.sis.util.logging.Logging;
/**
* Description of a metadata property inferred from Java reflection.
* For a given metadata instances (typically an {@link AbstractMetadata} subclasses,
* but other types are allowed), instances of {@code PropertyInformation} are obtained
* indirectly by the {@link MetadataStandard#asInformationMap(Class, KeyNamePolicy)} method.
*
* <p>This class implements also the {@link org.opengis.metadata.Identifier} and {@link CheckedContainer} interfaces.
* Those features are not directly used by this class, but is published in the {@link MetadataStandard} javadoc.</p>
*
* <div class="note"><b>API note:</b>
* The rational for implementing {@code CheckedContainer} is to consider each {@code ExtendedElementInformation}
* instance as the set of all possible values for the property. If the information had a {@code contains(E)} method,
* it would return {@code true} if the given value is valid for that property.</div>
*
* <h2>Immutability and thread safety</h2>
* This final class is immutable and thus thread-safe.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.5
*
* @param <E> the value type, either the method return type if not a collection,
* or the type of elements in the collection otherwise.
*
* @see InformationMap
* @see MetadataStandard#asInformationMap(Class, KeyNamePolicy)
* @see <a href="https://issues.apache.org/jira/browse/SIS-80">SIS-80</a>
*
* @since 0.3
* @module
*/
final class PropertyInformation<E> extends SimpleIdentifier // Implementing Identifier is part of SIS public API.
implements ExtendedElementInformation, CheckedContainer<E>
{
/**
* For cross-versions compatibility.
*/
private static final long serialVersionUID = 6279709738674566891L;
/**
* The interface which contain this property.
*
* @see #getParentEntity()
*/
private final Class<?> parent;
/**
* The value type, either the method return type if not a collection,
* or the type of elements in the collection otherwise.
*
* @see #getDataType()
* @see #getElementType()
*/
private final Class<E> elementType;
/**
* The minimum number of occurrences.
* A {@code minimumOccurs} value of -1 means that the property is conditional,
* i.e. the actual {@code minimumOccurs} value can either 0 or 1 depending on
* the value of another property.
*
* @see #getObligation()
*/
private final byte minimumOccurs;
/**
* The maximum number of occurrences as an unsigned number.
* Value 255 (or -1 as a signed number) shall be understood as {@link Integer#MAX_VALUE}.
*
* @see #getMaximumOccurrence()
*/
private final byte maximumOccurs;
/**
* The domain of valid values, or {@code null} if none. If non-null, then this is set to an
* instance of {@link ValueRange} at construction time, then replaced by an instance of
* {@link DomainRange} when first needed by the {@link #getDomainValue()} method.
*
* @see #getDomainValue()
*/
private volatile Object domainValue;
/**
* Creates a new {@code PropertyInformation} instance from the annotations on the given getter method.
*
* @param standard the international standard that define the property, or {@code null} if none.
* @param property the property name as defined by the international {@code standard}.
* @param getter the getter method defined in the interface.
* @param elementType the value type, either the method return type if not a collection,
* or the type of elements in the collection otherwise.
* @param range the range of valid values, or {@code null} if none. This information is associated to the
* implementation method rather than the interface one, because it is specific to SIS.
*/
@SuppressWarnings({"unchecked","rawtypes"})
PropertyInformation(final Citation standard, final String property, final Method getter,
final Class<E> elementType, final ValueRange range)
{
super(standard, property, getter.isAnnotationPresent(Deprecated.class));
parent = getter.getDeclaringClass();
this.elementType = elementType;
final UML uml = getter.getAnnotation(UML.class);
byte minimumOccurs = 0;
byte maximumOccurs = 1;
if (uml != null) {
switch (uml.obligation()) {
case MANDATORY: minimumOccurs = 1; break;
case FORBIDDEN: maximumOccurs = 0; break;
case CONDITIONAL: minimumOccurs = -1; break;
}
}
if (maximumOccurs != 0) {
final Class<?> c = getter.getReturnType();
if (c.isArray() || Collection.class.isAssignableFrom(c)) {
maximumOccurs = -1;
}
}
this.minimumOccurs = minimumOccurs;
this.maximumOccurs = maximumOccurs;
this.domainValue = range;
}
/**
* Returns the primary name by which this metadata element is identified.
*/
@Override
public String getName() {
return code;
}
/**
* Returns the ISO name of the class containing the property,
* or the simple class name if the ISO name is undefined.
*
* @see #getParentEntity()
*/
@Override
public final String getCodeSpace() {
String codespace = Types.getStandardName(parent);
if (codespace == null) {
codespace = parent.getSimpleName();
}
return codespace;
}
/**
* Unconditionally returns {@code null}.
*
* @deprecated This property was defined in the 2003 edition of ISO 19115,
* but has been removed in the 2014 edition.
*/
@Override
@Deprecated
public String getShortName() {
return null;
}
/**
* Unconditionally returns {@code null}.
*
* @deprecated This property was defined in the 2003 edition of ISO 19115,
* but has been removed in the 2014 edition.
*/
@Override
@Deprecated
public Integer getDomainCode() {
return null;
}
/**
* Returns the definition of this property, or {@code null} if none.
*/
@Override
public final InternationalString getDefinition() {
return Types.getDescription(parent, code);
}
/**
* Returns the obligation of the element.
*/
@Override
public Obligation getObligation() {
switch (minimumOccurs) {
case -1: return Obligation.CONDITIONAL;
case 0: return Obligation.OPTIONAL;
default: return Obligation.MANDATORY;
}
}
/**
* Returns the condition under which the extended element is mandatory.
* Current implementation always return {@code null}, since the condition
* is not yet documented programmatically.
*/
@Override
public InternationalString getCondition() {
return null;
}
/**
* Returns the kind of value provided in the extended element.
* This is a generic code that describe the element type.
* For more accurate information, see {@link #getElementType()}.
*/
@Override
public Datatype getDataType() {
if (CharSequence.class.isAssignableFrom(elementType)) return Datatype.CHARACTER_STRING;
if (CodeList .class.isAssignableFrom(elementType)) return Datatype.CODE_LIST;
if (Enum .class.isAssignableFrom(elementType)) return Datatype.ENUMERATION;
if (Numbers.isInteger(elementType)) {
return Datatype.INTEGER;
}
// TODO: check the org.opengis.annotation.Classifier annotation here.
return Datatype.TYPE_CLASS;
}
/**
* Returns the case type of values to be stored in the property.
* If the property type is an array or a collection, then this method
* returns the type of elements in the array or collection.
*
* @see TypeValuePolicy#ELEMENT_TYPE
*/
@Override
public Class<E> getElementType() {
return elementType;
}
/**
* Returns the maximum number of times that values are required.
* This method returns 0 if the property is forbidden, {@link Integer#MAX_VALUE}
* if the property is an array or a collection, or 1 otherwise.
*/
@Override
public Integer getMaximumOccurrence() {
final int n = Byte.toUnsignedInt(maximumOccurs);
return (n == 0xFF) ? Integer.MAX_VALUE : n;
}
/**
* Returns valid values that can be assigned to the extended element, or {@code null} if none.
* In the particular case of SIS implementation, this method may return a subclass of
* {@link org.apache.sis.measure.NumberRange}.
*/
@Override
@SuppressWarnings({"unchecked","rawtypes"})
public InternationalString getDomainValue() {
Object domain = domainValue;
if (domain != null) {
if (!(domain instanceof DomainRange)) {
try {
// Not a big deal if we create two instances of that in two concurrent threads.
domain = new DomainRange(elementType, (ValueRange) domain);
} catch (IllegalArgumentException e) {
/*
* May happen only if a ValueRange annotation is applied on the wrong method.
* The JUnit tests ensure that this never happen at least for the SIS metadata
* implementation. If this error happen anyway, the user probably doesn't expect
* to have an IllegalArgumentException while he didn't provided any argument.
* Returning null as a fallback is compliant with the method contract.
*/
Logging.unexpectedException(Logging.getLogger(Modules.METADATA),
PropertyInformation.class, "getDomainValue", e);
domain = null;
}
domainValue = domain;
}
}
return (DomainRange) domain;
}
/**
* Returns the name of the metadata entity under which this metadata element may appear.
* The name may be standard metadata element or other extended metadata element.
*
* @see #getCodeSpace()
*/
@Override
public Collection<String> getParentEntity() {
return Collections.singleton(getCodeSpace());
}
/**
* Specifies how the extended element relates to other existing elements and entities.
* The current implementation always return {@code null}.
*/
@Override
public InternationalString getRule() {
return null;
}
/**
* Unconditionally returns {@code null}.
*/
public InternationalString getRationale() {
return null;
}
/**
* Unconditionally returns an empty list.
*/
@Override
@Deprecated
public Collection<InternationalString> getRationales() {
return Collections.emptyList();
}
/**
* Returns the name of the person or organization creating the element.
*/
@Override
public Collection<? extends ResponsibleParty> getSources() {
return authority.getCitedResponsibleParties();
}
/**
* Compares the given object with this element information for equality.
*
* @param obj the object to compare with this element information for equality.
* @return {@code true} if both objects are equal.
*/
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (super.equals(obj)) {
final PropertyInformation<?> that = (PropertyInformation<?>) obj;
return this.parent == that.parent &&
this.elementType == that.elementType &&
this.minimumOccurs == that.minimumOccurs &&
this.maximumOccurs == that.maximumOccurs;
}
return false;
}
/**
* Computes a hash code value only from the code space and property name.
* We don't need to use the other properties, because the fully qualified
* property name should be a sufficient discriminator.
*/
@Override
public final int hashCode() {
return (parent.hashCode() + 31 * code.hashCode()) ^ (int) serialVersionUID;
}
/**
* Invoked by {@link #toString()} in order to append additional information after the identifier.
*/
@Override
protected void appendStringTo(final StringBuilder buffer) {
buffer.append(" : ").append(Types.getCodeLabel(getDataType()))
.append(", ").append(getObligation().name().toLowerCase(Locale.US))
.append(", maxOccurs=");
final int n = getMaximumOccurrence();
if (n != Integer.MAX_VALUE) {
buffer.append(n);
} else {
buffer.append('∞');
}
final InternationalString domainValue = getDomainValue();
if (domainValue != null) {
buffer.append(", domain=").append(domainValue);
}
}
}