| /* |
| * 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.Collection; |
| import java.util.Collections; |
| import org.opengis.util.GenericName; |
| import org.opengis.util.InternationalString; |
| import org.opengis.metadata.Identifier; |
| import org.opengis.metadata.maintenance.ScopeCode; |
| import org.opengis.metadata.quality.DataQuality; |
| import org.opengis.metadata.quality.EvaluationMethodType; |
| import org.apache.sis.metadata.iso.quality.AbstractElement; |
| import org.apache.sis.metadata.iso.quality.DefaultDataQuality; |
| import org.apache.sis.metadata.iso.quality.DefaultDomainConsistency; |
| import org.apache.sis.metadata.iso.quality.DefaultConformanceResult; |
| import org.apache.sis.metadata.iso.quality.DefaultScope; |
| import org.apache.sis.referencing.NamedIdentifier; |
| import org.apache.sis.util.resources.Errors; |
| |
| // Branch-dependent imports |
| import org.opengis.feature.Property; |
| import org.opengis.feature.PropertyType; |
| import org.opengis.feature.Attribute; |
| import org.opengis.feature.AttributeType; |
| import org.opengis.feature.Feature; |
| import org.opengis.feature.FeatureType; |
| import org.opengis.feature.FeatureAssociation; |
| import org.opengis.feature.FeatureAssociationRole; |
| |
| |
| /** |
| * Provides validation methods to be shared by different implementations. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 0.7 |
| * @since 0.5 |
| * @module |
| */ |
| final class Validator { |
| /** |
| * The data quality report. |
| */ |
| final DefaultDataQuality quality; |
| |
| /** |
| * Creates a new validator. |
| * |
| * @param scope {@code FEATURE} if the object to validate is a feature, or |
| * {@code ATTRIBUTE} for an attribute, or {@code null} otherwise. |
| */ |
| Validator(final ScopeCode scope) { |
| quality = new DefaultDataQuality(); |
| if (scope != null) { |
| quality.setScope(new DefaultScope(scope)); |
| } |
| } |
| |
| /** |
| * Adds a report for a constraint violation. If the given {@code report} is {@code null}, then this method creates |
| * a new {@link DefaultDomainConsistency} instance with the measure identification set to the property name. |
| * |
| * <div class="note"><b>Note:</b> |
| * setting {@code measureIdentification} to the property name may look like a departure from ISO intent, |
| * since the former should be an identification of the <em>quality measurement</em> rather then the measure itself. |
| * (setting {@code measureDescription} to {@code type.getDescription()} would probably be wrong for that reason). |
| * However {@code measureIdentification} is only an identifier, not a full description of the quality measurement |
| * We are not strictly forbidden to use the same identifier for both the quality measurement than the measurement |
| * itself. However strictly speaking, maybe we should use a different scope.</div> |
| * |
| * @param report where to add the result, or {@code null} if not yet created. |
| * @param type description of the property for which a constraint violation has been found. |
| * @param explanation explanation of the constraint violation. |
| * @return the {@code report}, or a new report if {@code report} was null. |
| */ |
| private AbstractElement addViolationReport(AbstractElement report, |
| final PropertyType type, final InternationalString explanation) |
| { |
| if (report == null) { |
| final GenericName name = type.getName(); |
| report = new DefaultDomainConsistency(); |
| // Do not invoke report.setMeasureDescription(type.getDescription()) - see above javadoc. |
| report.setMeasureIdentification(name instanceof Identifier ? (Identifier) name : new NamedIdentifier(name)); |
| report.setEvaluationMethodType(EvaluationMethodType.DIRECT_INTERNAL); |
| quality.getReports().add(report); |
| } |
| report.getResults().add(new DefaultConformanceResult(null, explanation, false)); |
| return report; |
| } |
| |
| /** |
| * Wraps singleton value in a collection for processing by {@code validate(…)} methods. |
| */ |
| private static Collection<?> asList(final Object value, final int maximumOccurrences) { |
| if (maximumOccurrences <= 1) { |
| return (value != null) ? Collections.singletonList(value) : Collections.emptyList(); |
| } else { |
| return (Collection<?>) value; |
| } |
| } |
| |
| /** |
| * Implementation of {@link AbstractFeature#quality()}, also shared by {@link Features} static method. |
| * |
| * @param type the type of the {@code feature} argument, provided explicitly for protecting from user overriding. |
| * @param feature the feature to validate. |
| */ |
| void validate(final FeatureType type, final Feature feature) { |
| for (final PropertyType pt : type.getProperties(true)) { |
| final Property property = feature.getProperty(pt.getName().toString()); |
| final DataQuality pq; |
| if (property instanceof AbstractAttribute<?>) { |
| pq = ((AbstractAttribute<?>) property).quality(); |
| } else if (property instanceof AbstractAssociation) { |
| pq = ((AbstractAssociation) property).quality(); |
| } else if (property instanceof Attribute<?>) { |
| validate(((Attribute<?>) property).getType(), ((Attribute<?>) property).getValues()); |
| continue; |
| } else if (property instanceof FeatureAssociation) { |
| validate(((FeatureAssociation) property).getRole(), ((FeatureAssociation) property).getValues()); |
| continue; |
| } else { |
| continue; |
| } |
| if (pq != null) { // Should not be null, but let be safe. |
| quality.getReports().addAll(pq.getReports()); |
| } |
| } |
| } |
| |
| /** |
| * Verifies if the given value is valid for the given attribute type. |
| * This method delegates to one of the {@code validate(…)} methods depending of the value type. |
| */ |
| void validateAny(final PropertyType type, final Object value) { |
| if (type instanceof AttributeType<?>) { |
| validate((AttributeType<?>) type, asList(value, |
| ((AttributeType<?>) type).getMaximumOccurs())); |
| } |
| if (type instanceof FeatureAssociationRole) { |
| validate((FeatureAssociationRole) type, asList(value, |
| ((FeatureAssociationRole) type).getMaximumOccurs())); |
| } |
| } |
| |
| /** |
| * Verifies if the given values are valid for the given attribute type. |
| */ |
| void validate(final AttributeType<?> type, final Collection<?> values) { |
| AbstractElement report = null; |
| for (final Object value : values) { |
| /* |
| * In theory, the following check is unnecessary since the type was constrained by the Attribute.setValue(V) |
| * method signature. However in practice the call to Attribute.setValue(…) is sometime done after type erasure, |
| * so we are better to check. |
| */ |
| final Class<?> valueClass = type.getValueClass(); |
| if (!valueClass.isInstance(value)) { |
| report = addViolationReport(report, type, Errors.formatInternational( |
| Errors.Keys.IllegalPropertyValueClass_3, type.getName(), valueClass, value.getClass())); |
| |
| // Report only the first violation for now. |
| break; |
| } |
| } |
| verifyCardinality(report, type, type.getMinimumOccurs(), type.getMaximumOccurs(), values.size()); |
| } |
| |
| /** |
| * Verifies if the given value is valid for the given association role. |
| */ |
| void validate(final FeatureAssociationRole role, final Collection<?> values) { |
| AbstractElement report = null; |
| for (final Object value : values) { |
| final FeatureType type = ((Feature) value).getType(); |
| final FeatureType valueType = role.getValueType(); |
| if (!valueType.isAssignableFrom(type)) { |
| report = addViolationReport(report, role, Errors.formatInternational( |
| Errors.Keys.IllegalPropertyValueClass_3, role.getName(), valueType.getName(), type.getName())); |
| |
| // Report only the first violation for now. |
| break; |
| } |
| } |
| verifyCardinality(report, role, role.getMinimumOccurs(), role.getMaximumOccurs(), values.size()); |
| } |
| |
| /** |
| * Verifies if the given value is compliant with the cardinality constraint. |
| * |
| * @param report where to add the result, or {@code null} if not yet created. |
| */ |
| private void verifyCardinality(final AbstractElement report, final PropertyType type, |
| final int minimumOccurs, final int maximumOccurs, final int count) |
| { |
| if (count < minimumOccurs) { |
| final InternationalString message; |
| if (count == 0) { |
| message = Errors.formatInternational(Errors.Keys.MissingValueForProperty_1, type.getName()); |
| } else { |
| message = Errors.formatInternational(Errors.Keys.TooFewOccurrences_2, minimumOccurs, type.getName()); |
| } |
| addViolationReport(report, type, message); |
| } else if (count > maximumOccurs) { |
| final InternationalString message; |
| if (maximumOccurs == 0) { |
| message = Errors.formatInternational(Errors.Keys.ForbiddenProperty_1, type.getName()); |
| } else { |
| message = Errors.formatInternational(Errors.Keys.TooManyOccurrences_2, maximumOccurs, type.getName()); |
| } |
| addViolationReport(report, type, message); |
| } |
| } |
| } |