/* * eXist Open Source Native XML Database * Copyright (C) 2013 The eXist Project * http://exist-db.org * * This program 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; either version 2 * of the License, or (at your option) any later version. * * This program 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. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * $Id$ */ package org.exist.indexing.range; import org.exist.dom.QName; import org.exist.storage.ElementValue; import org.exist.storage.NodePath; import org.exist.util.DatabaseConfigurationException; import org.exist.xquery.*; import org.exist.xquery.modules.range.RangeQueryRewriter; import org.exist.indexing.range.RangeIndex.Operator; import org.exist.xquery.value.AtomicValue; import org.exist.xquery.value.NumericValue; import org.exist.xquery.value.Sequence; import org.exist.xquery.value.StringValue; import org.w3c.dom.Element; import org.w3c.dom.Node; import javax.xml.XMLConstants; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import static java.util.regex.Pattern.CASE_INSENSITIVE; /** * * A condition that can be defined for complex range config elements * that compares an attribute. * * @author Marcel Schaeben */ public class RangeIndexConfigAttributeCondition extends RangeIndexConfigCondition{ private final String attributeName; private final QName attribute; private final String value; private final Operator operator; private boolean caseSensitive = true; private boolean numericComparison = false; private Double numericValue = null; private String lowercaseValue = null; private Pattern pattern = null; public RangeIndexConfigAttributeCondition(Element elem, NodePath parentPath) throws DatabaseConfigurationException { if (parentPath.getLastComponent().getNameType() == ElementValue.ATTRIBUTE) { throw new DatabaseConfigurationException("Range index module: Attribute condition cannot be defined for an attribute:" + parentPath.toString()); } this.attributeName = elem.getAttribute("attribute"); if (this.attributeName == null || this.attributeName.length() == 0) { throw new DatabaseConfigurationException("Range index module: Empty or no attribute qname in condition"); } this.attribute = new QName(QName.extractLocalName(this.attributeName), XMLConstants.NULL_NS_URI, QName.extractPrefix(this.attributeName), ElementValue.ATTRIBUTE); this.value = elem.getAttribute("value"); // parse operator (default to 'eq' if missing) if (elem.hasAttribute("operator")) { final String operatorName = elem.getAttribute("operator"); this.operator = Operator.getByName(operatorName.toLowerCase()); if (this.operator == null) { throw new DatabaseConfigurationException("Range index module: Invalid operator specified in range index condition: " + operatorName + "."); } } else { this.operator = Operator.EQ; } final String caseString = elem.getAttribute("case"); final String numericString = elem.getAttribute("numeric"); this.caseSensitive = (caseString != null && !caseString.equalsIgnoreCase("no")); this.numericComparison = (numericString != null && numericString.equalsIgnoreCase("yes")); // try to create a pattern matcher for a 'matches' condition if (this.operator == Operator.MATCH) { final int flags = this.caseSensitive ? 0 : CASE_INSENSITIVE; try { this.pattern = Pattern.compile(this.value, flags); } catch (PatternSyntaxException e) { RangeIndex.LOG.error(e); throw new DatabaseConfigurationException("Range index module: Invalid regular expression in condition: " + this.value); } } // try to parse the number value if numeric comparison is specified // store a reference to numeric value to avoid having to parse each time if (this.numericComparison) { switch(this.operator) { case MATCH: case STARTS_WITH: case ENDS_WITH: case CONTAINS: throw new DatabaseConfigurationException("Range index module: Numeric comparison not applicable for operator: " + this.operator.name()); } try { this.numericValue = Double.parseDouble(this.value); } catch (NumberFormatException e) { throw new DatabaseConfigurationException("Range index module: Numeric attribute condition specified, but required value cannot be parsed as number: " + this.value); } } } // lazily evaluate lowercase value to convert once when needed private String getLowercaseValue() { if (this.lowercaseValue == null) { if (this.value != null) { this.lowercaseValue = this.value.toLowerCase(); } } return this.lowercaseValue; } @Override public boolean matches(Node node) { if (node.getNodeType() == Node.ELEMENT_NODE && matchValue(((Element)node).getAttribute(attributeName))) { return true; } return false; } private boolean matchValue(String testValue) { switch (operator) { case EQ: case NE: boolean matches; if (this.numericComparison) { double testDouble = toDouble(testValue); matches = this.numericValue.equals(testDouble); } else if (!this.caseSensitive) { matches = this.value.equalsIgnoreCase(testValue); } else { matches = this.value.equals(testValue); } return this.operator == Operator.EQ ? matches : !matches; case GT: case LT: case GE: case LE: int result; if (this.numericComparison) { final double testDouble = toDouble(testValue); result = Double.compare(testDouble, this.numericValue); } else if (!this.caseSensitive) { result = testValue.toLowerCase().compareTo(this.getLowercaseValue()); } else { result = testValue.compareTo(this.value); } return matchOrdinal(this.operator, result); case ENDS_WITH: return this.caseSensitive ? testValue.endsWith(this.value) : testValue.toLowerCase().endsWith(this.getLowercaseValue()); case STARTS_WITH: return this.caseSensitive ? testValue.startsWith(this.value) : testValue.toLowerCase().startsWith(this.getLowercaseValue()); case CONTAINS: return this.caseSensitive ? testValue.contains(this.value) : testValue.toLowerCase().contains(this.getLowercaseValue()); case MATCH: final Matcher matcher = this.pattern.matcher(testValue); return matcher.matches(); } return false; } private boolean matchOrdinal(Operator operator, int result) { switch(operator) { case GT: return result > 0; case LT: return result < 0; case GE: return result >= 0; case LE: return result <= 0; } return false; } private Double toDouble(String value) { try { return Double.parseDouble(value); } catch (NumberFormatException e) { RangeIndex.LOG.debug("Non-numeric value encountered for numeric condition on @'" + this.attributeName + "': " + value); return new Double(0); } } @Override public boolean find(Predicate predicate) { final Expression inner = this.getInnerExpression(predicate); Operator operator; Expression lhe; Expression rhe ; // get the type of the expression inside the predicate and determine right and left hand arguments if (inner instanceof GeneralComparison) { final GeneralComparison comparison = (GeneralComparison) inner; operator = RangeQueryRewriter.getOperator(inner); lhe = comparison.getLeft(); rhe = comparison.getRight(); } else if (inner instanceof InternalFunctionCall) { // calls to matches() will not have been rewritten to a comparison, so check for function call final Function func = ((InternalFunctionCall) inner).getFunction(); if (func.isCalledAs("matches")) { operator = Operator.MATCH; lhe = func.getArgument(0); rhe = func.getArgument(1); lhe = unwrapSubExpression(lhe); rhe = unwrapSubExpression(rhe); } else { // predicate expression cannot be parsed as condition return false; } } else { // predicate expression cannot be parsed as condition return false; } // find the attribute name and value pair from the predicate to check against // first assume attribute is on the left and value is on the right LocationStep testStep = findLocationStep(lhe); AtomicValue testValue = findAtomicValue(rhe); switch (this.operator) { case EQ: case NE: // the equality operators are commutative so if attribute/value pair has not been found, // check the other way around if (testStep == null && testValue == null) { testStep = findLocationStep(rhe); testValue = findAtomicValue(lhe); } case GT: case LT: case GE: case LE: // for ordinal comparisons, attribute and value can also be the other way around in the predicate // but the operator has to be inverted if (testStep == null && testValue == null) { testStep = findLocationStep(rhe); testValue = findAtomicValue(lhe); operator = invertOrdinalOperator(operator); } } if (testStep != null && testValue != null) { final QName qname = testStep.getTest().getName(); Comparable foundValue; Comparable requiredValue; boolean valueTypeMatches; try { if (this.numericComparison) { valueTypeMatches = testValue instanceof NumericValue; requiredValue = this.numericValue; foundValue = testValue.toJavaObject(Double.class); } else { valueTypeMatches = testValue instanceof StringValue; if (this.caseSensitive) { requiredValue = this.getLowercaseValue(); foundValue = testValue.getStringValue().toLowerCase(); } else { requiredValue = this.value; foundValue = testValue.getStringValue(); } } if (qname.getNameType() == ElementValue.ATTRIBUTE && operator.equals(this.operator) && qname.equals(this.attribute) && valueTypeMatches && foundValue.equals(requiredValue)) { return true; } } catch (XPathException e) { RangeIndex.LOG.error("Value conversion error when testing predicate for condition, value: " + testValue.toString()); RangeIndex.LOG.error(e); } } return false; } private Expression unwrapSubExpression(Expression expr) { if (expr instanceof Atomize) { expr = ((Atomize) expr).getExpression(); } if (expr instanceof DynamicCardinalityCheck) { if (expr.getSubExpressionCount() == 1) { expr = expr.getSubExpression(0); } } if (expr instanceof PathExpr) { if (expr.getSubExpressionCount() == 1) { expr = expr.getSubExpression(0); } } return expr; } private LocationStep findLocationStep(Expression expr) { if (expr instanceof LocationStep) { return (LocationStep) expr; } return null; } private AtomicValue findAtomicValue(Expression expr) { if (expr instanceof AtomicValue) { return (AtomicValue) expr; } else if (expr instanceof LiteralValue) { return ((LiteralValue) expr).getValue(); } else if (expr instanceof VariableReference || expr instanceof Function) { try { final Sequence result = expr.eval(null); if (result instanceof AtomicValue) { return (AtomicValue) result; } } catch (XPathException e) { RangeIndex.LOG.error(e); } } return null; } private Operator invertOrdinalOperator(Operator operator) { switch(operator) { case LE: return Operator.GE; case GE: return Operator.LE; case LT: return Operator.GT; case GT: return Operator.LT; } return null; } }