| /* |
| * 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.feature; |
| |
| import java.util.Map; |
| import java.util.List; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.IdentityHashMap; |
| import java.util.Objects; |
| import org.opengis.util.GenericName; |
| import org.opengis.util.InternationalString; |
| import org.opengis.metadata.Identifier; |
| import org.apache.sis.feature.internal.Resources; |
| import org.apache.sis.feature.privy.AttributeConvention; |
| |
| |
| /** |
| * Indicates the role played by the association between two features. |
| * In the area of geographic information, there exist multiple kinds of associations: |
| * |
| * <ul> |
| * <li><b>Aggregation</b> represents associations between features which can exist even if the aggregate is destroyed.</li> |
| * <li><b>Composition</b> represents relationships where the owned features are destroyed together with the composite.</li> |
| * <li><b>Spatial</b> association represents spatial or topological relationships that may exist between features (e.g. <q>east of</q>).</li> |
| * <li><b>Temporal</b> association may represent for example a sequence of changes over time involving the replacement of some |
| * feature instances by other feature instances.</li> |
| * </ul> |
| * |
| * <h2>Immutability and thread safety</h2> |
| * Instances of this class are immutable if all properties ({@link GenericName} and {@link InternationalString} |
| * instances) and all arguments (e.g. {@code valueType}) given to the constructor are also immutable. |
| * Such immutable instances can be shared by many objects and passed between threads without synchronization. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.0 |
| * |
| * @see DefaultFeatureType |
| * @see AbstractAssociation |
| * |
| * @since 0.5 |
| */ |
| public class DefaultAssociationRole extends FieldType { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = 1592712639262027124L; |
| |
| /** |
| * The type of feature instances to be associated. |
| * |
| * @see #getValueType() |
| */ |
| @SuppressWarnings("serial") // Most SIS implementations are serializable. |
| private volatile FeatureType valueType; |
| |
| /** |
| * The name of the property to use as a title for the associated feature, or an empty string if none. |
| * This field is initially null, then computed when first needed. |
| * This information is used only by {@link AbstractAssociation#toString()} implementation. |
| */ |
| private transient volatile String titleProperty; |
| |
| /** |
| * Constructs an association to the given feature type. The properties map is given unchanged to |
| * the {@linkplain AbstractIdentifiedType#AbstractIdentifiedType(Map) super-class constructor}. |
| * The following table is a reminder of main (not all) recognized map entries: |
| * |
| * <table class="sis"> |
| * <caption>Recognized map entries (non exhaustive list)</caption> |
| * <tr> |
| * <th>Map key</th> |
| * <th>Value type</th> |
| * <th>Returned by</th> |
| * </tr> |
| * <tr> |
| * <td>{@value org.apache.sis.feature.AbstractIdentifiedType#NAME_KEY}</td> |
| * <td>{@link GenericName} or {@link String}</td> |
| * <td>{@link #getName()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.apache.sis.feature.AbstractIdentifiedType#DEFINITION_KEY}</td> |
| * <td>{@link InternationalString} or {@link String}</td> |
| * <td>{@link #getDefinition()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.apache.sis.feature.AbstractIdentifiedType#DESIGNATION_KEY}</td> |
| * <td>{@link InternationalString} or {@link String}</td> |
| * <td>{@link #getDesignation()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.apache.sis.feature.AbstractIdentifiedType#DESCRIPTION_KEY}</td> |
| * <td>{@link InternationalString} or {@link String}</td> |
| * <td>{@link #getDescription()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.apache.sis.feature.AbstractIdentifiedType#DEPRECATED_KEY}</td> |
| * <td>{@link Boolean}</td> |
| * <td>{@link #isDeprecated()}</td> |
| * </tr> |
| * </table> |
| * |
| * @param identification the name and other information to be given to this association role. |
| * @param valueType the type of feature values. |
| * @param minimumOccurs the minimum number of occurrences of the association within its containing entity. |
| * @param maximumOccurs the maximum number of occurrences of the association within its containing entity, |
| * or {@link Integer#MAX_VALUE} if there is no restriction. |
| * |
| * @see org.apache.sis.feature.builder.AssociationRoleBuilder |
| */ |
| public DefaultAssociationRole(final Map<String,?> identification, final DefaultFeatureType valueType, |
| final int minimumOccurs, final int maximumOccurs) |
| { |
| super(identification, minimumOccurs, maximumOccurs); |
| this.valueType = Objects.requireNonNull(valueType); |
| } |
| |
| /** |
| * Constructs an association to a feature type of the given name. |
| * This constructor can be used when creating a cyclic graph of {@link DefaultFeatureType} instances. |
| * In such cases, at least one association needs to be created while its {@code FeatureType} is not yet available. |
| * |
| * <h4>Example</h4> |
| * The following establishes a bidirectional association between feature types <var>A</var> and <var>B</var>: |
| * |
| * {@snippet lang="java" : |
| * String namespace = "My model"; |
| * GenericName nameOfA = Names.createTypeName(namespace, ":", "Feature type A"); |
| * GenericName nameOfB = Names.createTypeName(namespace, ":", "Feature type B"); |
| * FeatureType typeA = new DefaultFeatureType(Map.of(NAME_KEY, nameOfA), false, null, |
| * new DefaultAssociationRole(Map.of(NAME_KEY, "Association to B"), nameOfB, 1, 1), |
| * // More properties if desired. |
| * ); |
| * FeatureType typeB = new DefaultFeatureType(Map.of(NAME_KEY, nameOfB), false, null, |
| * new DefaultAssociationRole(Map.of(NAME_KEY, "Association to A"), featureA, 1, 1), |
| * // More properties if desired. |
| * ); |
| * } |
| * |
| * After the above code completed, the {@linkplain #getValueType() value type} of <q>association to B</q> |
| * has been automatically set to the {@code typeB} instance. |
| * |
| * Callers shall make sure that the feature types graph will not contain more than one feature of the given name. |
| * If more than one {@code FeatureType} instance of the given name is found at resolution time, the selected one |
| * is undetermined. |
| * |
| * @param identification the name and other information to be given to this association role. |
| * @param valueType the name of the type of feature values. |
| * @param minimumOccurs the minimum number of occurrences of the association within its containing entity. |
| * @param maximumOccurs the maximum number of occurrences of the association within its containing entity, |
| * or {@link Integer#MAX_VALUE} if there is no restriction. |
| */ |
| public DefaultAssociationRole(final Map<String,?> identification, final GenericName valueType, |
| final int minimumOccurs, final int maximumOccurs) |
| { |
| super(identification, minimumOccurs, maximumOccurs); |
| this.valueType = new NamedFeatureType(Objects.requireNonNull(valueType)); |
| } |
| |
| /** |
| * Returns {@code true} if the associated {@code FeatureType} is complete (not just a name). |
| * This method returns {@code false} if this {@code FeatureAssociationRole} has been |
| * {@linkplain #DefaultAssociationRole(Map, GenericName, int, int) constructed with only a feature name} |
| * and that named feature has not yet been resolved. |
| * |
| * @return {@code true} if the associated feature is complete, or {@code false} if only its name is known. |
| * |
| * @see #getValueType() |
| * |
| * @since 0.8 |
| */ |
| public final boolean isResolved() { |
| final FeatureType type = valueType; |
| if (type instanceof NamedFeatureType) { |
| final FeatureType resolved = ((NamedFeatureType) type).resolved; |
| if (resolved == null) { |
| return false; |
| } |
| valueType = resolved; |
| } |
| return true; |
| } |
| |
| /** |
| * If the associated feature type is a placeholder for a {@code FeatureType} to be defined later, |
| * replaces the placeholder by the actual instance if available. Otherwise do nothing. |
| * |
| * This method is needed only in case of cyclic graph, e.g. feature <var>A</var> has an association |
| * to feature <var>B</var> which has an association back to <var>A</var>. It may also be <var>A</var> |
| * having an association to itself, <i>etc.</i> |
| * |
| * @param creating the feature type in process of being constructed. |
| * @param properties {@code creating.getProperties(false)} given as a direct reference to the internal field, |
| * without invoking {@code getProperties(…)}. We do that because {@code resolve(…)} is invoked while the |
| * given {@code DefaultFeatureType} is under creation. Since {@code getProperties(…)} can be overridden, |
| * invoking that method on {@code creating} may cause a failure with user code. |
| * @return {@code true} if this association references a resolved feature type after this method call. |
| */ |
| final boolean resolve(final DefaultFeatureType creating, final Collection<AbstractIdentifiedType> properties) { |
| final FeatureType type = valueType; |
| if (type instanceof NamedFeatureType) { |
| FeatureType resolved = ((NamedFeatureType) type).resolved; |
| if (resolved == null) { |
| final GenericName name = type.getName(); |
| if (name.equals(creating.getName())) { |
| resolved = creating; // This is the most common case. |
| } else { |
| /* |
| * The feature that we need to resolve is not the one we just created. Maybe we can find |
| * this desired feature in an association of the `creating` feature, instead of beeing |
| * the `creating` feature itself. This is a little bit unusual, but not illegal. |
| */ |
| final List<DefaultFeatureType> deferred = new ArrayList<>(); |
| resolved = search(creating, properties, name, deferred); |
| if (resolved == null) { |
| /* |
| * Did not found the desired FeatureType in the `creating` instance. |
| * Try harder, by searching recursively in associations of associations. |
| */ |
| if (deferred.isEmpty() || (resolved = deepSearch(deferred, name)) == null) { |
| return false; |
| } |
| } |
| } |
| ((NamedFeatureType) type).resolved = resolved; |
| } |
| valueType = resolved; |
| } |
| return true; |
| } |
| |
| /** |
| * Searches in the given {@code feature} for an associated feature type of the given name. |
| * This method does not search recursively in the associations of the associated features. |
| * Such recursive search will be performed by {@link #deepSearch(List, GenericName)} only |
| * if we do not find the desired feature in the most direct way. |
| * |
| * <p>Current implementation does not check that there are no duplicated names. |
| * See {@link #deepSearch(List, GenericName)} for a rational.</p> |
| * |
| * @param feature the feature in which to search. |
| * @param properties {@code feature.getProperties(false)}, or {@code null} for letting this method performing the call. |
| * @param name the name of the feature to search. |
| * @param deferred where to store {@code FeatureType}s to be eventually used for a deep search. |
| * @return the feature of the given name, or {@code null} if none. |
| */ |
| private static DefaultFeatureType search(final DefaultFeatureType feature, Collection<? extends AbstractIdentifiedType> properties, |
| final GenericName name, final List<DefaultFeatureType> deferred) |
| { |
| /* |
| * Search only in associations declared in the given feature, not in inherited associations. |
| * The inherited associations will be checked in a separated loop below if we did not found |
| * the request feature type in explicitly declared associations. |
| */ |
| if (properties == null) { |
| properties = feature.getProperties(false); |
| } |
| for (final AbstractIdentifiedType property : properties) { |
| if (property instanceof DefaultAssociationRole) { |
| final FeatureType valueType; |
| valueType = ((DefaultAssociationRole) property).valueType; |
| if (valueType instanceof NamedFeatureType) { |
| continue; // Skip unresolved feature types. |
| } |
| if (name.equals(valueType.getName())) { |
| return (DefaultFeatureType) valueType; |
| } |
| deferred.add((DefaultFeatureType) valueType); |
| } |
| } |
| /* |
| * Search in inherited associations as a separated step, in order to include the overridden |
| * associations in the search. Overridden associations have the same association role name, |
| * but not necessarily the same feature type (may be a subtype). This is equivalent to |
| * "covariant return type" in the Java language. |
| */ |
| for (DefaultFeatureType type : feature.getSuperTypes()) { |
| if (name.equals(type.getName())) { |
| return type; |
| } |
| type = search(type, null, name, deferred); |
| if (type != null) { |
| return type; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Potentially invoked after {@code search(FeatureType, Collection, GenericName, List)} for searching |
| * in associations of associations. |
| * |
| * <p>Current implementation does not check that there are no duplicated names. Even if we did so, |
| * a graph of feature types may have no duplicated names at this time but some duplicated names |
| * later. We rather put a warning in {@link #DefaultAssociationRole(Map, GenericName, int, int)} |
| * javadoc.</p> |
| * |
| * @param deferred the feature types collected by {@code search(FeatureType, Collection, GenericName, List)}. |
| * @param name the name of the feature to search. |
| * @return the feature of the given name, or {@code null} if none. |
| */ |
| private static DefaultFeatureType deepSearch(final List<DefaultFeatureType> deferred, final GenericName name) { |
| final Map<FeatureType,Boolean> done = new IdentityHashMap<>(8); |
| for (int i=0; i<deferred.size();) { |
| DefaultFeatureType valueType = deferred.get(i++); |
| if (done.put(valueType, Boolean.TRUE) == null) { |
| deferred.subList(0, i).clear(); // Discard previous value for making more room. |
| valueType = search(valueType, null, name, deferred); |
| if (valueType != null) { |
| return valueType; |
| } |
| i = 0; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the type of feature values. |
| * |
| * <p>This method cannot be invoked if {@link #isResolved()} returns {@code false}. |
| * However, it is still possible to {@linkplain Features#getValueTypeName |
| * get the associated feature type name}.</p> |
| * |
| * <div class="warning"><b>Warning:</b> In a future SIS version, the return type may be changed |
| * to {@code org.opengis.feature.FeatureType}. This change is pending GeoAPI revision.</div> |
| * |
| * @return the type of feature values. |
| * @throws IllegalStateException if the feature type has been specified |
| * {@linkplain #DefaultAssociationRole(Map, GenericName, int, int) only by its name} |
| * and not yet resolved. |
| * |
| * @see #isResolved() |
| */ |
| public final DefaultFeatureType getValueType() { |
| /* |
| * This method shall be final for consistency with other methods in this classes |
| * which use the `valueType` field directly. Furthermore, this method is invoked |
| * (indirectly) by DefaultFeatureType constructors. |
| */ |
| FeatureType type = valueType; |
| if (type instanceof NamedFeatureType) { |
| type = ((NamedFeatureType) type).resolved; |
| if (type == null) { |
| throw new IllegalStateException(Resources.format(Resources.Keys.UnresolvedFeatureName_1, getName())); |
| } |
| valueType = type; |
| } |
| return (DefaultFeatureType) type; |
| } |
| |
| /** |
| * Returns the name of the feature type. This information is always available |
| * even when the name has not yet been {@linkplain #resolve resolved}. |
| */ |
| static GenericName getValueTypeName(final DefaultAssociationRole role) { |
| // Method is static for compatibility with branches on GeoAPI snapshots. |
| return role.valueType.getName(); |
| } |
| |
| /** |
| * Returns the name of the property to use as a title for the associated feature, or {@code null} if none. |
| * This method applies the following heuristic rules: |
| * |
| * <ul> |
| * <li>If associated feature has a property named {@code "sis:identifier"}, then this method returns that name.</li> |
| * <li>Otherwise if the associated feature has a mandatory property of type {@link CharSequence}, {@link GenericName} |
| * or {@link Identifier}, then this method returns the name of that property.</li> |
| * <li>Otherwise if the associated feature has an optional property of type {@link CharSequence}, {@link GenericName} |
| * or {@link Identifier}, then this method returns the name of that property.</li> |
| * <li>Otherwise this method returns {@code null}.</li> |
| * </ul> |
| * |
| * This method should be used only for display purpose, not as a reliable or stable way to get the identifier. |
| * The heuristic rules implemented in this method may change in any future Apache SIS version. |
| * |
| * <p><b>API note:</b> a non-static method would be more elegant in this "SIS for GeoAPI 3.0" branch. |
| * However this method needs to be static in other SIS branches, because they work with interfaces |
| * rather than SIS implementation. We keep the method static in this branch too for easier merges.</p> |
| */ |
| static String getTitleProperty(final DefaultAssociationRole role) { |
| String p = role.titleProperty; // No synchronization - not a big deal if computed twice. |
| if (p != null) { |
| return p.isEmpty() ? null : p; |
| } |
| p = searchTitleProperty(role.getValueType()); |
| role.titleProperty = (p != null) ? p : ""; |
| return p; |
| } |
| |
| /** |
| * Implementation of {@link #getTitleProperty(DefaultAssociationRole)} for first search, |
| * or for non-SIS {@code FeatureAssociationRole} implementations. |
| */ |
| private static String searchTitleProperty(final DefaultFeatureType ft) { |
| String fallback = null; |
| try { |
| return ft.getProperty(AttributeConvention.IDENTIFIER).getName().toString(); |
| } catch (IllegalArgumentException e) { |
| // Ignore. |
| } |
| for (final AbstractIdentifiedType type : ft.getProperties(true)) { |
| if (type instanceof DefaultAttributeType<?>) { |
| final DefaultAttributeType<?> pt = (DefaultAttributeType<?>) type; |
| final Class<?> valueClass = pt.getValueClass(); |
| if (CharSequence.class.isAssignableFrom(valueClass) || |
| GenericName .class.isAssignableFrom(valueClass) || |
| Identifier .class.isAssignableFrom(valueClass)) |
| { |
| final String name = pt.getName().toString(); |
| if (pt.getMaximumOccurs() != 0) { |
| return name; |
| } else if (fallback == null) { |
| fallback = name; |
| } |
| } |
| } |
| } |
| return fallback; |
| } |
| |
| /** |
| * Returns the minimum number of occurrences of the association within its containing entity. |
| * The returned value is greater than or equal to zero. |
| * |
| * @return the minimum number of occurrences of the association within its containing entity. |
| */ |
| @Override |
| public final int getMinimumOccurs() { |
| return super.getMinimumOccurs(); |
| } |
| |
| /** |
| * Returns the maximum number of occurrences of the association within its containing entity. |
| * The returned value is greater than or equal to the {@link #getMinimumOccurs()} value. |
| * If there is no maximum, then this method returns {@link Integer#MAX_VALUE}. |
| * |
| * @return the maximum number of occurrences of the association within its containing entity, |
| * or {@link Integer#MAX_VALUE} if none. |
| */ |
| @Override |
| public final int getMaximumOccurs() { |
| return super.getMaximumOccurs(); |
| } |
| |
| /** |
| * Creates a new association instance of this role. |
| * |
| * @return a new association instance. |
| * |
| * @see AbstractAssociation#create(DefaultAssociationRole) |
| */ |
| public AbstractAssociation newInstance() { |
| return AbstractAssociation.create(this); |
| } |
| |
| /** |
| * Returns a hash code value for this association role. |
| * |
| * @return {@inheritDoc} |
| */ |
| @Override |
| public int hashCode() { |
| /* |
| * Do not use the full `valueType` object for computing hash code, |
| * because it may change before and after `resolve` is invoked. In |
| * addition, this avoid infinite recursivity in case of cyclic graph. |
| */ |
| return super.hashCode() + valueType.getName().hashCode(); |
| } |
| |
| /** |
| * Compares this association role with the given object for equality. |
| * |
| * @return {@inheritDoc} |
| */ |
| @Override |
| public boolean equals(final Object obj) { |
| if (obj == this) { |
| return true; |
| } |
| if (super.equals(obj)) { |
| final DefaultAssociationRole that = (DefaultAssociationRole) obj; |
| return valueType.equals(that.valueType); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns a string representation of this association role. |
| * The returned string is for debugging purpose and may change in any future SIS version. |
| * |
| * @return a string representation of this association role for debugging purpose. |
| */ |
| @Override |
| public String toString() { |
| return toString(deprecated, "FeatureAssociationRole", getName(), valueType.getName()).toString(); |
| } |
| } |