| /* |
| * 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.annotation.Obligation; |
| import org.opengis.metadata.Datatype; |
| import org.opengis.metadata.citation.Citation; |
| import org.opengis.metadata.ExtendedElementInformation; |
| import org.opengis.metadata.citation.Responsibility; |
| 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; |
| |
| import static java.util.logging.Logger.getLogger; |
| |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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(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; |
| } |
| |
| /** |
| * Returns the name of the person or organization creating the element. |
| */ |
| @Override |
| public Collection<? extends Responsibility> 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); |
| } |
| } |
| } |