/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2005-2011, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.data.complex.filter; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Scanner; import java.util.logging.Logger; import javax.xml.XMLConstants; import javax.xml.namespace.QName; import org.geotools.data.complex.ComplexFeatureConstants; import org.geotools.feature.type.Types; import org.geotools.util.CheckedArrayList; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.Name; import org.opengis.util.Cloneable; import org.xml.sax.helpers.NamespaceSupport; /** * Utility class to evaluate XPath expressions against an Attribute instance, which may be any * Attribute, whether it is simple, complex, a feature, etc. * <p> * At the difference of the Filter subsystem, which works against Attribute contents (for example to * evaluate a comparison filter), the XPath subsystem, for which this class is the single entry * point, works against Attribute instances. That is, the result of an XPath expression, if a single * value, is an Attribtue, not the attribute content, or a List of Attributes, for instance. * </p> * * @author Gabriel Roldan (Axios Engineering) * @author Rini Angreani (CSIRO Earth Science and Resource Engineering) * @version $Id$ * */ public class XPathUtil { private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(XPathUtil.class .getPackage().getName()); public static class StepList extends CheckedArrayList<Step> { private static final long serialVersionUID = -5612786286175355862L; private StepList() { super(XPathUtil.Step.class); } public StepList(StepList steps) { super(XPathUtil.Step.class); addAll(steps); } public String toString() { StringBuffer sb = new StringBuffer(); for (Iterator<Step> it = iterator(); it.hasNext();) { Step s = (Step) it.next(); sb.append(s.toString()); if (it.hasNext()) { sb.append("/"); } } return sb.toString(); } public boolean containsPredicate() { for (int i=0; i< size(); i++) { if (get(i).getPredicate() != null) { return true; } } return false; } public boolean startsWith(StepList other) { if (other.size() > this.size()) { return false; } for (int i = 0; i < other.size(); i++) { Step thisStep = this.get(i); Step otherStep = other.get(i); if (thisStep.isIndexed && otherStep.isIndexed) { return thisStep.equals(otherStep); } else { return thisStep.equalsIgnoreIndex(otherStep); } } return true; } public StepList subList(int fromIndex, int toIndex) { if (fromIndex < 0) throw new IndexOutOfBoundsException("fromIndex = " + fromIndex); if (toIndex > size()) throw new IndexOutOfBoundsException("toIndex = " + toIndex); if (fromIndex > toIndex) throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); StepList subList = new StepList(); for (int i = fromIndex; i < toIndex; i++) { subList.add(this.get(i)); } return subList; } public StepList clone() { StepList copy = new StepList(); for (Step step : this) { copy.add((Step) step.clone()); } return copy; } /** * Compares this StepList with another for equivalence regardless of the indexes of each * Step. * * @param propertyName * @return <code>true</code> if this step list has the same location paths than * <code>propertyName</code> ignoring the indexes in each step. <code>false</code> * otherwise. */ public boolean equalsIgnoreIndex(final StepList propertyName) { if (propertyName == null) { return false; } if (propertyName == this) { return true; } if (size() != propertyName.size()) { return false; } Iterator mine = iterator(); Iterator him = propertyName.iterator(); Step myStep; Step hisStep; while (mine.hasNext()) { myStep = (Step) mine.next(); hisStep = (Step) him.next(); if (!myStep.equalsIgnoreIndex(hisStep)) { return false; } } return true; } /** * Find the first steps matching the xpath within this list, and set an index to it. * * @param index * the new index for the matching steps * @param xpath * the xpath to be searched */ public void setIndex(int index, String xpath, String separator) { if (this.toString().contains(xpath)) { for (int i = 0; i < size() - 1; i++) { String firstString = get(i).toString(); if (xpath.equals(firstString)) { get(i).setIndex(index); return; } if (xpath.startsWith(firstString)) { StringBuffer buf = new StringBuffer(firstString); buf.append(separator); for (int j = i + 1; j < size() - 1; j++) { buf.append(get(j).toString()); if (buf.toString().equals(xpath)) { get(j).setIndex(index); return; } buf.append(separator); } } } } } } /** * * @author Gabriel Roldan * */ public static class Step implements Cloneable { private int index; private String predicate = null; private QName attributeName; private boolean isXmlAttribute; private boolean isIndexed; /** * Creates a "property" xpath step (i.e. isXmlAttribute() == false). * * @param name * @param index */ public Step(final QName name, final int index) { this(name, index, false, false); } /** * Creates an xpath step for the given qualified name and index; and the given flag to * indicate if it it an "attribute" or "property" step. * * @param name * the qualified name of the step (name should include prefix to be reflected in * toString()) * @param index * the index (indexing starts at 1 for Xpath) of the step * @param isXmlAttribute * whether the step referers to an "attribute" or a "property" (like for * attributes and elements in xml) * @throws NullPointerException * if <code>name==null</code> * @throws IllegalArgumentException * if <code>index < 1</code> */ public Step(final QName name, final int index, boolean isXmlAttribute) { this(name, index, isXmlAttribute, false); } /** * Creates an xpath step for the given qualified name and index; and the given flag to * indicate if it it an "attribute" or "property" step. * * @param name * the qualified name of the step (name should include prefix to be reflected in * toString()) * @param index * the index (indexing starts at 1 for Xpath) of the step * @param isXmlAttribute * whether the step referers to an "attribute" or a "property" (like for * attributes and elements in xml) * @param isIndexed * whether or not the index is to be shown in the string representation even if * index = 1 * @throws NullPointerException * if <code>name==null</code> * @throws IllegalArgumentException * if <code>index < 1</code> */ public Step(final QName name, final int index, boolean isXmlAttribute, boolean isIndexed) { if (name == null) { throw new NullPointerException("name"); } if (index < 1) { throw new IllegalArgumentException("index shall be >= 1"); } this.attributeName = name; this.index = index; this.isXmlAttribute = isXmlAttribute; this.isIndexed = isIndexed; } public Step(final QName name, boolean isXmlAttribute, final String predicate) { if (name == null) { throw new NullPointerException("name"); } this.attributeName = name; this.index = 1; this.isIndexed = false; this.isXmlAttribute = isXmlAttribute; this.predicate = predicate; } /** * Compares this Step with another for equivalence ignoring the steps indexes. * * @param hisStep * @return */ public boolean equalsIgnoreIndex(Step other) { if (other == null) { return false; } if (other == this) { return true; } return attributeName.equals(other.attributeName) && isXmlAttribute == other.isXmlAttribute; } public int getIndex() { return index; } public String getPredicate() { return predicate; } public boolean isIndexed() { return isIndexed; } public QName getName() { return attributeName; } public String toString() { StringBuffer sb = new StringBuffer(isXmlAttribute ? "@" : ""); if (XMLConstants.DEFAULT_NS_PREFIX != attributeName.getPrefix()) { sb.append(attributeName.getPrefix()).append(':'); } sb.append(attributeName.getLocalPart()); if (isIndexed) { // we want to print index = 1 as well if specified // so filtering on the first index doesn't return all // e.g. gml:name[1] doesn't get translated to // gml:name i.e. all gml:name instances sb.append("[").append(index).append("]"); } else if (predicate != null) { sb.append("[").append(predicate).append("]"); } return sb.toString(); } public boolean equals(Object o) { if (!(o instanceof Step)) { return false; } Step s = (Step) o; return attributeName.equals(s.attributeName) && index == s.index && isXmlAttribute == s.isXmlAttribute && predicate == s.predicate; } public int hashCode() { return 17 * attributeName.hashCode() + 37 * index; } public Step clone() { return predicate==null? new Step(this.attributeName, this.index, this.isXmlAttribute, this.isIndexed): new Step(this.attributeName, this.isXmlAttribute, this.predicate); } /** * Flag that indicates that this single step refers to an "attribute" rather than a * "property". * <p> * I.e. it was created from the last step of an expression like * <code>foo/bar@attribute</code>. * </p> * * @return */ public boolean isXmlAttribute() { return isXmlAttribute; } public void setIndex(int index) { this.index = index; isIndexed = true; } } /** * Split X-path string in to string steps, ignoring / characters inside [] * * @param s x-path string * @return list of string steps */ private static List<String> splitPath(String s) { ArrayList<String> parts = new ArrayList<String>(); StringBuffer b = new StringBuffer(); int insideIndex = 0; for (int pos = 0 ; pos < s.length() ; pos++) { if (s.charAt(pos) == '/' && insideIndex==0) { parts.add(b.toString()); b = new StringBuffer(); } else { if (s.charAt(pos) == '[') { insideIndex++; } else if (s.charAt(pos) == ']') { insideIndex--; } b.append(s.charAt(pos)); } } parts.add(b.toString()); return parts; } /** * Returns the list of steps in an x-path expression that represents the root element. * * @param root * non null descriptor of the root attribute, generally the Feature descriptor. * @param namespaces * namespace support for generating qnames from namespaces. * @return A list of unique of steps in an xpath expression. * @throws IllegalArgumentException * if <code>root</code> is undefined. */ public static StepList rootElementSteps(final AttributeDescriptor rootElement, final NamespaceSupport namespaces) throws IllegalArgumentException { if (rootElement == null) { throw new NullPointerException("root"); } StepList steps = new StepList(); QName qName = Types.toQName(rootElement.getName(), namespaces); steps.add(new Step(qName, 1, false, false)); return steps; } /** * Returns the list of stepts in <code>xpathExpression</code> by cleaning it up removing * unnecessary elements. * <p> * </p> * * @param root * non null descriptor of the root attribute, generally the Feature descriptor. Used * to ignore the first step in xpathExpression if the expression's first step is * named as rootName. * * @param xpathExpression * @return * @throws IllegalArgumentException * if <code>xpathExpression</code> has no steps or it isn't a valid XPath expression * against <code>type</code>. */ public static StepList steps(final AttributeDescriptor root, final String xpathExpression, final NamespaceSupport namespaces) throws IllegalArgumentException { if (root == null) { throw new NullPointerException("root"); } if (xpathExpression == null) { throw new NullPointerException("xpathExpression"); } String expression = xpathExpression.trim(); if ("".equals(expression)) { throw new IllegalArgumentException("expression is empty"); } StepList steps = new StepList(); if ("/".equals(expression)) { expression = root.getName().getLocalPart(); } if (expression.startsWith("/")) { expression = expression.substring(1); } final List<String> partialSteps = splitPath(expression); if (partialSteps.size() == 0) { throw new IllegalArgumentException("no steps provided"); } int startIndex = 0; for (int i = startIndex; i < partialSteps.size(); i++) { String step = partialSteps.get(i); if ("..".equals(step)) { steps.remove(steps.size() - 1); } else if (".".equals(step)) { continue; } else { int index = 1; boolean isXmlAttribute = false; boolean isIndexed = false; String predicate = null; String stepName = step; if (step.indexOf('[') != -1) { int start = step.indexOf('['); int end = step.indexOf(']'); stepName = step.substring(0, start); String s = step.substring(start + 1, end); Scanner scanner = new Scanner(s); if (scanner.hasNextInt()) { index = scanner.nextInt(); isIndexed = true; } else { predicate = s; } } if (step.charAt(0) == '@') { isXmlAttribute = true; stepName = stepName.substring(1); } QName qName = deglose(stepName, root, namespaces, isXmlAttribute); if (predicate == null) { steps.add(new Step(qName, index, isXmlAttribute, isIndexed)); } else { steps.add(new Step(qName, isXmlAttribute, predicate)); } } // // if (step.indexOf('[') != -1) { // int start = step.indexOf('['); // int end = step.indexOf(']'); // String stepName = step.substring(0, start); // int stepIndex = Integer.parseInt(step.substring(start + 1, end)); // QName qName = deglose(stepName, root, namespaces); // steps.add(new Step(qName, stepIndex)); // } else if ("..".equals(step)) { // steps.remove(steps.size() - 1); // } else if (".".equals(step)) { // continue; // } else { // QName qName = deglose(step, root, namespaces); // steps.add(new Step(qName, 1)); // } } // XPath simplification phase: if the xpath expression contains more // nodes // than the root node itself, and the root node is present, remove the // root // node as it is redundant if (root != null && steps.size() > 1) { Step step = (Step) steps.get(0); Name rootName = root.getName(); QName stepName = step.getName(); if (Types.equals(rootName, stepName)) { LOGGER.fine("removing root name from xpath " + steps + " as it is redundant"); steps.remove(0); } } return steps; } private static QName deglose(final String prefixedName, final AttributeDescriptor root, final NamespaceSupport namespaces, boolean isXmlAttribute) { if (prefixedName == null) { throw new NullPointerException("prefixedName"); } QName name = null; String prefix; final String namespaceUri; final String localName; int prefixIdx = prefixedName.indexOf(':'); if (prefixIdx == -1) { localName = prefixedName; final Name rootName = root.getName(); // don't use default namespace for client properties (xml attribute), and FEATURE_LINK final String defaultNamespace = (isXmlAttribute || localName.equals(ComplexFeatureConstants.FEATURE_CHAINING_LINK_NAME .getLocalPart()) || rootName.getNamespaceURI() == null) ? XMLConstants.NULL_NS_URI : namespaces.getURI("") == null ? rootName.getNamespaceURI() : namespaces .getURI(""); namespaceUri = defaultNamespace; if (XMLConstants.NULL_NS_URI.equals(defaultNamespace)) { prefix = XMLConstants.DEFAULT_NS_PREFIX; } else { if (!localName.equals(rootName.getLocalPart())) { LOGGER.fine("Using root's namespace " + defaultNamespace + " for step named '" + localName + "', as no prefix was stated"); } prefix = namespaces.getPrefix(defaultNamespace); if (prefix == null) { //throw new IllegalStateException("Default namespace is not mapped to a prefix: " // + defaultNamespace); prefix = ""; } } } else { prefix = prefixedName.substring(0, prefixIdx); localName = prefixedName.substring(prefixIdx + 1); namespaceUri = namespaces.getURI(prefix); } name = new QName(namespaceUri, localName, prefix); return name; } public static boolean equals(Name targetNodeName, StepList targetXPath) { if (targetXPath.size() == 1) { Step rootStep = targetXPath.get(0); QName stepName = rootStep.getName(); if (Types.equals(targetNodeName, stepName)) { return true; } } return false; } }