/* * Copyright (c) 2012 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * HUMBOLDT EU Integrated Project #030962 * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.common.instancevalidator; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map.Entry; import javax.annotation.Nullable; import javax.xml.namespace.QName; import org.eclipse.core.runtime.IProgressMonitor; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import de.fhg.igd.slf4jplus.ATransaction; import eu.esdihumboldt.hale.common.align.model.AlignmentUtil; import eu.esdihumboldt.hale.common.align.model.EntityDefinition; import eu.esdihumboldt.hale.common.align.model.impl.TypeEntityDefinition; import eu.esdihumboldt.hale.common.core.service.ServiceProvider; import eu.esdihumboldt.hale.common.instance.extension.validation.ConstraintValidator; import eu.esdihumboldt.hale.common.instance.extension.validation.ConstraintValidatorExtension; import eu.esdihumboldt.hale.common.instance.extension.validation.GroupPropertyConstraintValidator; import eu.esdihumboldt.hale.common.instance.extension.validation.InstanceValidationContext; import eu.esdihumboldt.hale.common.instance.extension.validation.PropertyConstraintValidator; import eu.esdihumboldt.hale.common.instance.extension.validation.TypeConstraintValidator; import eu.esdihumboldt.hale.common.instance.extension.validation.ValidationException; import eu.esdihumboldt.hale.common.instance.extension.validation.ValidationLocation; import eu.esdihumboldt.hale.common.instance.extension.validation.report.InstanceValidationReport; import eu.esdihumboldt.hale.common.instance.extension.validation.report.InstanceValidationReporter; import eu.esdihumboldt.hale.common.instance.extension.validation.report.impl.DefaultInstanceValidationMessage; import eu.esdihumboldt.hale.common.instance.extension.validation.report.impl.DefaultInstanceValidationReporter; import eu.esdihumboldt.hale.common.instance.model.Group; import eu.esdihumboldt.hale.common.instance.model.Instance; import eu.esdihumboldt.hale.common.instance.model.InstanceCollection; import eu.esdihumboldt.hale.common.instance.model.InstanceReference; import eu.esdihumboldt.hale.common.instance.model.MutableInstance; import eu.esdihumboldt.hale.common.instance.model.ResourceIterator; import eu.esdihumboldt.hale.common.instance.model.impl.DefaultInstance; import eu.esdihumboldt.hale.common.instancevalidator.extension.InstanceModelValidatorExtension; import eu.esdihumboldt.hale.common.instancevalidator.extension.InstanceModelValidatorFactory; import eu.esdihumboldt.hale.common.schema.SchemaSpaceID; import eu.esdihumboldt.hale.common.schema.model.ChildDefinition; import eu.esdihumboldt.hale.common.schema.model.DefinitionUtil; import eu.esdihumboldt.hale.common.schema.model.GroupPropertyConstraint; import eu.esdihumboldt.hale.common.schema.model.GroupPropertyDefinition; import eu.esdihumboldt.hale.common.schema.model.PropertyConstraint; import eu.esdihumboldt.hale.common.schema.model.PropertyDefinition; import eu.esdihumboldt.hale.common.schema.model.TypeConstraint; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.common.schema.model.constraint.ConstraintUtil; import eu.esdihumboldt.hale.common.schema.model.constraint.property.Cardinality; import eu.esdihumboldt.hale.common.schema.model.constraint.property.ChoiceFlag; import eu.esdihumboldt.hale.common.schema.model.constraint.property.NillableFlag; import eu.esdihumboldt.hale.common.schema.model.constraint.type.HasValueFlag; import eu.esdihumboldt.hale.common.schema.model.constraint.type.SkipValidation; import eu.esdihumboldt.hale.io.xsd.constraint.XmlAttributeFlag; /** * Validator for instances using constraints. * * @author Kai Schwierczek */ public class InstanceValidator { private static final ALogger log = ALoggerFactory.getLogger(InstanceValidator.class); /** * Create a default validator instance. * * @param services the service provider, if available * @return the validator instance */ public static InstanceValidator createDefaultValidator(@Nullable ServiceProvider services) { List<InstanceModelValidator> validators = new ArrayList<>(); // validators via extension for (InstanceModelValidatorFactory factory : InstanceModelValidatorExtension.getInstance() .getFactories()) { try { validators.add(factory.createExtensionObject()); } catch (Exception e) { log.error("Error instantiating instance validator " + factory.getIdentifier(), e); } } // TODO validators via service or other configuration? // inject service provider if (services != null) { for (InstanceModelValidator validator : validators) { validator.setServiceProvider(services); } } return new InstanceValidator(validators); } // XXX Data views show only warnings, if something will be changed to errors // they need an update, too. private final List<InstanceModelValidator> additionalValidators = new ArrayList<>(); /** * Create a new instance validator. * * @param validators any validators to be used in addition to constraint * validators */ public InstanceValidator(@Nullable List<InstanceModelValidator> validators) { super(); if (validators != null) { this.additionalValidators.addAll(validators); } } /** * Validates the given instances using all constraints that are validatable. * * @param instances the instances to validate * @param monitor the progress monitor * @return a report of the validation */ public InstanceValidationReport validateInstances(InstanceCollection instances, IProgressMonitor monitor) { monitor.beginTask("Instance validation", instances.hasSize() ? instances.size() : IProgressMonitor.UNKNOWN); InstanceValidationReporter reporter = new DefaultInstanceValidationReporter(false); reporter.setSuccess(false); ATransaction trans = log.begin("Instance validation"); InstanceValidationContext context = new InstanceValidationContext(); ResourceIterator<Instance> iterator = instances.iterator(); try { while (iterator.hasNext()) { if (monitor.isCanceled()) return reporter; Instance instance = iterator.next(); validateInstance(instance, reporter, instance.getDefinition().getName(), new ArrayList<QName>(), false, instances.getReference(instance), context, null, null); monitor.worked(1); } } finally { iterator.close(); trans.end(); } validateContext(context, reporter); reporter.setSuccess(true); return reporter; } /** * Validate the information collected in the instance validation context. * Should be performed after all instances haven been validated. * * @param context the validation context * @param reporter the validation reporter */ public void validateContext(InstanceValidationContext context, InstanceValidationReporter reporter) { ConstraintValidatorExtension extension = ConstraintValidatorExtension.getInstance(); for (Entry<Class<TypeConstraint>, TypeConstraintValidator> validator : extension .getTypeConstraintValidators().entrySet()) { validateContext(context, validator.getValue(), validator.getKey(), reporter); } for (Entry<Class<PropertyConstraint>, PropertyConstraintValidator> validator : extension .getPropertyConstraintValidators().entrySet()) { validateContext(context, validator.getValue(), validator.getKey(), reporter); } for (Entry<Class<GroupPropertyConstraint>, GroupPropertyConstraintValidator> validator : extension .getGroupPropertyConstraintValidators().entrySet()) { validateContext(context, validator.getValue(), validator.getKey(), reporter); } } private void validateContext(InstanceValidationContext context, ConstraintValidator validator, Class<?> constraintClass, InstanceValidationReporter reporter) { try { validator.validateContext(context, reporter); } catch (ValidationException e) { reporter.warn(new DefaultInstanceValidationMessage(null, null, Collections.<QName> emptyList(), constraintClass.getSimpleName(), e.getMessage())); } catch (Exception e) { log.error("Error performing instance validation", e); } } /** * Validates the given object. The created reports messages do not have an * {@link InstanceReference} set. * * @param object the object to validate (i. e. an instance, group or basic * value) * @param childDef the child definition of the given object * @return a report of the validation */ public InstanceValidationReporter validate(Object object, ChildDefinition<?> childDef) { InstanceValidationReporter reporter = new DefaultInstanceValidationReporter(false); reporter.setSuccess(false); InstanceValidationContext context = new InstanceValidationContext(); // first a special case for Choice-Flag // XXX a better way to do this than coding this special case? boolean onlyCheckExistingChildren = false; if (childDef.asGroup() != null) { GroupPropertyConstraintValidator validator = ConstraintValidatorExtension.getInstance() .getGroupPropertyConstraintValidators().get(ChoiceFlag.class); if (validator != null) try { validator.validateGroupPropertyConstraint(new Object[] { object }, childDef.asGroup().getConstraint(ChoiceFlag.class), childDef.asGroup(), context); } catch (ValidationException vE) { reporter.warn(new DefaultInstanceValidationMessage(null, null, Collections.<QName> emptyList(), ChoiceFlag.class.getSimpleName(), vE.getMessage())); } onlyCheckExistingChildren = childDef.asGroup().getConstraint(ChoiceFlag.class) .isEnabled(); } // then validate the object as if it were a lone property value validateChildren(new Object[] { object }, childDef, reporter, null, new ArrayList<QName>(), onlyCheckExistingChildren, null, context, null); reporter.setSuccess(true); return reporter; } /** * Validates the given {@link Instance}. The created reports messages do not * have an {@link InstanceReference} set. * * @param instance the instance to validate * @return a report of the validation */ public InstanceValidationReporter validate(Instance instance) { InstanceValidationReporter reporter = new DefaultInstanceValidationReporter(false); reporter.setSuccess(false); InstanceValidationContext context = new InstanceValidationContext(); validateInstance(instance, reporter, instance.getDefinition().getName(), new ArrayList<QName>(), false, null, context, null, null); reporter.setSuccess(true); return reporter; } /** * Validates the instances value against existing * {@link TypeConstraintValidator}s and calls * {@link #validateGroupChildren(Group, InstanceValidationReporter, QName, List, boolean, InstanceReference, InstanceValidationContext, ChildDefinition, EntityDefinition)} * . * * @param instance the instance to validate * @param reporter the reporter to report to * @param type the top level type * @param path the current property path * @param onlyCheckExistingChildren whether to only validate existing * children (in case of a choice) or not * @param reference the instance reference * @param context the instance validation context * @param presentIn the child definition this instance is present in, if * applicable * @param entity the instance entity definition or <code>null</code> */ public void validateInstance(Instance instance, InstanceValidationReporter reporter, QName type, List<QName> path, boolean onlyCheckExistingChildren, InstanceReference reference, InstanceValidationContext context, @Nullable ChildDefinition<?> presentIn, @Nullable EntityDefinition entity) { TypeDefinition typeDef = instance.getDefinition(); if (entity == null) { // if no entity is provided, use the instance type as entity entity = new TypeEntityDefinition(typeDef, SchemaSpaceID.TARGET, null); } if (skipValidation(typeDef, instance)) { return; } // type constraint validators for (Entry<Class<TypeConstraint>, TypeConstraintValidator> entry : ConstraintValidatorExtension .getInstance().getTypeConstraintValidators().entrySet()) { try { entry.getValue().validateTypeConstraint(instance, typeDef.getConstraint(entry.getKey()), context); } catch (ValidationException vE) { reporter.warn(new DefaultInstanceValidationMessage(reference, type, new ArrayList<QName>(path), entry.getKey().getSimpleName(), vE.getMessage())); } } // generic instance validators for (InstanceModelValidator validator : additionalValidators) { try { validator.validateInstance(instance, entity, context); } catch (ValidationException vE) { reporter.warn(new DefaultInstanceValidationMessage(reference, type, new ArrayList<QName>(path), validator.getCategory(), vE.getMessage())); } } validateGroupChildren(instance, reporter, type, path, onlyCheckExistingChildren, reference, context, presentIn, entity); } /** * Determines if validation should be skipped for a certain property type * and value. * * @param typeDef the property type * @param value the property value * @return if validation should be skipped for the property and its children */ protected boolean skipValidation(TypeDefinition typeDef, Object value) { SkipValidation skip = typeDef.getConstraint(SkipValidation.class); return skip.skipValidation(value); } /** * Validates the given {@link Group}'s children against the {@link Group}'s * definition. * * @param group the group to validate * @param reporter the reporter to report to * @param type the top level type * @param path the current property path * @param onlyCheckExistingChildren whether to only validate existing * children (in case of a choice) or not * @param reference the instance reference * @param context the instance validation context * @param presentIn the child definition this group is present in, if * applicable * @param groupEntity the group's entity definition or <code>null</code> */ private void validateGroupChildren(Group group, InstanceValidationReporter reporter, QName type, List<QName> path, boolean onlyCheckExistingChildren, InstanceReference reference, InstanceValidationContext context, @Nullable ChildDefinition<?> presentIn, EntityDefinition groupEntity) { Collection<? extends ChildDefinition<?>> childDefs = DefinitionUtil .getAllChildren(group.getDefinition()); // special case handling - nillable XML element with only attributes -> // check only existing children (=attributes) if (group instanceof Instance && presentIn != null && presentIn.asProperty() != null) { Instance instance = (Instance) group; if (presentIn.asProperty().getConstraint(NillableFlag.class).isEnabled() && instance.getValue() == null) { // test if all properties present are attributes boolean onlyAttributes = true; // but there must be an attribute present (otherwise we are not // sure this is XML) boolean foundAttribute = false; for (QName propertyName : group.getPropertyNames()) { ChildDefinition<?> childDef = presentIn.asProperty().getPropertyType() .getChild(propertyName); if (childDef == null || childDef.asProperty() == null || !childDef.asProperty() .getConstraint(XmlAttributeFlag.class).isEnabled()) { onlyAttributes = false; break; } else { foundAttribute = true; } } if (onlyAttributes && foundAttribute) { onlyCheckExistingChildren = true; } } } validateGroupChildren(group, childDefs, reporter, type, path, onlyCheckExistingChildren, reference, context, groupEntity); } /** * Validates the given {@link Group}'s children against the {@link Group}'s * definition. * * @param group the group to validate * @param childDefs the pre-determined children to validate (can be all * children or a subset) * @param reporter the reporter to report to * @param type the top level type * @param path the current property path * @param onlyCheckExistingChildren whether to only validate existing * children (in case of a choice) or not * @param reference the instance reference * @param context the instance validation context * @param parent the parent group's entity definition or <code>null</code> */ private void validateGroupChildren(Group group, Collection<? extends ChildDefinition<?>> childDefs, InstanceValidationReporter reporter, QName type, List<QName> path, boolean onlyCheckExistingChildren, InstanceReference reference, InstanceValidationContext context, @Nullable EntityDefinition parent) { for (ChildDefinition<?> childDef : childDefs) { QName name = childDef.getName(); path.add(name); EntityDefinition child = (parent != null) ? AlignmentUtil.getChild(parent, name) : null; // Cannot use getPropertyNames in case of onlyCheckExistingChildren, // because then I get no ChildDefinitions. Object[] property = group.getProperty(name); if (!onlyCheckExistingChildren || (property != null && property.length > 0)) { if (childDef.asGroup() != null) { validateGroup(property, childDef.asGroup(), reporter, type, path, reference, context, child); } else if (childDef.asProperty() != null) { validateProperty(property, childDef.asProperty(), reporter, type, path, reference, context, child); } else throw new IllegalStateException("Illegal child type."); } path.remove(path.size() - 1); } } /** * Validates the given property values against their * {@link PropertyDefinition}.<br> * Then calls * {@link #validateChildren(Object[], ChildDefinition, InstanceValidationReporter, QName, List, boolean, InstanceReference, InstanceValidationContext, EntityDefinition)} * . * * @param properties the array of existing properties, may be null * @param propertyDef their definition * @param reporter the reporter to report to * @param type the top level type * @param path the current property path * @param reference the instance reference * @param context the instance validation context * @param entity the property's entity definition or <code>null</code> */ @SuppressWarnings("unchecked") private void validateProperty(Object[] properties, PropertyDefinition propertyDef, InstanceValidationReporter reporter, QName type, List<QName> path, InstanceReference reference, InstanceValidationContext context, @Nullable EntityDefinition entity) { ValidationLocation loc = new ValidationLocation(reference, type, new ArrayList<QName>(path)); // property constraint validators for (Entry<Class<PropertyConstraint>, PropertyConstraintValidator> entry : ConstraintValidatorExtension .getInstance().getPropertyConstraintValidators().entrySet()) { try { entry.getValue().validatePropertyConstraint(properties, propertyDef .getConstraint((Class<? extends PropertyConstraint>) ConstraintUtil .getConstraintType(entry.getKey())), propertyDef, context, loc); } catch (ValidationException vE) { reporter.warn(new DefaultInstanceValidationMessage(loc, entry.getKey().getSimpleName(), vE.getMessage())); } } if (properties != null) { // generic validators for (InstanceModelValidator validator : additionalValidators) { for (Object value : properties) { // visit each value if (value instanceof Instance) { try { validator.validateInstance((Instance) value, entity, context); } catch (ValidationException vE) { reporter.warn(new DefaultInstanceValidationMessage(reference, type, new ArrayList<QName>(path), validator.getCategory(), vE.getMessage())); } } else { try { validator.validateProperty(value, propertyDef, entity, context); } catch (ValidationException vE) { reporter.warn(new DefaultInstanceValidationMessage(reference, type, new ArrayList<QName>(path), validator.getCategory(), vE.getMessage())); } } } } } validateChildren(properties, propertyDef, reporter, type, path, false, reference, context, entity); } /** * Validates the given property values against their * {@link GroupPropertyDefinition}.<br> * Then calls * {@link #validateChildren(Object[], ChildDefinition, InstanceValidationReporter, QName, List, boolean, InstanceReference, InstanceValidationContext, EntityDefinition)} * . * * @param properties the array of existing properties, may be null * @param groupDef their definition * @param reporter the reporter to report to * @param type the top level type * @param path the current property path * @param reference the instance reference * @param context the instance validation context * @param groupEntity the group's entity definition */ private void validateGroup(Object[] properties, GroupPropertyDefinition groupDef, InstanceValidationReporter reporter, QName type, List<QName> path, InstanceReference reference, InstanceValidationContext context, EntityDefinition groupEntity) { // group property constraints for (Entry<Class<GroupPropertyConstraint>, GroupPropertyConstraintValidator> entry : ConstraintValidatorExtension .getInstance().getGroupPropertyConstraintValidators().entrySet()) { try { entry.getValue().validateGroupPropertyConstraint(properties, groupDef.getConstraint(entry.getKey()), groupDef, context); } catch (ValidationException vE) { reporter.warn(new DefaultInstanceValidationMessage(reference, type, new ArrayList<QName>(path), entry.getKey().getSimpleName(), vE.getMessage())); } } if (properties != null) { // generic validators for (InstanceModelValidator validator : additionalValidators) { for (Object value : properties) { // visit each value if (value instanceof Group) { try { validator.validateGroup((Group) value, groupDef, groupEntity, context); } catch (ValidationException vE) { reporter.warn(new DefaultInstanceValidationMessage(reference, type, new ArrayList<QName>(path), validator.getCategory(), vE.getMessage())); } } else { log.error("Invalid value for group property, should be Group object"); } } } } // In case of enabled choice flag only check existing children. // That only one child exists should get checked above in a validator // for the choice flag. validateChildren(properties, groupDef, reporter, type, path, groupDef.getConstraint(ChoiceFlag.class).isEnabled(), reference, context, groupEntity); } /** * Validates the given property values (their values - as instances - and/or * group children). * * @param properties the array of existing properties, may be null * @param childDef their definition * @param reporter the reporter to report to * @param type the top level type * @param path the current property path * @param onlyCheckExistingChildren whether to only validate existing * children (in case of a choice) or not * @param reference the instance reference * @param context the instance validation context * @param entity the entity definition related to the property values or * <code>null</code> */ private void validateChildren(Object[] properties, ChildDefinition<?> childDef, InstanceValidationReporter reporter, QName type, List<QName> path, boolean onlyCheckExistingChildren, InstanceReference reference, InstanceValidationContext context, @Nullable EntityDefinition entity) { if (properties != null && properties.length > 0) { for (Object property : properties) { if (property instanceof Instance) { validateInstance((Instance) property, reporter, type, path, onlyCheckExistingChildren, reference, context, childDef, entity); } else if (property instanceof Group) { validateGroupChildren((Group) property, reporter, type, path, onlyCheckExistingChildren, reference, context, childDef, entity); } else { if (childDef.asGroup() != null) reporter.warn(new DefaultInstanceValidationMessage(reference, type, new ArrayList<QName>(path), "Wrong group", "A property is no group")); else if (childDef.asProperty() != null) { if (!skipValidation(childDef.asProperty().getPropertyType(), property)) { // don't skip property // wrap value in dummy instance for type validation MutableInstance instance = new DefaultInstance( childDef.asProperty().getPropertyType(), null); instance.setValue(property); validateInstance(instance, reporter, type, path, onlyCheckExistingChildren, reference, context, childDef, entity); } } } } } else { // no property value /* * Special case: No property value, but a combination of minimum * cardinality greater than zero and NillableFlag is set. Then there * can be sub-properties that are required. * * Applicable for XML (simple) types with mandatory attributes. */ if (childDef.asProperty() != null && childDef.asProperty().getConstraint(Cardinality.class).getMinOccurs() > 0 && childDef.asProperty().getConstraint(NillableFlag.class).isEnabled() && childDef.asProperty().getPropertyType().getConstraint(HasValueFlag.class) .isEnabled() && !childDef.asProperty().getPropertyType().getChildren().isEmpty()) { // collect XML attribute children List<ChildDefinition<?>> attributes = new ArrayList<ChildDefinition<?>>(); for (ChildDefinition<?> child : childDef.asProperty().getPropertyType() .getChildren()) { if (child.asProperty() != null && child.asProperty() .getConstraint(XmlAttributeFlag.class).isEnabled()) { attributes.add(child); } } if (!attributes.isEmpty()) { // create an empty dummy instance Instance instance = new DefaultInstance(childDef.asProperty().getPropertyType(), null); validateGroupChildren(instance, attributes, reporter, type, path, onlyCheckExistingChildren, reference, context, entity); } } } } }