blob: 20697e83e30eb72f21b24ea0eba32b3a77707142 [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.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);
}
}
}