blob: 0ecfd7ccffddd694679f79624e8459f1cf33428e [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.feature;
import java.util.Map;
import java.util.Optional;
import java.util.IdentityHashMap;
import org.opengis.util.GenericName;
import org.opengis.util.NameFactory;
import org.opengis.util.InternationalString;
import org.opengis.metadata.maintenance.ScopeCode;
import org.opengis.metadata.quality.ConformanceResult;
import org.opengis.metadata.quality.DataQuality;
import org.opengis.metadata.quality.Element;
import org.opengis.metadata.quality.Result;
import org.apache.sis.util.Static;
import org.apache.sis.util.iso.DefaultNameFactory;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.feature.Resources;
// Branch-dependent imports
import org.opengis.feature.Attribute;
import org.opengis.feature.AttributeType;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
import org.opengis.feature.FeatureAssociationRole;
import org.opengis.feature.IdentifiedType;
import org.opengis.feature.InvalidPropertyValueException;
import org.opengis.feature.Operation;
import org.opengis.feature.PropertyType;
/**
* Static methods working on features or attributes.
*
* @author Martin Desruisseaux (Geomatys)
* @author Johann Sorel (Geomatys)
* @author Alexis Manin (Geomatys)
* @version 1.1
* @since 0.5
* @module
*/
public final class Features extends Static {
/**
* Do not allow instantiation of this class.
*/
private Features() {
}
/**
* Casts the given attribute type to the given parameterized type.
* An exception is thrown immediately if the given type does not have the expected
* {@linkplain DefaultAttributeType#getValueClass() value class}.
*
* @param <V> the expected value class.
* @param type the attribute type to cast, or {@code null}.
* @param valueClass the expected value class.
* @return the attribute type casted to the given value class, or {@code null} if the given type was null.
* @throws ClassCastException if the given attribute type does not have the expected value class.
*
* @category verification
*/
@SuppressWarnings("unchecked")
public static <V> AttributeType<V> cast(final AttributeType<?> type, final Class<V> valueClass)
throws ClassCastException
{
if (type != null) {
final Class<?> actual = type.getValueClass();
/*
* We require a strict equality - not type.isAssignableFrom(actual) - because in
* the latter case we could have (to be strict) to return a <? extends V> type.
*/
if (!valueClass.equals(actual)) {
throw new ClassCastException(Resources.format(Resources.Keys.MismatchedValueClass_3,
type.getName(), valueClass, actual));
}
}
return (AttributeType<V>) type;
}
/**
* Casts the given attribute instance to the given parameterized type.
* An exception is thrown immediately if the given instance does not have the expected
* {@linkplain DefaultAttributeType#getValueClass() value class}.
*
* @param <V> the expected value class.
* @param attribute the attribute instance to cast, or {@code null}.
* @param valueClass the expected value class.
* @return the attribute instance casted to the given value class, or {@code null} if the given instance was null.
* @throws ClassCastException if the given attribute instance does not have the expected value class.
*
* @category verification
*/
@SuppressWarnings("unchecked")
public static <V> Attribute<V> cast(final Attribute<?> attribute, final Class<V> valueClass)
throws ClassCastException
{
if (attribute != null) {
final Class<?> actual = attribute.getType().getValueClass();
/*
* We require a strict equality - not type.isAssignableFrom(actual) - because in
* the latter case we could have (to be strict) to return a <? extends V> type.
*/
if (!valueClass.equals(actual)) {
throw new ClassCastException(Resources.format(Resources.Keys.MismatchedValueClass_3,
attribute.getName(), valueClass, actual));
}
}
return (Attribute<V>) attribute;
}
/**
* Returns the given type as an {@link AttributeType} by casting if possible, or by getting the result type
* of an operation. More specifically this method returns the first of the following types which apply:
*
* <ul>
* <li>If the given type is an instance of {@link AttributeType}, then it is returned as-is.</li>
* <li>If the given type is an instance of {@link Operation} and the {@linkplain Operation#getResult()
* result type} is an {@link AttributeType}, then that result type is returned.</li>
* <li>If the given type is an instance of {@link Operation} and the {@linkplain Operation#getResult()
* result type} is another operation, then the above check is performed recursively.</li>
* </ul>
*
* @param type the data type to express as an attribute type.
* @return the attribute type, or empty if this method cannot find any.
*
* @since 1.1
*/
public static Optional<AttributeType<?>> toAttribute(IdentifiedType type) {
if (!(type instanceof AttributeType<?>)) {
if (!(type instanceof Operation)) {
return Optional.empty();
}
type = ((Operation) type).getResult();
if (!(type instanceof AttributeType<?>)) {
if (!(type instanceof Operation)) {
return Optional.empty();
}
/*
* Operation returns another operation. This case should be rare and should never
* contain a cycle. However given that the consequence of an infinite cycle here
* would be thread freeze, we check as a safety.
*/
final Map<IdentifiedType,Boolean> done = new IdentityHashMap<>(4);
while (!((type = ((Operation) type).getResult()) instanceof AttributeType<?>)) {
if (!(type instanceof Operation) || done.put(type, Boolean.TRUE) != null) {
return Optional.empty();
}
}
}
}
return Optional.of((AttributeType<?>) type);
}
/**
* Finds a feature type common to all given types, or returns {@code null} if none is found.
* The return value is either one of the given types, or a parent common to all types.
* A feature <var>F</var> is considered a common parent if <code>F.{@link DefaultFeatureType#isAssignableFrom
* isAssignableFrom}(type)</code> returns {@code true} for all elements <var>type</var> in the given array.
*
* @param types types for which to find a common type, or {@code null}.
* @return a feature type which is assignable from all given types, or {@code null} if none.
*
* @see FeatureType#isAssignableFrom(FeatureType)
*
* @since 1.0
*/
public static FeatureType findCommonParent(final Iterable<? extends FeatureType> types) {
return (types != null) ? CommonParentFinder.select(types) : null;
}
/**
* Returns the type of values provided by the given property. For {@linkplain AttributeType attributes}
* (which is the most common case), the value type is given by {@link AttributeType#getValueClass()}.
* For {@linkplain FeatureAssociationRole feature associations}, the value type is {@link Feature}.
* For {@linkplain Operation operations}, the value type is determined recursively from the
* {@linkplain Operation#getResult() operation result}.
* If the value type can not be determined, then this method returns {@code null}.
*
* @param type the property for which to get the type of values, or {@code null}.
* @return the type of values provided by the given property, or {@code null} if unknown.
*
* @see AttributeType#getValueClass()
*
* @since 1.0
*/
public static Class<?> getValueClass(PropertyType type) {
while (type instanceof Operation) {
final IdentifiedType result = ((Operation) type).getResult();
if (result != type && result instanceof PropertyType) {
type = (PropertyType) result;
} else if (result instanceof FeatureType) {
return Feature.class;
} else {
break;
}
}
if (type instanceof AttributeType<?>) {
return ((AttributeType<?>) type).getValueClass();
} else if (type instanceof FeatureAssociationRole) {
return Feature.class;
} else {
return null;
}
}
/**
* Returns the name of the type of values that the given property can take.
* The type of value can be a {@link Class}, a {@link org.opengis.feature.FeatureType}
* or another {@code PropertyType} depending on given argument:
*
* <ul>
* <li>If {@code property} is an {@link AttributeType}, then this method gets the
* {@linkplain DefaultAttributeType#getValueClass() value class} and
* {@linkplain DefaultNameFactory#toTypeName(Class) maps that class to a name}.</li>
* <li>If {@code property} is a {@link FeatureAssociationRole}, then this method gets
* the name of the {@linkplain DefaultAssociationRole#getValueType() value type}.
* This methods can work even if the associated {@code FeatureType} is not yet resolved.</li>
* <li>If {@code property} is an {@link Operation}, then this method returns the name of the
* {@linkplain AbstractOperation#getResult() result type}.</li>
* </ul>
*
* @param property the property for which to get the name of value type.
* @return the name of value type, or {@code null} if none.
*
* @since 0.8
*/
public static GenericName getValueTypeName(final PropertyType property) {
if (property instanceof FeatureAssociationRole) {
// Tested first because this is the main interest for this method.
return DefaultAssociationRole.getValueTypeName((FeatureAssociationRole) property);
} else if (property instanceof AttributeType<?>) {
final DefaultNameFactory factory = DefaultFactories.forBuildin(NameFactory.class, DefaultNameFactory.class);
return factory.toTypeName(((AttributeType<?>) property).getValueClass());
} else if (property instanceof Operation) {
final IdentifiedType result = ((Operation) property).getResult();
if (result != null) {
return result.getName();
}
}
return null;
}
/**
* If the given property is a link, returns the name of the referenced property.
* A link is an operation created by a call to {@link FeatureOperations#link(Map, PropertyType)},
* in which case the value returned by this method is the name of the {@link PropertyType} argument
* which has been given to that {@code link(…)} method.
*
* @param property the property to test, or {@code null} if none.
* @return the referenced property name if {@code property} is a link, or an empty value otherwise.
*
* @see FeatureOperations#link(Map, PropertyType)
*
* @since 1.1
*/
public static Optional<String> getLinkTarget(final PropertyType property) {
if (property instanceof LinkOperation) {
return Optional.of(((LinkOperation) property).referentName);
}
return Optional.empty();
}
/**
* Ensures that all characteristics and property values in the given feature are valid.
* An attribute is valid if it contains a number of values between the
* {@linkplain DefaultAttributeType#getMinimumOccurs() minimum} and
* {@linkplain DefaultAttributeType#getMaximumOccurs() maximum number of occurrences} (inclusive),
* all values are instances of the expected {@linkplain DefaultAttributeType#getValueClass() value class},
* and the attribute is compliant with any other restriction that the implementation may add.
*
* <p>This method gets a quality report as documented in the {@link AbstractFeature#quality()} method
* and verifies that all {@linkplain org.apache.sis.metadata.iso.quality.DefaultConformanceResult#pass()
* conformance tests pass}. If at least one {@code ConformanceResult.pass} attribute is false, then an
* {@code InvalidPropertyValueException} is thrown. Otherwise this method returns doing nothing.
*
* @param feature the feature to validate, or {@code null}.
* @throws InvalidPropertyValueException if the given feature is non-null and does not pass validation.
*
* @since 0.7
*/
public static void validate(final Feature feature) throws InvalidPropertyValueException {
if (feature != null) {
/*
* Delegate to AbstractFeature.quality() if possible because the user may have overridden the method.
* Otherwise fallback on the same code than AbstractFeature.quality() default implementation.
*/
final DataQuality quality;
if (feature instanceof AbstractFeature) {
quality = ((AbstractFeature) feature).quality();
} else {
final Validator v = new Validator(ScopeCode.FEATURE);
v.validate(feature.getType(), feature);
quality = v.quality;
}
/*
* Loop on quality elements and check conformance results.
* NOTE: other types of result are ignored for now, since those other
* types may require threshold and other information to be evaluated.
*/
for (Element element : quality.getReports()) {
for (Result result : element.getResults()) {
if (result instanceof ConformanceResult) {
if (Boolean.FALSE.equals(((ConformanceResult) result).pass())) {
final InternationalString message = ((ConformanceResult) result).getExplanation();
if (message != null) {
throw new InvalidFeatureException(message);
}
}
}
}
}
}
}
}