/* * #! * Ontopia OSL Schema * #- * Copyright (C) 2001 - 2014 The Ontopia Project * #- * Licensed 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 net.ontopia.topicmaps.schema.impl.osl; import java.util.Collection; import java.util.Iterator; import java.util.HashMap; import net.ontopia.utils.OntopiaRuntimeException; import net.ontopia.infoset.core.LocatorIF; import net.ontopia.topicmaps.core.AssociationIF; import net.ontopia.topicmaps.core.AssociationRoleIF; import net.ontopia.topicmaps.core.OccurrenceIF; import net.ontopia.topicmaps.core.ScopedIF; import net.ontopia.topicmaps.core.TMObjectIF; import net.ontopia.topicmaps.core.TopicIF; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.core.TopicNameIF; import net.ontopia.topicmaps.core.VariantNameIF; import net.ontopia.topicmaps.utils.TypeHierarchyUtils; import net.ontopia.topicmaps.schema.core.ConstraintIF; import net.ontopia.topicmaps.schema.core.CardinalityConstraintIF; import net.ontopia.topicmaps.schema.core.SchemaValidatorIF; import net.ontopia.topicmaps.schema.core.SchemaViolationException; import net.ontopia.topicmaps.schema.core.ValidationHandlerIF; import net.ontopia.topicmaps.schema.core.TMObjectMatcherIF; import net.ontopia.topicmaps.schema.utils.ExceptionValidationHandler; /** * INTERNAL: A schema validator that can be used to validate topic map * constructs against an OSL schema. The schema validator is bound to * a particular OSL schema. */ public class SchemaValidator implements SchemaValidatorIF { protected OSLSchema schema; protected ValidationHandlerIF handler; SchemaValidator(OSLSchema schema) { this.schema = schema; this.handler = new ExceptionValidationHandler(); } // --- SchemaValidatorIF methods public void validate(TopicIF topic) throws SchemaViolationException { // find appropriate class TopicClass klass = (TopicClass) findClass(topic, schema.getTopicClasses()); if (klass == null) { if (schema.isStrict()) handler.violation("No matching rule for topic", topic.getTopicMap(), topic, null); return; } // pick out constraint collections Collection basenamecs = klass.getAllTopicNameConstraints(); Collection occurrencecs = klass.getAllOccurrenceConstraints(); Collection rolecs = klass.getAllRoleConstraints(); // other classes Collection others = klass.getOtherClasses(); Iterator it = topic.getTypes().iterator(); while (it.hasNext()) { TopicIF tclass = (TopicIF) it.next(); if (klass.getTypeSpecification().matchType(tclass)) continue; boolean found = false; Iterator it2 = others.iterator(); while (it2.hasNext()) { TypeSpecification typespec = (TypeSpecification) it2.next(); if (typespec.matchType(tclass)) { found = true; TopicClass otherclass = (TopicClass) findClassFor(tclass, schema.getTopicClasses()); if (otherclass != null) { basenamecs.addAll(otherclass.getAllTopicNameConstraints()); occurrencecs.addAll(otherclass.getAllOccurrenceConstraints()); rolecs.addAll(otherclass.getAllRoleConstraints()); } break; } } if (!found) handler.violation("Topic instance of illegal other class", topic, tclass, null); } // characteristics validate(topic, basenamecs, topic.getTopicNames(), klass.isStrict()); validate(topic, occurrencecs, topic.getOccurrences(), klass.isStrict()); validate(topic, rolecs, topic.getRoles(), klass.isStrict()); } public void validate(TopicMapIF topicmap) throws SchemaViolationException { TypeHierarchyUtils utils = new TypeHierarchyUtils(); handler.startValidation(); // required superclass/subclass relationships Iterator it = schema.getTopicClasses().iterator(); while (it.hasNext()) { TopicClass klass = (TopicClass) it.next(); TopicClass superclass = klass.getSuperclass(); if (superclass != null) { TopicIF subtopic = getTopic(klass, topicmap); if (subtopic != null) { TopicIF supertopic = getTopic(superclass, topicmap); if (!utils.getSuperclasses(subtopic).contains(supertopic)) handler.violation("Topic class not subclass of other class, as " + "required by schema", topicmap, subtopic, null); } } } // topics it = topicmap.getTopics().iterator(); while (it.hasNext()) validate((TopicIF) it.next()); // associations it = topicmap.getAssociations().iterator(); while (it.hasNext()) validate((AssociationIF) it.next()); handler.endValidation(); } public void validate(AssociationIF association) throws SchemaViolationException { TopicMapIF tm = association.getTopicMap(); // find appropriate class AssociationClass klass = (AssociationClass) findClass(association, schema.getAssociationClasses()); if (klass == null) { if (schema.isStrict()) handler.violation("No matching rule for association", tm, association, null); return; } validateScope(tm, association, klass); // characteristics validate(association, klass.getRoleConstraints(), association.getRoles(), true); } public void setValidationHandler(ValidationHandlerIF handler) { this.handler = handler; } public ValidationHandlerIF getValidationHandler() { return handler; } // --- Internal methods protected void validate(TMObjectIF container, Collection constraints, Collection objects) throws SchemaViolationException { validate(container, constraints, objects, false); } protected void validate(TMObjectIF container, Collection constraints, Collection objects, boolean strict) throws SchemaViolationException { HashMap counts = new HashMap(); // initialize the counts Iterator it = constraints.iterator(); while (it.hasNext()) { CardinalityConstraintIF constraint = (CardinalityConstraintIF) it.next(); counts.put(constraint, new Counter()); } // now validate it = objects.iterator(); while (it.hasNext()) { TMObjectIF object = (TMObjectIF) it.next(); boolean ok = false; Iterator it2 = constraints.iterator(); while (it2.hasNext()) { CardinalityConstraintIF constraint = (CardinalityConstraintIF) it2.next(); if (constraint.matches(object)) { validate(object, constraint); ok = true; Counter counter = (Counter) counts.get(constraint); counter.count++; if (counter.count > constraint.getMaximum() && constraint.getMaximum() != CardinalityConstraintIF.INFINITY) handler.violation(counter.count + " matches to constraint (" + getRange(constraint) + " required)", container, object, constraint); // break; // not breaking means that we do implicit merging of // constraints from otherclasses by validating against *all* // constraints to see if that works. a proper semantic // analysis of the constraints merged in would be better, // but this works for now. see bug 557 for more information. } } if (!ok && strict) handler.violation("No matching rule for characteristic", container, object, null); } // check the minimum constraints it = counts.keySet().iterator(); while (it.hasNext()) { CardinalityConstraintIF constraint = (CardinalityConstraintIF) it.next(); Counter counter = (Counter) counts.get(constraint); if (counter.count < constraint.getMinimum()) handler.violation("" + counter.count + " matches to constraint; " + constraint.getMinimum() + " required", container, null, constraint); } } protected void validate(TMObjectIF object, ConstraintIF constraint) throws SchemaViolationException { if (object instanceof TopicNameIF) validate((TopicNameIF) object, (TopicNameConstraint) constraint); else if (object instanceof OccurrenceIF) validate((OccurrenceIF) object, (OccurrenceConstraint) constraint); else if (object instanceof AssociationRoleIF && constraint instanceof TopicRoleConstraint) validate((AssociationRoleIF) object, (TopicRoleConstraint) constraint); else if (object instanceof AssociationRoleIF && constraint instanceof AssociationRoleConstraint) validate((AssociationRoleIF) object, (AssociationRoleConstraint) constraint); else if (object instanceof VariantNameIF) validate((VariantNameIF) object, (VariantConstraint) constraint); else handler.violation("INTERNAL: Unknown object: " + object, null, null, constraint); } protected void validate(TopicNameIF basename, TopicNameConstraint constraint) throws SchemaViolationException { validate(basename, constraint.getVariantConstraints(), basename.getVariants()); } protected void validate(VariantNameIF variant, VariantConstraint constraint) { // all the checking is already done } protected void validate(OccurrenceIF occ, OccurrenceConstraint constraint) throws SchemaViolationException { TopicIF topic = occ.getTopic(); validateScope(topic, occ, constraint); if (constraint.getInternal() == OccurrenceConstraint.RESOURCE_INTERNAL && occ.getLocator() != null) handler.violation("Occurrence " + occ + " is not internal", topic, occ, constraint); if (constraint.getInternal() == OccurrenceConstraint.RESOURCE_EXTERNAL && occ.getValue() != null) handler.violation("Occurrence " + occ + " is not external", topic, occ, constraint); } protected void validate(AssociationRoleIF role, TopicRoleConstraint constraint) throws SchemaViolationException { // System.out.println("Validating " + occ + " against " + constraint); } protected void validate(AssociationRoleIF role, AssociationRoleConstraint constraint) throws SchemaViolationException { // validating player TopicIF player = role.getPlayer(); if (player == null) return; Iterator it = constraint.getPlayerTypes().iterator(); if (!it.hasNext()) return; // nothing was said about the type of the player, so... while (it.hasNext()) { TypeSpecification typespec = (TypeSpecification) it.next(); if (typespec.matches(player)) return; } handler.violation("Association role player of illegal type", role.getAssociation(), role, constraint); } protected void validateScope(TMObjectIF container, ScopedIF scoped, ScopedConstraintIF constraint) throws SchemaViolationException { ScopeSpecification spec = constraint.getScopeSpecification(); if (spec == null) return; // no spec, everything is allowed if (!spec.matches(scoped)) handler.violation("Scope did not match constraint", container, scoped, constraint); } protected ConstraintIF findClass(TMObjectIF object, Collection classes) { Iterator it = classes.iterator(); while (it.hasNext()) { ConstraintIF candidate = (ConstraintIF) it.next(); if (candidate.matches(object)) return candidate; } return null; } protected ConstraintIF findClass(TopicIF topic, Collection classes) { TopicClass klass = (TopicClass) findClass((TMObjectIF) topic, classes); if (klass == null) return klass; // go through and check if we ought to use a subclass instead while (!klass.getSubclasses().isEmpty()) { TopicClass prev = klass; Iterator it = klass.getSubclasses().iterator(); while (it.hasNext()) { TopicClass candidate = (TopicClass) it.next(); if (candidate.matches(topic)) { klass = candidate; break; } } if (prev == klass) break; } return klass; } protected ConstraintIF findClassFor(TopicIF tclass, Collection classes) { Iterator it = classes.iterator(); while (it.hasNext()) { TopicClass klass = (TopicClass) it.next(); TypeSpecification spec = klass.getTypeSpecification(); if (spec.matchType(tclass)) return klass; } return null; } protected TopicIF getTopic(TopicClass klass, TopicMapIF tm) { TypeSpecification spec = klass.getTypeSpecification(); if (spec == null) return null; TMObjectMatcherIF matcher = spec.getClassMatcher(); if (matcher == null) return null; if (matcher instanceof InternalTopicRefMatcher) { InternalTopicRefMatcher m = (InternalTopicRefMatcher) matcher; LocatorIF loc = tm.getStore().getBaseAddress().resolveAbsolute(m.getRelativeURI()); return (TopicIF) tm.getObjectByItemIdentifier(loc); } else if (matcher instanceof SourceLocatorMatcher) { SourceLocatorMatcher m = (SourceLocatorMatcher) matcher; return (TopicIF) tm.getObjectByItemIdentifier(m.getLocator()); } else if (matcher instanceof SubjectIndicatorMatcher) { SubjectIndicatorMatcher m = (SubjectIndicatorMatcher) matcher; return tm.getTopicBySubjectIdentifier(m.getLocator()); } else throw new OntopiaRuntimeException("INTERNAL ERROR: Illegal topic class type matcher: " + matcher); } // --- Internal helper methods private String getRange(CardinalityConstraintIF constraint) { String range = Integer.toString(constraint.getMinimum()) + "-"; if (constraint.getMaximum() == CardinalityConstraintIF.INFINITY) return range + "inf"; else return range + Integer.toString(constraint.getMaximum()); } // --- Counter class class Counter { public int count = 0; } }