package client.net.sf.saxon.ce.expr; import client.net.sf.saxon.ce.expr.sort.AtomicComparer; import client.net.sf.saxon.ce.expr.sort.CodepointCollatingComparer; import client.net.sf.saxon.ce.expr.sort.CodepointCollator; import client.net.sf.saxon.ce.expr.sort.GenericAtomicComparer; import client.net.sf.saxon.ce.functions.*; import client.net.sf.saxon.ce.lib.StringCollator; import client.net.sf.saxon.ce.om.Item; import client.net.sf.saxon.ce.om.NamePool; import client.net.sf.saxon.ce.om.StandardNames; import client.net.sf.saxon.ce.trans.NoDynamicContextException; import client.net.sf.saxon.ce.trans.XPathException; import client.net.sf.saxon.ce.type.*; import client.net.sf.saxon.ce.value.*; import client.net.sf.saxon.ce.value.StringValue; import java.math.BigDecimal; /** * ValueComparison: a boolean expression that compares two atomic values * for equals, not-equals, greater-than or less-than. Implements the operators * eq, ne, lt, le, gt, ge */ public final class ValueComparison extends BinaryExpression implements ComparisonExpression { private AtomicComparer comparer; private BooleanValue resultWhenEmpty = null; private boolean needsRuntimeCheck; /** * Create a comparison expression identifying the two operands and the operator * * @param p1 the left-hand operand * @param op the operator, as a token returned by the Tokenizer (e.g. Token.LT) * @param p2 the right-hand operand */ public ValueComparison(Expression p1, int op, Expression p2) { super(p1, op, p2); } /** * Set the AtomicComparer used to compare atomic values * @param comparer the AtomicComparer */ public void setAtomicComparer(AtomicComparer comparer) { this.comparer = comparer; } /** * Get the AtomicComparer used to compare atomic values. This encapsulates any collation that is used. * Note that the comparer is always known at compile time. */ public AtomicComparer getAtomicComparer() { return comparer; } /** * Get the primitive (singleton) operator used: one of Token.FEQ, Token.FNE, Token.FLT, Token.FGT, * Token.FLE, Token.FGE */ public int getSingletonOperator() { return operator; } /** * Set the result to be returned if one of the operands is an empty sequence * @param value the result to be returned if an operand is empty. Supply null to mean the empty sequence. */ public void setResultWhenEmpty(BooleanValue value) { resultWhenEmpty = value; } /** * Get the result to be returned if one of the operands is an empty sequence * @return BooleanValue.TRUE, BooleanValue.FALSE, or null (meaning the empty sequence) */ public BooleanValue getResultWhenEmpty() { return resultWhenEmpty; } /** * Type-check the expression */ public Expression typeCheck(ExpressionVisitor visitor, ItemType contextItemType) throws XPathException { NamePool namePool = visitor.getConfiguration().getNamePool(); TypeHierarchy th = visitor.getConfiguration().getTypeHierarchy(); StaticContext env = visitor.getStaticContext(); operand0 = visitor.typeCheck(operand0, contextItemType); if (Literal.isEmptySequence(operand0)) { return (resultWhenEmpty == null ? operand0 : Literal.makeLiteral(resultWhenEmpty)); } operand1 = visitor.typeCheck(operand1, contextItemType); if (Literal.isEmptySequence(operand1)) { return (resultWhenEmpty == null ? operand1 : Literal.makeLiteral(resultWhenEmpty)); } final SequenceType optionalAtomic = SequenceType.OPTIONAL_ATOMIC; RoleLocator role0 = new RoleLocator(RoleLocator.BINARY_EXPR, Token.tokens[operator], 0); operand0 = TypeChecker.staticTypeCheck(operand0, optionalAtomic, false, role0, visitor); RoleLocator role1 = new RoleLocator(RoleLocator.BINARY_EXPR, Token.tokens[operator], 1); operand1 = TypeChecker.staticTypeCheck(operand1, optionalAtomic, false, role1, visitor); AtomicType t0 = operand0.getItemType(th).getAtomizedItemType(); AtomicType t1 = operand1.getItemType(th).getAtomizedItemType(); BuiltInAtomicType p0 = (BuiltInAtomicType)t0.getPrimitiveItemType(); if (p0.equals(BuiltInAtomicType.UNTYPED_ATOMIC)) { p0 = BuiltInAtomicType.STRING; } BuiltInAtomicType p1 = (BuiltInAtomicType)t1.getPrimitiveItemType(); if (p1.equals(BuiltInAtomicType.UNTYPED_ATOMIC)) { p1 = BuiltInAtomicType.STRING; } needsRuntimeCheck = p0.equals(BuiltInAtomicType.ANY_ATOMIC) || p1.equals(BuiltInAtomicType.ANY_ATOMIC); if (!needsRuntimeCheck && !Type.isComparable(p0, p1, Token.isOrderedOperator(operator))) { boolean opt0 = Cardinality.allowsZero(operand0.getCardinality()); boolean opt1 = Cardinality.allowsZero(operand1.getCardinality()); if (opt0 || opt1) { // This is a comparison such as (xs:integer? eq xs:date?). This is almost // certainly an error, but we need to let it through because it will work if // one of the operands is an empty sequence. String which = null; if (opt0) which = "the first operand is"; if (opt1) which = "the second operand is"; if (opt0 && opt1) which = "one or both operands are"; visitor.getStaticContext().issueWarning("Comparison of " + t0.toString(namePool) + (opt0 ? "?" : "") + " to " + t1.toString(namePool) + (opt1 ? "?" : "") + " will fail unless " + which + " empty", getSourceLocator()); needsRuntimeCheck = true; } else { typeError("Cannot compare " + t0.toString(namePool) + " to " + t1.toString(namePool), "XPTY0004", null); } } if (!(operator == Token.FEQ || operator == Token.FNE)) { if (!p0.isOrdered()) { typeError("Type " + t0.toString(env.getNamePool()) + " is not an ordered type", "XPTY0004", null); } if (!p1.isOrdered()) { typeError("Type " + t1.toString(env.getNamePool()) + " is not an ordered type", "XPTY0004", null); } } if (comparer == null) { // In XSLT, only do this the first time through, otherwise the default-collation attribute may be missed final String defaultCollationName = env.getDefaultCollationName(); StringCollator comp = env.getConfiguration().getNamedCollation(defaultCollationName); if (comp == null) { comp = CodepointCollator.getInstance(); } comparer = GenericAtomicComparer.makeAtomicComparer( p0, p1, comp, env.getConfiguration().getConversionContext()); } return this; } /** * Perform optimisation of an expression and its subexpressions. * <p/> * <p>This method is called after all references to functions and variables have been resolved * to the declaration of the function or variable, and after all type checking has been done.</p> * * @param visitor an expression visitor * @param contextItemType the static type of "." at the point where this expression is invoked. * The parameter is set to null if it is known statically that the context item will be undefined. * If the type of the context item is not known statically, the argument is set to * {@link client.net.sf.saxon.ce.type.Type#ITEM_TYPE} * @return the original expression, rewritten if appropriate to optimize execution * @throws XPathException if an error is discovered during this phase * (typically a type error) */ public Expression optimize(ExpressionVisitor visitor, ItemType contextItemType) throws XPathException { TypeHierarchy th = visitor.getConfiguration().getTypeHierarchy(); operand0 = visitor.optimize(operand0, contextItemType); operand1 = visitor.optimize(operand1, contextItemType); Value value0 = null; Value value1 = null; if (operand0 instanceof Literal) { value0 = ((Literal)operand0).getValue(); } if (operand1 instanceof Literal) { value1 = ((Literal)operand1).getValue(); } // evaluate the expression now if both arguments are constant if ((value0 != null) && (value1 != null)) { try { AtomicValue r = (AtomicValue)evaluateItem(visitor.getStaticContext().makeEarlyEvaluationContext()); //noinspection RedundantCast return Literal.makeLiteral(r == null ? (Value)EmptySequence.getInstance() : (Value)r); } catch (NoDynamicContextException e) { // early evaluation failed, typically because the implicit context isn't available. // Try again at run-time return this; } } // optimise count(x) eq 0 (or gt 0, ne 0, eq 0, etc) if (operand0 instanceof Count && Literal.isAtomic(operand1)) { if (isZero(value1)) { if (operator == Token.FEQ || operator == Token.FLE) { // rewrite count(x)=0 as empty(x) return SystemFunction.makeSystemFunction( "empty", new Expression[]{((FunctionCall) operand0).argument[0]}); } else if (operator == Token.FNE || operator == Token.FGT) { // rewrite count(x)!=0, count(x)>0 as exists(x) return SystemFunction.makeSystemFunction( "exists", new Expression[]{((FunctionCall) operand0).argument[0]}); } else if (operator == Token.FGE) { // rewrite count(x)>=0 as true() return Literal.makeLiteral(BooleanValue.TRUE); } else { // singletonOperator == Token.FLT // rewrite count(x)<0 as false() return Literal.makeLiteral(BooleanValue.FALSE); } } else if (value1 instanceof IntegerValue && (operator == Token.FGT || operator == Token.FGE)) { // rewrite count(x) gt n as exists(x[n+1]) // and count(x) ge n as exists(x[n]) long val = ((IntegerValue) value1).intValue(); if (operator == Token.FGT) { val++; } FilterExpression filter = new FilterExpression(((FunctionCall) operand0).argument[0], Literal.makeLiteral(new IntegerValue(new BigDecimal(val)))); ExpressionTool.copyLocationInfo(this, filter); return SystemFunction.makeSystemFunction("exists", new Expression[]{filter}); } } // optimise (0 eq count(x)), etc if (operand1 instanceof Count && isZero(value0)) { ValueComparison vc = new ValueComparison(operand1, Token.inverse(operator), operand0); ExpressionTool.copyLocationInfo(this, vc); return visitor.optimize(visitor.typeCheck(vc, contextItemType), contextItemType); } // optimise string-length(x) = 0, >0, !=0 etc if ((operand0 instanceof StringLength) && (((StringLength) operand0).getNumberOfArguments() == 1) && isZero(value1)) { Expression arg = (((StringLength)operand0).getArguments()[0]); switch (operator) { case Token.FEQ: case Token.FLE: return SystemFunction.makeSystemFunction("not", new Expression[]{arg}); case Token.FNE: case Token.FGT: return SystemFunction.makeSystemFunction("boolean", new Expression[]{arg}); case Token.FGE: return Literal.makeLiteral(BooleanValue.TRUE); case Token.FLT: return Literal.makeLiteral(BooleanValue.FALSE); } } // optimise (0 = string-length(x)), etc if ((operand1 instanceof StringLength) && (((StringLength) operand1).getNumberOfArguments() == 1) && isZero(value0)) { Expression arg = (((StringLength)operand1).getArguments()[0]); switch (operator) { case Token.FEQ: case Token.FGE: return SystemFunction.makeSystemFunction("not", new Expression[]{arg}); case Token.FNE: case Token.FLT: return SystemFunction.makeSystemFunction("boolean", new Expression[]{arg}); case Token.FLE: return Literal.makeLiteral(BooleanValue.TRUE); case Token.FGT: return Literal.makeLiteral(BooleanValue.FALSE); } } // optimise string="" etc // Note we can change S!="" to boolean(S) for cardinality zero-or-one, but we can only // change S="" to not(S) for cardinality exactly-one. int p0 = operand0.getItemType(th).getPrimitiveType(); if ((p0 == StandardNames.XS_STRING || p0 == StandardNames.XS_ANY_URI || p0 == StandardNames.XS_UNTYPED_ATOMIC) && operand1 instanceof Literal && ((Literal)operand1).getValue() instanceof StringValue && ((StringValue)((Literal)operand1).getValue()).isZeroLength() && comparer instanceof CodepointCollatingComparer) { switch (operator) { case Token.FNE: case Token.FGT: return SystemFunction.makeSystemFunction("boolean", new Expression[]{operand0}); case Token.FEQ: case Token.FLE: if (operand0.getCardinality() == StaticProperty.EXACTLY_ONE) { return SystemFunction.makeSystemFunction("not", new Expression[]{operand0}); } } } // optimize "" = string etc int p1 = operand1.getItemType(th).getPrimitiveType(); if ((p1 == StandardNames.XS_STRING || p1 == StandardNames.XS_ANY_URI || p1 == StandardNames.XS_UNTYPED_ATOMIC) && operand0 instanceof Literal && ((Literal)operand0).getValue() instanceof StringValue && ((StringValue)((Literal)operand0).getValue()).isZeroLength() && comparer instanceof CodepointCollatingComparer) { switch (operator) { case Token.FNE: case Token.FLT: return SystemFunction.makeSystemFunction("boolean", new Expression[]{operand1}); case Token.FEQ: case Token.FGE: if (operand1.getCardinality() == StaticProperty.EXACTLY_ONE) { return SystemFunction.makeSystemFunction("not", new Expression[]{operand1}); } } } return this; } /** * Test whether an expression is constant zero * @param v the value to be tested * @return true if the operand is the constant zero (of any numeric data type) */ private static boolean isZero(Value v) { return v instanceof NumericValue && ((NumericValue)v).compareTo(0) == 0; } /** * Evaluate the effective boolean value of the expression * * @param context the given context for evaluation * @return a boolean representing the result of the comparison of the two operands */ public boolean effectiveBooleanValue(XPathContext context) throws XPathException { try { AtomicValue v0 = ((AtomicValue) operand0.evaluateItem(context)); if (v0 == null) { return (resultWhenEmpty == BooleanValue.TRUE); // normally false } AtomicValue v1 = ((AtomicValue) operand1.evaluateItem(context)); if (v1 == null) { return (resultWhenEmpty == BooleanValue.TRUE); // normally false } return compare(v0, operator, v1, comparer.provideContext(context), needsRuntimeCheck); } catch (XPathException e) { // re-throw the exception with location information added e.maybeSetLocation(getSourceLocator()); e.maybeSetContext(context); throw e; } } /** * Compare two atomic values, using a specified operator and collation * * @param v0 the first operand * @param op the operator, as defined by constants such as {@link client.net.sf.saxon.ce.expr.Token#FEQ} or * {@link client.net.sf.saxon.ce.expr.Token#FLT} * @param v1 the second operand * @param collator the Collator to be used when comparing strings * @param checkTypes * @return the result of the comparison: -1 for LT, 0 for EQ, +1 for GT * @throws XPathException if the values are not comparable */ static boolean compare(AtomicValue v0, int op, AtomicValue v1, AtomicComparer collator, boolean checkTypes) throws XPathException { if (checkTypes && !Type.isComparable(v0.getPrimitiveType(), v1.getPrimitiveType(), Token.isOrderedOperator(op))) { XPathException e2 = new XPathException("Cannot compare " + Type.displayTypeName(v0) + " to " + Type.displayTypeName(v1)); e2.setErrorCode("XPTY0004"); e2.setIsTypeError(true); throw e2; } if (v0.isNaN() || v1.isNaN()) { return (op == Token.FNE); } try { switch (op) { case Token.FEQ: return collator.comparesEqual(v0, v1); case Token.FNE: return !collator.comparesEqual(v0, v1); case Token.FGT: return collator.compareAtomicValues(v0, v1) > 0; case Token.FLT: return collator.compareAtomicValues(v0, v1) < 0; case Token.FGE: return collator.compareAtomicValues(v0, v1) >= 0; case Token.FLE: return collator.compareAtomicValues(v0, v1) <= 0; default: throw new UnsupportedOperationException("Unknown operator " + op); } } catch (ClassCastException err) { XPathException e2 = new XPathException("Cannot compare " + Type.displayTypeName(v0) + " to " + Type.displayTypeName(v1)); e2.setErrorCode("XPTY0004"); e2.setIsTypeError(true); throw e2; } } /** * Evaluate the expression in a given context * * @param context the given context for evaluation * @return a BooleanValue representing the result of the numeric comparison of the two operands, * or null representing the empty sequence */ public Item evaluateItem(XPathContext context) throws XPathException { try { AtomicValue v0 = (AtomicValue) operand0.evaluateItem(context); if (v0 == null) { return resultWhenEmpty; } AtomicValue v1 = (AtomicValue) operand1.evaluateItem(context); if (v1 == null) { return resultWhenEmpty; } return BooleanValue.get(compare(v0, operator, v1, comparer.provideContext(context), needsRuntimeCheck)); } catch (XPathException e) { // re-throw the exception with location information added e.maybeSetLocation(getSourceLocator()); e.maybeSetContext(context); throw e; } } /** * Determine the data type of the expression * * @param th the type hierarchy cache * @return Type.BOOLEAN */ public ItemType getItemType(TypeHierarchy th) { return BuiltInAtomicType.BOOLEAN; } /** * Determine the static cardinality. */ public int computeCardinality() { if (resultWhenEmpty != null) { return StaticProperty.EXACTLY_ONE; } else { return super.computeCardinality(); } } } // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. // This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.