/* * 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.io.gml.reader.internal.instance; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Queue; import javax.annotation.Nullable; import javax.xml.namespace.QName; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.instance.model.Group; import eu.esdihumboldt.hale.common.instance.model.Instance; import eu.esdihumboldt.hale.common.instance.model.MutableGroup; import eu.esdihumboldt.hale.common.schema.model.ChildDefinition; import eu.esdihumboldt.hale.common.schema.model.DefinitionGroup; import eu.esdihumboldt.hale.common.schema.model.DefinitionUtil; import eu.esdihumboldt.hale.common.schema.model.GroupPropertyDefinition; import eu.esdihumboldt.hale.common.schema.model.PropertyDefinition; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.common.schema.model.constraint.property.Cardinality; import eu.esdihumboldt.hale.common.schema.model.constraint.property.ChoiceFlag; import eu.esdihumboldt.hale.io.xsd.constraint.XmlAttributeFlag; /** * Utility methods regarding group handling * * @author Simon Templer */ public class GroupUtil { private static final ALogger log = ALoggerFactory.getLogger(GroupUtil.class); /** * Determine the property definition for the given property name. * * @param groups the stack of the current group objects. The topmost element * is the current group object * @param propertyName the property name * @param allowFallback states if falling back to non-strict mode is allowed * for determining the property definition * @param ignoreNamespaces if a property with a differing namespace may be * accepted * @return the group property or <code>null</code> if none is found */ static GroupProperty determineProperty(List<MutableGroup> groups, QName propertyName, boolean allowFallback, boolean ignoreNamespaces) { return determineProperty(groups, propertyName, true, allowFallback, ignoreNamespaces); } /** * Determine the property definition for the given property name. * * @param groups the stack of the current group objects. The topmost element * is the current group object * @param propertyName the property name * @param strict states if for assessing possible property definitions * strict checks regarding the structure are applied * @param allowFallback states if with strict mode being enabled, falling * back to non-strict mode is allowed (this will not be * propagated to subsequent calls) * @param ignoreNamespaces if a property with a differing namespace may be * accepted * @return the group property or <code>null</code> if none is found */ private static GroupProperty determineProperty(List<MutableGroup> groups, QName propertyName, boolean strict, boolean allowFallback, boolean ignoreNamespaces) { if (groups.isEmpty()) { return null; } // the current group final MutableGroup currentGroup = groups.get(groups.size() - 1); // the queue to collect the siblings of the current group with LinkedList<GroupPath> siblings = new LinkedList<GroupPath>(); /* * Policy: find the property as high in the hierarchy as possible * * This might lead to problems with some special schemas, e.g. if a * group is defined that allows unbounded occurrences of an element X * and the parent type allows one occurrence there will be trouble if we * have more than two or three of those elements (depending on group and * element cardinalities). * * If this really poses a problem in the practice we might need * configuration parameters to use different policies. IMHO (ST) in well * designed schemas this problem will not occur. * * This problem only arises because we read all the data from the stream * and don't know anything about what comes ahead - another possibility * could be to change this behavior where needed. */ // preferred 1: property of a parent group List<MutableGroup> keep = new ArrayList<MutableGroup>(groups); List<MutableGroup> close = new ArrayList<MutableGroup>(); // sort groups in those that must be kept and those that may be closed for (int i = keep.size() - 1; i >= 0 && allowClose(keep.get(i)); i--) { close.add(0, keep.get(i)); keep.remove(i); } if (!close.isEmpty()) { // collect parents groups List<MutableGroup> parents = new ArrayList<MutableGroup>(close); parents.remove(parents.size() - 1); // remove current group if (!keep.isEmpty()) { parents.add(0, keep.get(0)); // insert top allowed parent first // in list } int maxDescent = close.size() - 1; // prototype that is copied for each parent List<MutableGroup> stackPrototype = new ArrayList<MutableGroup>(keep); LinkedList<GroupPath> level = new LinkedList<GroupPath>(); LinkedList<GroupPath> nextLevel = new LinkedList<GroupPath>(); for (int i = 0; i < parents.size(); i++) { // add existing parent GroupPath path = new GroupPath(new ArrayList<MutableGroup>(stackPrototype), null); level.addFirst(path); GroupProperty gp = null; // check for a direct match in the group PropertyDefinition property = determineDirectProperty(parents.get(i), propertyName, strict, ignoreNamespaces); if (property != null) { gp = new GroupProperty(property, path); } if (gp == null && maxDescent >= 0) { // >= 0 because also for // maxDescent 0 we get // siblings // check the sub-properties gp = determineSubProperty(level, propertyName, nextLevel, 0, strict, ignoreNamespaces); } if (gp != null) { return gp; } // XXX remove XXX add twin of parent to next level check // (because it was ignored) // List<MutableGroup> twinParents = new ArrayList<MutableGroup>(stackPrototype); // List<DefinitionGroup> twinChildren = new ArrayList<DefinitionGroup>(); // twinChildren.add(parents.get(i).getDefinition()); // GroupPath twin = new GroupPath(twinParents, twinChildren); // nextLevel.add(twin); // prepare stack prototype for next parent if (i + 1 < parents.size()) { stackPrototype.add(parents.get(i + 1)); } // swap lists, clear nextLevel LinkedList<GroupPath> tmp = level; level = nextLevel; nextLevel = tmp; nextLevel.clear(); } siblings = level; } // preferred 2: property of the current group PropertyDefinition property = determineDirectProperty(currentGroup, propertyName, strict, ignoreNamespaces); if (property != null) { return new GroupProperty(property, new GroupPath(groups, null)); } // preferred 3: property of a sub-group, sibling group or sibling // sub-group siblings.addFirst(new GroupPath(groups, null)); // add current group // check the sub-properties GroupProperty gp = determineSubProperty(siblings, propertyName, null, -1, strict, ignoreNamespaces); if (gp != null) { return gp; } if (strict && allowFallback) { // fall-back: property in any group without validity checks // XXX though allowClose will still be strict log.warn(MessageFormat .format("Could not find valid property path for {0}, source data might be invalid regarding the source schema.", propertyName)); return determineProperty(groups, propertyName, false, false, ignoreNamespaces); } return null; } /** * Find a child definition based on the name. * * @param parent the parent type or group * @param propertyName the property name * @param ignoreNamespaces if matches with differing namespace should be * allowed * @return the child definition or <code>null</code> */ @Nullable static ChildDefinition<?> findChild(DefinitionGroup parent, QName propertyName, boolean ignoreNamespaces) { ChildDefinition<?> result = parent.getChild(propertyName); if (result == null && ignoreNamespaces) { // look for local name match for (ChildDefinition<?> candidate : DefinitionUtil.getAllChildren(parent)) { if (candidate.getName().getLocalPart().equals(propertyName.getLocalPart())) { result = candidate; break; } } } return result; } /** * Determines if a property value for the given property name may be added * to the given group and returns the corresponding property definition. * * @param group the group * @param propertyName the property name * @param strict states if additional checks are applied apart from whether * the property exists * @param ignoreNamespaces if a property with a differing namespace may be * accepted * @return the property definition or <code>null</code> if none is found or * no value may be added */ private static PropertyDefinition determineDirectProperty(MutableGroup group, QName propertyName, boolean strict, boolean ignoreNamespaces) { ChildDefinition<?> child = findChild(group.getDefinition(), propertyName, ignoreNamespaces); if (child != null && child.asProperty() != null && (!strict || allowAdd(group, null, child.asProperty().getName()))) { return child.asProperty(); } return null; } /** * Determine the property definition for the given property name in * sub-groups of the given group stack. * * @param paths the group paths whose children shall be checked for the * property * @param propertyName the property name * @param leafs the queue is populated with the leafs in the explored * definition group tree that are not processed because of the * max descent, may be <code>null</code> if no population is * needed * @param maxDescent the maximum descent, -1 for no maximum descent * @param strict states if additional checks are applied apart from whether * the property exists * @param ignoreNamespaces if a property with a differing namespace may be * accepted * @return the property definition or <code>null</code> if none is found */ private static GroupProperty determineSubProperty(Queue<GroupPath> paths, QName propertyName, Queue<GroupPath> leafs, int maxDescent, boolean strict, boolean ignoreNamespaces) { if (maxDescent != -1 && maxDescent < 0) { return null; } while (!paths.isEmpty()) { GroupPath path = paths.poll(); DefinitionGroup lastDef = null; if (path.getChildren() != null && !path.getChildren().isEmpty()) { // check if path is a valid result if (path.allowAdd(propertyName, strict, ignoreNamespaces)) { ChildDefinition<?> property = findChild(path.getLastDefinition(), propertyName, ignoreNamespaces); if (property != null && property.asProperty() != null) { // return group property return new GroupProperty(property.asProperty(), path); } else { log.error("False positive for property candidate."); } } lastDef = path.getLastDefinition(); } else { // the first path which must not be checked, just the children // must be added to the queue List<MutableGroup> parents = path.getParents(); if (parents != null && !parents.isEmpty()) { lastDef = parents.get(parents.size() - 1).getDefinition(); } } if (lastDef != null) { // add children to queue Collection<? extends ChildDefinition<?>> children = DefinitionUtil .getAllChildren(lastDef); for (ChildDefinition<?> child : children) { if (child.asGroup() != null && (path.getChildren() == null || !path.getChildren().contains( child.asGroup()))) { // (check for // definition cycle) List<DefinitionGroup> childDefs = new ArrayList<DefinitionGroup>(); if (path.getChildren() != null) { childDefs.addAll(path.getChildren()); } childDefs.add(child.asGroup()); GroupPath newPath = new GroupPath(path.getParents(), childDefs); // check if path is valid if (!strict || newPath.isValid()) { // check max descent if (maxDescent >= 0 && newPath.getChildren().size() > maxDescent) { if (leafs != null) { leafs.add(newPath); } } else { paths.add(newPath); } } } } } } return null; } /** * Determines if the given group is valid and may be closed * * @param currentGroup the current group * @return if the group may be closed */ private static boolean allowClose(MutableGroup currentGroup) { if (currentGroup instanceof Instance) { return false; // instances may never be closed, they have no parent // in the group stack } if (currentGroup.getDefinition() instanceof GroupPropertyDefinition && ((GroupPropertyDefinition) currentGroup.getDefinition()).getConstraint( ChoiceFlag.class).isEnabled()) { // group is a choice Iterator<QName> it = currentGroup.getPropertyNames().iterator(); if (it.hasNext()) { // choice has at least on value set -> check cardinality for the // corresponding property QName name = it.next(); return isValidCardinality(currentGroup, currentGroup.getDefinition().getChild(name)); } // else check all children like below } // determine all children Collection<? extends ChildDefinition<?>> children = DefinitionUtil .getAllChildren(currentGroup.getDefinition()); // check cardinality of children for (ChildDefinition<?> childDef : children) { if (isValidCardinality(currentGroup, childDef)) { // XXX is this // correct?! // should it be // !isValid... // instead? return false; } } return true; } /** * Determines if a child is contained in a given group with a valid minimum * cardinality. * * @param group the group * @param childDef the child definition * @return if the minimum cardinality of the child definition is matched in * the group */ static boolean isValidCardinality(Group group, ChildDefinition<?> childDef) { Cardinality cardinality = null; if (childDef.asProperty() != null) { cardinality = childDef.asProperty().getConstraint(Cardinality.class); } else if (childDef.asGroup() != null) { cardinality = childDef.asGroup().getConstraint(Cardinality.class); } else { log.error("Unrecognized child definition."); } if (cardinality != null) { // check minimum long min = cardinality.getMinOccurs(); if (min > 0 && min != Cardinality.UNBOUNDED) { Object[] values = group.getProperty(childDef.getName()); int count = (values == null) ? (0) : (values.length); if (min > count) { return false; } } } return true; } /** * Determines if another value of the given property may be added to the * given group. * * @param group the group, <code>null</code> represents an empty group * @param groupDef the definition of the given group, may be * <code>null</code> if the group is not <code>null</code> * @param propertyName the property name * @return if another property value may be added to the group */ @SuppressWarnings("null") static boolean allowAdd(Group group, DefinitionGroup groupDef, QName propertyName) { if (group == null && groupDef == null) { throw new IllegalArgumentException(); } final DefinitionGroup def; if (groupDef == null) { def = group.getDefinition(); } else { def = groupDef; } if (group == null) { // create an empty dummy group if none is specified group = new Group() { @Override public Object[] getProperty(QName propertyName) { return null; } @Override public Iterable<QName> getPropertyNames() { return Collections.emptyList(); } @Override public DefinitionGroup getDefinition() { return def; } }; } if (def instanceof GroupPropertyDefinition) { // group property GroupPropertyDefinition gpdef = (GroupPropertyDefinition) def; if (gpdef.getConstraint(ChoiceFlag.class).isEnabled()) { // choice // a choice may only contain one of its properties for (QName pName : group.getPropertyNames()) { if (!pName.equals(propertyName)) { // other property is present -> may not add property // value return false; } } // check cardinality return allowAddCheckCardinality(group, propertyName); } else { // sequence, group(, attributeGroup) // check order if (!allowAddCheckOrder(group, propertyName, def)) { return false; } // check cardinality return allowAddCheckCardinality(group, propertyName); } } else if (def instanceof TypeDefinition) { // type TypeDefinition typeDef = (TypeDefinition) def; // check order if (!allowAddCheckOrder(group, propertyName, typeDef)) { return false; } // check cardinality return allowAddCheckCardinality(group, propertyName); } return false; } /** * Determines if another value of the given property may be added to the * given group based on values available in the group and the order of the * child definitions in the given definition group. * * @param group the group, <code>null</code> represents an empty group * @param propertyName the property name * @param groupDef the definition group * @return if another property value may be added to the group based on the * values and the child definition order */ private static boolean allowAddCheckOrder(Group group, QName propertyName, final DefinitionGroup groupDef) { boolean before = true; Collection<? extends ChildDefinition<?>> children = DefinitionUtil.getAllChildren(groupDef); for (ChildDefinition<?> childDef : children) { if (childDef.getName().equals(propertyName)) { before = false; } else { // ignore XML attributes if (childDef.asProperty() != null && childDef.asProperty().getConstraint(XmlAttributeFlag.class).isEnabled()) { continue; } // ignore groups that contain no elements if (childDef.asGroup() != null && !StreamGmlHelper.hasElements(childDef.asGroup())) { continue; } if (before) { // child before the property // the property may only be added if all children before are // valid in their cardinality if (!isValidCardinality(group, childDef)) { return false; } } else { // child after the property // the property may only be added if there are no values for // children after the property Object[] values = group.getProperty(childDef.getName()); if (values != null && values.length > 0) { return false; } } } } // no fail -> allow add return true; } /** * Determines if another value of the given property may be added to the * given group based on the cardinality of the property. * * @param group the group * @param propertyName the property name * @return if another property value may be added to the group based on the * property cardinality */ private static boolean allowAddCheckCardinality(Group group, QName propertyName) { ChildDefinition<?> child = group.getDefinition().getChild(propertyName); Cardinality cardinality = DefinitionUtil.getCardinality(child); // check maximum long max = cardinality.getMaxOccurs(); if (max == Cardinality.UNBOUNDED) { return true; // add allowed in any case } else if (max <= 0) { return false; // add never allowed } Object[] values = group.getProperty(propertyName); if (values == null) { return true; // allowed because max is 1 or more } return values.length < max; } }