/* * 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.writer.internal.geometry.writers; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import javax.xml.namespace.QName; import eu.esdihumboldt.hale.common.schema.model.ChildDefinition; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.io.gml.writer.internal.GmlWriterUtil; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.DefinitionPath; /** * Represents a pattern for matching an abstract path * * @author Simon Templer * @partner 01 / Fraunhofer Institute for Computer Graphics Research */ public class Pattern { /** * Pattern type */ private enum PatternType { /** combines multiple patterns with an AND relation */ AND, /** combines multiple patterns with an OR relation */ OR, /** matches a pattern */ MATCH } /** * Valid pattern element types */ private static enum ElementType { /** * Represents one XML element with any name */ ONE_ELEMENT, /** * Represents any number of XML elements with any names */ ANY_ELEMENTS, /** * Represents an XML element with a certain name */ NAMED_ELEMENT } /** * A pattern element */ private static class PatternElement { private final ElementType type; private final QName name; /** * Constructor * * @param type the element type * @param name the element name */ public PatternElement(ElementType type, QName name) { super(); this.type = type; this.name = name; } /** * @return the type */ public ElementType getType() { return type; } /** * @return the name */ public QName getName() { return name; } } private static final String ELEMENT_DELIMITER = "/"; //$NON-NLS-1$ private static final String WILDCARD_ONE = "*"; //$NON-NLS-1$ private static final String WILDCARD_ANY = "**"; //$NON-NLS-1$ private static final String NS_MARKER = "\""; //$NON-NLS-1$ /** * Placeholder for the GML namespace that may be used in patterns */ public static final String GML_NAMESPACE_PLACEHOLDER = "_____________gml_____________"; /** * Parse a pattern from the given string. Pattern elements must be separated * by <code>/</code>. Valid elements are <code>*</code> (one XML element * with any name), <code>**</code> (any number of XML elements with any * name) and an XML element name. An XML element name may also include a * namespace, the namespace must be wrapped by quotes (<code>"</code>). If * no namespace is specified the GML namespace is assumed. * * @param pattern the pattern string * * @return the parsed pattern */ public static Pattern parse(String pattern) { List<PatternElement> elements = new ArrayList<PatternElement>(); String[] parts = pattern.split(ELEMENT_DELIMITER); // TODO - * or ** after ** not allowed for (int i = 0; i < parts.length; i++) { String part = parts[i]; if (part != null && !part.trim().isEmpty()) { part = part.trim(); if (part.equals(WILDCARD_ONE)) { // one element elements.add(new PatternElement(ElementType.ONE_ELEMENT, null)); } else if (part.equals(WILDCARD_ANY)) { // any elements elements.add(new PatternElement(ElementType.ANY_ELEMENTS, null)); } else { // name or namespace + name if (part.startsWith(NS_MARKER)) { // namespace + name // check if the part contains another marker boolean isLast = part.length() > 1 && part.substring(1).contains(NS_MARKER); // add parts until name is complete while (!isLast && i < parts.length - 1) { String next = parts[++i]; isLast = next.contains(NS_MARKER); part += next; } if (!isLast) { throw new IllegalArgumentException( "No terminating namespace quote found"); //$NON-NLS-1$ } else { // separate namespace and name int index = part.lastIndexOf(NS_MARKER); String namespace = part.substring(1, index).trim(); String name = part.substring(index + 1).trim(); elements.add(new PatternElement(ElementType.NAMED_ELEMENT, new QName( namespace, name))); } } else { // element name only, assuming GML namespace elements.add(new PatternElement(ElementType.NAMED_ELEMENT, new QName( GML_NAMESPACE_PLACEHOLDER, part))); } } } } return new Pattern(PatternType.MATCH, pattern, elements, null); } /** * Create a pattern that combines multiple patterns with a logical OR. When * matching, the path of the first pattern that matches is returned. * * @param patterns the sub-patterns * * @return the OR pattern */ public static Pattern or(Pattern... patterns) { return new Pattern(PatternType.OR, null, null, Arrays.asList(patterns)); } /** * Create a pattern that combines multiple patterns with a logical AND. When * matching, all patterns must match and the path of the first pattern is * returned. * * @param patterns the sub-patterns * * @return the AND pattern */ public static Pattern and(Pattern... patterns) { return new Pattern(PatternType.AND, null, null, Arrays.asList(patterns)); } private final List<PatternElement> elements; private final List<Pattern> subPatterns; private final String patternString; private final PatternType type; /** * Constructor * * @param type the pattern type * @param patternString the pattern string * @param elements the pattern elements * @param subPatterns the sub-patterns */ private Pattern(PatternType type, String patternString, List<PatternElement> elements, List<Pattern> subPatterns) { super(); this.patternString = patternString; this.elements = elements; this.subPatterns = subPatterns; this.type = type; } /** * Matches the type against the encoding pattern. * * @param type the type definition * @param path the definition path * @param gmlNs the GML namespace * * @return the new path if there is a match, <code>null</code> otherwise */ public DefinitionPath match(TypeDefinition type, DefinitionPath path, String gmlNs) { switch (this.type) { case AND: { DefinitionPath fPath = null; for (Pattern pattern : subPatterns) { DefinitionPath res = pattern.match(type, path, gmlNs); if (res == null) { // all must match return null; } else if (fPath == null) { // remember the first path fPath = res; } } return fPath; } case OR: { for (Pattern pattern : subPatterns) { DefinitionPath res = pattern.match(type, path, gmlNs); if (res != null) { // any must match return res; } } return null; // none matched } case MATCH: default: return match(type, path, gmlNs, new HashSet<TypeDefinition>(), new LinkedList<PatternElement>(elements)); } } /** * Matches the type against the encoding pattern. * * @param type the type definition * @param path the definition path * @param gmlNs the GML namespace * @param checkedTypes the type definitions that have already been checked * (to prevent cycles) * @param remainingElements the remaining elements to match * * @return the new path if there is a match, <code>null</code> otherwise */ private static DefinitionPath match(TypeDefinition type, DefinitionPath path, String gmlNs, HashSet<TypeDefinition> checkedTypes, List<PatternElement> remainingElements) { if (remainingElements == null || remainingElements.isEmpty()) { return null; } if (checkedTypes.contains(type)) { return null; } else { checkedTypes.add(type); } PatternElement first = remainingElements.get(0); PatternElement checkAgainst; boolean allowAttributeDescent; boolean removeFirstForAttributeDescent = false; boolean allowSubtypeDescent = true; switch (first.getType()) { case ONE_ELEMENT: checkAgainst = null; // only descend allowAttributeDescent = true; removeFirstForAttributeDescent = true; // first element may not be // removed for sub-type // descent // special case: was last element if (remainingElements.size() == 1) { return path; } break; case ANY_ELEMENTS: // check against the next named element PatternElement named = null; for (int i = 1; i < remainingElements.size() && named == null; i++) { PatternElement element = remainingElements.get(i); if (element.getType().equals(ElementType.NAMED_ELEMENT)) { named = element; } } if (named == null) { // no named element return null; } else { checkAgainst = named; } allowAttributeDescent = true; break; case NAMED_ELEMENT: checkAgainst = first; // check the current allowAttributeDescent = false; // only allow sub-type descent break; default: throw new IllegalStateException("Unknown pattern element type"); //$NON-NLS-1$ } if (checkAgainst != null) { // get the last path element QName elementName = path.getLastName(); QName name = checkAgainst.getName(); // inject namespace if needed if (name.getNamespaceURI() == GML_NAMESPACE_PLACEHOLDER) { name = new QName(gmlNs, name.getLocalPart()); } // check direct match if (name.equals(elementName)) { // match for the element name -> we are on the right track int index = remainingElements.indexOf(checkAgainst); if (index == remainingElements.size() - 1) { // is last - we have a full match return path; } // remove the element (and any leading wildcards) from the queue remainingElements = remainingElements.subList(index + 1, remainingElements.size()); // for a name match, no sub-type descent is allowed allowSubtypeDescent = false; // but an attribute descent is ok allowAttributeDescent = true; } else { // no name match // sub-type descent is still allowed, don't remove element } } // descend further if (allowSubtypeDescent) { // step down sub-types // XXX now represented in choices // XXX sub-type must work through parent choice // for (SchemaElement element : type.getSubstitutions(path.getLastName())) { // DefinitionPath candidate = match( // element.getType(), // new DefinitionPath(path).addSubstitution(element), // gmlNs, // new HashSet<TypeDefinition>(checkedTypes), // new ArrayList<PatternElement>(remainingElements)); // // if (candidate != null) { // return candidate; // } // } } if (allowAttributeDescent) { if (removeFirstForAttributeDescent) { remainingElements.remove(0); } // step down properties @java.lang.SuppressWarnings("unchecked") Iterable<ChildDefinition<?>> children = (Iterable<ChildDefinition<?>>) ((path.isEmpty()) ? (type .getChildren()) : (type.getDeclaredChildren())); Iterable<DefinitionPath> childPaths = GmlWriterUtil.collectPropertyPaths(children, path, true); for (DefinitionPath childPath : childPaths) { DefinitionPath candidate = match(childPath.getLastType(), childPath, gmlNs, new HashSet<TypeDefinition>(checkedTypes), new ArrayList<PatternElement>( remainingElements)); if (candidate != null) { return candidate; } } } return null; } /** * Determines if the pattern is valid. * * @return if the pattern is valid */ public boolean isValid() { // for (PatternElement element : elements) { // if (element.getType().equals(ElementType.NAMED_ELEMENT)) { // return true; // } // } // // return false; // XXX for now assume any pattern is valid return true; } /** * @see Object#toString() */ @Override public String toString() { switch (type) { case AND: return relationString(" AND "); //$NON-NLS-1$ case OR: return relationString(" OR "); //$NON-NLS-1$ case MATCH: default: return patternString; } } private String relationString(String delimiter) { if (subPatterns == null) throw new IllegalStateException("Sub-patterns must be set for AND/OR patterns"); //$NON-NLS-1$ StringBuffer result = new StringBuffer("("); //$NON-NLS-1$ boolean first = true; for (Pattern pattern : subPatterns) { if (first) { first = false; } else { result.append(delimiter); } result.append(pattern.toString()); } result.append(")"); //$NON-NLS-1$ return result.toString(); } }