/********************************************************************** * Copyright (c) 2005-2009 ant4eclipse project team. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nils Hartmann, Daniel Kasmeroglu, Gerd Wuetherich **********************************************************************/ package org.ant4eclipse.lib.core.xquery; import org.ant4eclipse.lib.core.CoreExceptionCode; import org.ant4eclipse.lib.core.exception.Ant4EclipseException; import org.xml.sax.Attributes; import java.util.Arrays; import java.util.Vector; /** * This object stores a simple query used to access XML content. These queries will be visited by the SAXParser, so they * can collect their values. The query will be visited each time the ContentHandler of the SAXParser processes an * element. The query ignores these calls in case it is related to a higher level within the XML hierarchie or it has * been abandoned within a higher level. * * @techres [03-Feb-2006:KASI] Only one indexed element is allowed within a query. * * @author Daniel Kasmeroglu (daniel.kasmeroglu@kasisoft.net) * * @todo [28-Jun-2009:KASI] Check if there's a way to replace this using the xpath querying provided with update jdks. */ public class XQuery { // the complete path in fragments, each fragment indicates the next deeper // level private String[] _splitted; // a corresponding list of conditions private Condition[] _conditions; // the attribute name in case we're handling an attribute private String _attribute; // the counter is used in case there's a reference to a n-th element private int[] _counter; // the result may be a String or a Vector instance private Vector<String> _values; // keeps the depth which is acceptable for this query private int _accept; // a flag which indicates that some data has to be collected. this // is necessary because element data will be set within the call // of the method 'endVisit' private boolean _matched; // a depth level from which we're willing to generate default values. // this means that an 'unsatisfied' query forces to generate a null value // entry. private int _forcedepth; // the original query private String _xquery; private String _fileName; /** * Initializes this instance with the supplied query. * * @param query * A query used to retrieve XML data. */ XQuery(String fileName, String query) { this._fileName = fileName; this._attribute = null; this._forcedepth = -1; this._xquery = query; if (!query.startsWith("/")) { invalid(query, "Query needs to starts with a slash !"); } // reduce the first two slashes for the root indication query = query.substring(1); // create the single fragments this._splitted = query.split("/"); // check if we're querying an attribute if (this._splitted[this._splitted.length - 1].startsWith("@")) { String[] temp = new String[this._splitted.length - 1]; this._attribute = this._splitted[this._splitted.length - 1].substring(1); System.arraycopy(this._splitted, 0, temp, 0, temp.length); this._splitted = temp; } // try to check for a forced depth for (int i = this._splitted.length - 1; i >= 0; i--) { if (this._splitted[i].startsWith("{")) { if (!this._splitted[i].endsWith("}")) { invalid(query, "Element opened with '{' lacks a corresponding '}' brace."); } this._forcedepth = i; this._splitted[i] = this._splitted[i].substring(1, this._splitted[i].length() - 1); break; } } // strip conditions this._conditions = new Condition[this._splitted.length]; this._counter = new int[this._splitted.length]; Arrays.fill(this._conditions, null); for (int i = 0; i < this._splitted.length; i++) { int idx1 = this._splitted[i].indexOf('['); if (idx1 != -1) { int idx2 = this._splitted[i].indexOf(']'); if (idx2 == -1) { invalid(query, "Condition openend with '[' lacks a corresponding ']' brace."); } this._conditions[i] = createCondition(query, this._splitted[i].substring(idx1 + 1, idx2), i); this._splitted[i] = this._splitted[i].substring(0, idx1); } } reset(); } /** * Creates a condition instance from a condition string. * * @param query * The query which has been used. * @param condition * The condition which shall be translated into a Condition instance. * @param depth * The depth where the newly created condition will be used. * * @return The Condition instance which will be used while evaluating. */ private Condition createCondition(String query, String condition, int depth) { try { // indexed element return new IndexCompare(Integer.parseInt(condition), depth); } catch (NumberFormatException ex) { // create a count-function used to calculate the number of occurrences if ("count()".equals(condition)) { return new CounterFunction(depth); } // this must be an attributed expression if (!condition.startsWith("@")) { /** * @note [03-Feb-2006:KASI] We're not checking if the attribute is on the right side. */ invalid(query, "An attributed expression must start with a '@' character."); } // we're having an attribute based expression. currently only // the equality operator is supported int idx = condition.indexOf('='); if (idx == -1) { invalid(query, "Only '=' operations are supported within an attributed expression."); } // strip the testvalue and do the comparison String attrname = condition.substring(1, idx); int idx1 = condition.indexOf('\''); int idx2 = condition.indexOf('\'', idx1 + 1); if ((idx1 == -1) || (idx2 == -1)) { invalid(query, "The attributed expression doesn't contain a comparison value."); } String testvalue = condition.substring(idx1 + 1, idx2); return new StringCompare(attrname, testvalue); } } /** * Checks whether the current element matches this query or not. * * @param element * The element that shall be tested. * @param attrs * The currently used attributes in case we need to check an optional condition. * * @return true <=> The element is acceptable for this query. */ private boolean matches(String element, Attributes attrs) { // there's a named element, so check it if ((!"*".equals(this._splitted[this._accept])) && (!element.equals(this._splitted[this._accept]))) { // it doesn't apply to this query, so leave here return false; } // check if the condition applies to the current state if (this._conditions[this._accept] != null) { return this._conditions[this._accept].check(element, attrs); } // the current state is supported by this query return true; } /** * Modifies the counters. * * @param depth * The depth of the current counter. * @param element * The currently used element. */ private void adjustCounter(int depth, String element) { if (depth < this._splitted.length) { if (element.equals(this._splitted[depth])) { this._counter[depth] = this._counter[depth] + 1; for (int i = depth + 1; i < this._splitted.length; i++) { this._counter[i] = 0; } } } } /** * This function will be called whenever a new element has been entered. * * @param depth * The current depth within the XML document. * @param element * The name of the current element. * @param attrs * The attributes associated with this element. */ void visit(int depth, String element, Attributes attrs) { if (depth >= this._splitted.length) { // this element is to deep for this query return; } // modify the element counters adjustCounter(depth, element); if (depth == this._accept) { // this might be a candidate for checking if (matches(element, attrs)) { // mark the requirement to generate a value if ((depth == this._forcedepth) && (this._attribute == null)) { this._matched = true; } // all conditions met, so we can increase the depth // to prepare for the next level this._accept++; // it was the last part of the query, so fetch the attribute // if this query is used for an attribute if (depth == (this._splitted.length - 1)) { if (this._attribute != null) { addValue(attrs.getValue(this._attribute)); } else { if (this._forcedepth == -1) { this._matched = true; } } } } } } /** * The element has been left. * * @param depth * The current depth within the XML document. * @param content * The trimmed text content of the XML element. */ void endVisit(int depth, String content) { if (this._accept == (depth + 1)) { if (this._matched) { if (depth == this._splitted.length - 1) { // nice, the query could be satisfied addValue(content); this._matched = false; } else if (depth == this._forcedepth) { // no match for this query, but we're advised to // create a default value addValue(null); this._matched = false; } } this._accept--; } } /** * Prepare this query for another XML document. */ void reset() { this._values = null; this._accept = 0; Arrays.fill(this._counter, 0); this._matched = false; } /** * Adds the supplied value to this query. * * @param newvalue * The value that shall be added. */ private void addValue(String newvalue) { if (this._values == null) { this._values = new Vector<String>(); } this._values.add(newvalue); } /** * Returns the data collected by this query. * * @return The data collected by this query (Vector{String}) */ public String[] getResult() { if (this._values == null) { return new String[0]; } String[] result = new String[this._values.size()]; this._values.toArray(result); return result; } /** * Returns the data collected by this query. If no or one result were found, this method returns null or the result. * If more than one result were found, an XQueryException will be thrown. * * @return The data collected by this query (String) */ public String getSingleResult() { String[] results = getResult(); if (results.length > 1) { throw new Ant4EclipseException(CoreExceptionCode.X_QUERY_DUCPLICATE_ENTRY_EXCEPTION, this._xquery, this._fileName); } if (results.length == 0) { return null; } return results[0]; } /** * Returns true if at least one result is available. * * @return true <=> At least one result is available. */ public boolean gotResult() { return this._values != null; } /** * Returns the counter for the supplied level. * * @param depth * The depth which counter will be returned. * * @return The depth of the supplied level. */ private int counter(int depth) { // -1 because the method 'adjustCounter' will be called before // the use of this function. return this._counter[depth] - 1; } /** * {@inheritDoc} */ @Override public String toString() { return this._xquery; } /** * Creates an exception indicating that the query is invalid. * * @param query * The invalid query. * @param msg * An error message. */ private void invalid(String query, String msg) { throw new Ant4EclipseException(CoreExceptionCode.X_QUERY_INVALID_QUERY_EXCEPTION, query, msg); } /** * A Condition used to test a query expression. */ private interface Condition { /** * Tests a query expression using the supplied attributes. * * @param element * The element name. * @param attrs * The attributes which has to be used for the test. * * @return true <=> The condition is met. */ boolean check(String element, Attributes attrs); } /* ENDINTERFACE */ /** * Condition implementation which checks for a specific element level. */ private class IndexCompare implements Condition { private int level; private int index; /** * Initializes this condition using the supplied element level. * * @param lvlindex * The element count used for the check. * @param cnt * The index used for the comparison. */ public IndexCompare(int lvlindex, int cnt) { this.level = lvlindex; this.index = cnt; } /** * {@inheritDoc} */ public boolean check(String element, Attributes attrs) { return counter(this.index) == this.level; } } /* ENDCLASS */ /** * Implements a simple function that generates the values. */ private class CounterFunction implements Condition { private int index; /** * Initializes this counter. * * @param idx * The index of the counter that shall be used. */ public CounterFunction(int idx) { this.index = idx; } /** * {@inheritDoc} */ public boolean check(String element, Attributes attrs) { if (counter(this.index) == 0) { // create a new entry addValue("0"); } // increment the current value Vector<String> values = XQuery.this._values; String lastval = values.get(values.size() - 1); int value = Integer.parseInt(lastval); values.set(values.size() - 1, String.valueOf(value + 1)); // function evaluation, so we don't need to process anything else return false; } } /* ENDCLASS */ /** * Condition implementation which checks for a specific attribute value. */ private class StringCompare implements Condition { private String attr; private String value; /** * Initializes this string comparison condition. * * @param attrname * The name of the attribute which has to be tested. * @param expected * The value of the expected attribute value. */ public StringCompare(String attrname, String expected) { this.attr = attrname; this.value = expected; } /** * {@inheritDoc} */ public boolean check(String element, Attributes attrs) { return this.value.equals(attrs.getValue(this.attr)); } } /* ENDCLASS */ } /* ENDCLASS */