org.modeshape.jcr.query.xpath.XPathToQueryTranslator Maven / Gradle / Ivy
/*
* ModeShape (http://www.modeshape.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modeshape.jcr.query.xpath;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.modeshape.jcr.api.query.qom.Operator;
import org.modeshape.jcr.query.QueryBuilder;
import org.modeshape.jcr.query.QueryBuilder.ConstraintBuilder;
import org.modeshape.jcr.query.QueryBuilder.OrderByBuilder;
import org.modeshape.jcr.query.QueryBuilder.OrderByOperandBuilder;
import org.modeshape.jcr.query.model.AllNodes;
import org.modeshape.jcr.query.model.Query;
import org.modeshape.jcr.query.model.QueryCommand;
import org.modeshape.jcr.query.model.TypeSystem;
import org.modeshape.jcr.query.parse.InvalidQueryException;
import org.modeshape.jcr.query.xpath.XPath.And;
import org.modeshape.jcr.query.xpath.XPath.AttributeNameTest;
import org.modeshape.jcr.query.xpath.XPath.AxisStep;
import org.modeshape.jcr.query.xpath.XPath.BinaryComponent;
import org.modeshape.jcr.query.xpath.XPath.Comparison;
import org.modeshape.jcr.query.xpath.XPath.Component;
import org.modeshape.jcr.query.xpath.XPath.ContextItem;
import org.modeshape.jcr.query.xpath.XPath.DescendantOrSelf;
import org.modeshape.jcr.query.xpath.XPath.ElementTest;
import org.modeshape.jcr.query.xpath.XPath.Except;
import org.modeshape.jcr.query.xpath.XPath.FilterStep;
import org.modeshape.jcr.query.xpath.XPath.FunctionCall;
import org.modeshape.jcr.query.xpath.XPath.Intersect;
import org.modeshape.jcr.query.xpath.XPath.Literal;
import org.modeshape.jcr.query.xpath.XPath.NameTest;
import org.modeshape.jcr.query.xpath.XPath.NodeTest;
import org.modeshape.jcr.query.xpath.XPath.Or;
import org.modeshape.jcr.query.xpath.XPath.OrderBy;
import org.modeshape.jcr.query.xpath.XPath.OrderBySpec;
import org.modeshape.jcr.query.xpath.XPath.ParenthesizedExpression;
import org.modeshape.jcr.query.xpath.XPath.PathExpression;
import org.modeshape.jcr.query.xpath.XPath.StepExpression;
import org.modeshape.jcr.query.xpath.XPath.TextTest;
import org.modeshape.jcr.query.xpath.XPath.Union;
import org.modeshape.jcr.value.PropertyType;
/**
* A component that translates an {@link XPath} abstract syntax model representation into a {@link QueryCommand ModeShape abstract
* query model}.
*/
public class XPathToQueryTranslator {
protected static final Map CAST_FUNCTION_NAME_TO_TYPE;
static {
Map map = new HashMap();
map.put(new NameTest("fn", "string"), PropertyType.STRING.getName().toUpperCase());
map.put(new NameTest("xs", "string"), PropertyType.STRING.getName().toUpperCase());
map.put(new NameTest("xs", "base64Binary"), PropertyType.BINARY.getName().toUpperCase());
map.put(new NameTest("xs", "double"), PropertyType.DOUBLE.getName().toUpperCase());
map.put(new NameTest("xs", "long"), PropertyType.LONG.getName().toUpperCase());
map.put(new NameTest("xs", "boolean"), PropertyType.BOOLEAN.getName().toUpperCase());
map.put(new NameTest("xs", "dateTime"), PropertyType.DATE.getName().toUpperCase());
map.put(new NameTest("xs", "string"), PropertyType.PATH.getName().toUpperCase());
map.put(new NameTest("xs", "string"), PropertyType.NAME.getName().toUpperCase());
map.put(new NameTest("xs", "IDREF"), PropertyType.REFERENCE.getName().toUpperCase());
CAST_FUNCTION_NAME_TO_TYPE = Collections.unmodifiableMap(map);
}
private final String query;
private final TypeSystem typeSystem;
private final QueryBuilder builder;
private final Set aliases = new HashSet();
public XPathToQueryTranslator( TypeSystem context,
String query ) {
this.query = query;
this.typeSystem = context;
this.builder = new QueryBuilder(this.typeSystem);
}
public QueryCommand createQuery( Component xpath ) {
if (xpath instanceof BinaryComponent) {
BinaryComponent binary = (BinaryComponent)xpath;
if (binary instanceof Union) {
createQuery(binary.getLeft());
builder.union();
createQuery(binary.getRight());
return builder.query();
} else if (binary instanceof Intersect) {
createQuery(binary.getLeft());
builder.intersect();
createQuery(binary.getRight());
return builder.query();
} else if (binary instanceof Except) {
createQuery(binary.getLeft());
builder.except();
createQuery(binary.getRight());
return builder.query();
}
} else if (xpath instanceof PathExpression) {
translate((PathExpression)xpath);
return builder.query();
}
// unexpected component ...
throw new InvalidQueryException(query,
"Acceptable XPath queries must lead with a path expression or must be a union, intersect or except");
}
protected void translate( PathExpression pathExpression ) {
List steps = pathExpression.getSteps();
assert !steps.isEmpty();
if (!pathExpression.isRelative()) {
// Absolute path must start with "/jcr:root/" or "//" ...
Component first = steps.get(0).collapse();
// Result will be NameTest("jcr","root") or DescendantOrSelf ...
if (first instanceof DescendantOrSelf) {
// do nothing ...
} else if (first instanceof NameTest && steps.size() == 1 && ((NameTest)first).matches("jcr", "root")) {
// We can actually remove this first step, since relative paths are relative to the root ...
steps = steps.subList(1, steps.size());
} else if (first instanceof NameTest && steps.size() > 1 && ((NameTest)first).matches("jcr", "root")) {
// We can actually remove this first step, since relative paths are relative to the root ...
steps = steps.subList(1, steps.size());
} else {
throw new InvalidQueryException(query, "An absolute path expression must start with '//' or '/jcr:root/...'");
}
}
// Walk the steps along the path expression ...
ConstraintBuilder where = builder.where();
List path = new ArrayList();
String tableName = null;
for (StepExpression step : steps) {
if (step instanceof AxisStep) {
AxisStep axis = (AxisStep)step;
NodeTest nodeTest = axis.getNodeTest();
if (nodeTest instanceof NameTest) {
if (appliesToPathConstraint(axis.getPredicates())) {
// Everything in this axis/step applies to the path, so add it and we'll translate it below ...
path.add(step);
} else {
// The constraints are more complicated than can be applied to the path ...
// if (!nameTest.isWildcard()) {
// There is a non-wildcard name test that we still need to add to the path ...
path.add(step);
// }
// We need to define a new source/table ...
tableName = translateSource(tableName, path, where);
translatePredicates(axis.getPredicates(), tableName, where);
path.clear();
}
} else if (nodeTest instanceof ElementTest) {
// We need to build a new source with the partial path we have so far ...
tableName = translateElementTest((ElementTest)nodeTest, path, where);
translatePredicates(axis.getPredicates(), tableName, where);
path.clear();
} else if (nodeTest instanceof AttributeNameTest) {
AttributeNameTest attributeName = (AttributeNameTest)nodeTest;
builder.select(nameFrom(attributeName.getNameTest()));
} else if (nodeTest instanceof TextTest) {
NameTest nameTest = new NameTest("jcr", "xmltext");
List predicates = axis.getPredicates();
if (predicates == null || predicates.isEmpty() || appliesToPathConstraint(predicates)) {
AxisStep textStep = new AxisStep(nameTest, axis.getPredicates());
path.add(textStep);
} else {
tableName = translateSource(tableName, path, where);
translatePredicates(predicates, tableName, where);
path.clear();
}
} else {
throw new InvalidQueryException(query, "The '" + step + "' step is not supported");
}
} else if (step instanceof FilterStep) {
FilterStep filter = (FilterStep)step;
Component primary = filter.getPrimaryExpression();
List predicates = filter.getPredicates();
if (primary instanceof ContextItem) {
if (appliesToPathConstraint(predicates)) {
// Can ignore the '.' ...
} else {
// The constraints are more complicated, so we need to define a new source/table ...
path.add(step);
tableName = translateSource(tableName, path, where);
translatePredicates(predicates, tableName, where);
path.clear();
}
} else if (primary instanceof Literal) {
throw new InvalidQueryException(query,
"A literal is not supported in the primary path expression; therefore '"
+ primary + "' is not valid");
} else if (primary instanceof FunctionCall) {
throw new InvalidQueryException(query,
"A function call is not supported in the primary path expression; therefore '"
+ primary + "' is not valid");
} else if (primary instanceof ParenthesizedExpression) {
// This can be used to define an OR-ed set of expressions defining select columns ...
ParenthesizedExpression paren = (ParenthesizedExpression)primary;
Component wrapped = paren.getWrapped().collapse();
if (wrapped instanceof AttributeNameTest) {
AttributeNameTest attributeName = (AttributeNameTest)wrapped;
builder.select(nameFrom(attributeName.getNameTest()));
} else if (wrapped instanceof BinaryComponent) {
for (AttributeNameTest attributeName : extractAttributeNames((BinaryComponent)wrapped)) {
builder.select(nameFrom(attributeName.getNameTest()));
}
path.add(filter); // in case any element names are there
} else {
throw new InvalidQueryException(query,
"A parenthesized expression of this type is not supported in the primary path expression; therefore '"
+ primary + "' is not valid");
}
}
} else {
path.add(step);
}
}
if (steps.isEmpty() || !path.isEmpty()) {
translateSource(tableName, path, where);
}
where.end();
// Process the order-by clause ...
OrderBy orderBy = pathExpression.getOrderBy();
if (orderBy != null) {
OrderByBuilder orderByBuilder = builder.orderBy();
for (OrderBySpec spec : orderBy) {
OrderByOperandBuilder operandBuilder = null;
switch (spec.getOrder()) {
case ASCENDING:
operandBuilder = orderByBuilder.ascending();
break;
case DESCENDING:
operandBuilder = orderByBuilder.descending();
break;
}
assert operandBuilder != null;
if (spec.getAttributeName() != null) {
// This order by is defined by an attribute ...
NameTest attribute = spec.getAttributeName();
assert !attribute.isWildcard();
if (attribute.matches("jcr", "path")) {
String pathOf = tableName;
if (pathOf == null) pathOf = aliases.iterator().next();
operandBuilder.path(pathOf);
} else {
operandBuilder.propertyValue(tableName, attribute.toString());
builder.select(tableName + "." + attribute.toString());
}
} else if (spec.getScoreFunction() != null) {
// This order-by is defined by a "jcr:score" function ...
FunctionCall scoreFunction = spec.getScoreFunction();
assert scoreFunction != null;
List args = scoreFunction.getParameters();
String nameOfTableToScore = tableName;
if (!args.isEmpty()) {
if (args.size() == 1 && args.get(0) instanceof NameTest) {
// Just the table name ...
NameTest tableNameTest = (NameTest)args.get(0);
nameOfTableToScore = tableNameTest.toString();
}
}
operandBuilder.fullTextSearchScore(nameOfTableToScore);
} else {
PathExpression axes = spec.getPath();
List axesSteps = axes.getSteps();
if (axesSteps.size() != 2) {
throw new InvalidQueryException("Only child axis supported in ORDER BY clause");
}
NameTest childNode = (NameTest) ((AxisStep)axesSteps.get(0)).getNodeTest();
String alias = childNode.getLocalTest();
NameTest attribute = ((AttributeNameTest) ((AxisStep)axesSteps.get(1)).getNodeTest()).getNameTest();
builder.select(tableName + "." + attribute.toString());
builder.join("nt:base as " + alias).onChildNode(tableName, alias);
operandBuilder.propertyValue(alias, attribute.toString());
}
}
orderByBuilder.end();
}
// Try building this query, because we need to check the # of columns selected and the # of sources ...
Query query = (Query)builder.query();
if (query.columns().isEmpty() && query.source() instanceof AllNodes) {
// This is basically 'SELECT * FROM __ALLNODES__", which means that no type was explicitly specified and
// nothing was selected from that type. According to JCR 1.0 Section 6.6.3.1, this equates to
// 'SELECT * FROM [nt:base]', and since there is just one property on nt:base (but many on __ALLNODES__)
// this really equates to 'SELECT [jcr:primaryType] FROM __ALLNODES__'.
builder.select("jcr:primaryType");
}
}
/**
* Find any {@link AttributeNameTest attribute names} that have been unioned together (with '|'). Any other combination of
* objects results in an error.
*
* @param binary the binary component
* @return the list of attribute names, if that's all that's in the supplied component; may be empty
*/
protected List extractAttributeNames( BinaryComponent binary ) {
List results = new ArrayList();
boolean failed = false;
if (binary instanceof Union) {
for (int i = 0; i != 2; ++i) {
Component comp = i == 0 ? binary.getLeft() : binary.getRight();
comp = comp.collapse();
if (comp instanceof Union) {
results.addAll(extractAttributeNames((BinaryComponent)comp));
} else if (comp instanceof AttributeNameTest) {
results.add((AttributeNameTest)comp);
} else if (comp instanceof NameTest) {
// Element names, which are fine but we'll ignore
} else {
failed = true;
break;
}
}
} else {
failed = true;
}
if (failed) {
throw new InvalidQueryException(query,
"A parenthesized expression in a path step may only contain ORed and ANDed attribute names or element names; therefore '"
+ binary + "' is not valid");
}
return results;
}
/**
* Find any {@link NameTest element names} that have been unioned together (with '|'). Any other combination of objects
* results in an error.
*
* @param binary the binary component
* @return the list of attribute names, if that's all that's in the supplied component; may be empty
*/
protected List extractElementNames( BinaryComponent binary ) {
List results = new ArrayList();
boolean failed = false;
if (binary instanceof Union) {
for (int i = 0; i != 2; ++i) {
Component comp = i == 0 ? binary.getLeft() : binary.getRight();
comp = comp.collapse();
if (comp instanceof Union) {
results.addAll(extractElementNames((BinaryComponent)comp));
} else if (comp instanceof AttributeNameTest) {
// ignore these ...
} else if (comp instanceof NameTest) {
results.add((NameTest)comp);
} else {
failed = true;
break;
}
}
} else {
failed = true;
}
if (failed) {
throw new InvalidQueryException(query,
"A parenthesized expression in a path step may only contain ORed element names; therefore '"
+ binary + "' is not valid");
}
return results;
}
protected String translateSource( String tableName,
List path,
ConstraintBuilder where ) {
if (path.size() == 0) {
// This is a query against the root node ...
String alias = newAlias();
builder.fromAllNodesAs(alias);
where.path(alias).isEqualTo("/");
return alias;
}
String alias = newAlias();
if (tableName != null) {
// This is after some element(...) steps, so we need to join ...
builder.joinAllNodesAs(alias);
alias = tableName;
} else {
// This is the only part of the query ...
builder.fromAllNodesAs(alias);
tableName = alias;
}
if (path.size() == 1 && path.get(0).collapse() instanceof NameTest) {
// Node immediately below root ...
NameTest nodeName = (NameTest)path.get(0).collapse();
where.path(alias).isLike("/" + nameFrom(nodeName) + "[%]");
} else if (path.size() == 2 && path.get(0) instanceof DescendantOrSelf && path.get(1).collapse() instanceof NameTest) {
// Node anywhere ...
NameTest nodeName = (NameTest)path.get(1).collapse();
if (!nodeName.isWildcard()) {
where.nodeName(alias).isEqualTo(nameFrom(nodeName));
}
} else {
// Must be just a bunch of descendant-or-self, axis and filter steps ...
translatePathExpressionConstraint(new PathExpression(true, path, null), where, alias);
}
return tableName;
}
protected String translateElementTest( ElementTest elementTest,
List pathConstraint,
ConstraintBuilder where ) {
String tableName = null;
NameTest typeName = elementTest.getTypeName();
if (typeName.isWildcard()) {
tableName = newAlias();
builder.fromAllNodesAs(tableName);
} else {
if (typeName.getLocalTest() == null) {
throw new InvalidQueryException(
query,
"The '"
+ elementTest
+ "' clause uses a partial wildcard in the type name, but only a wildcard on the whole name is supported");
}
tableName = nameFrom(typeName);
builder.from(tableName);
}
if (elementTest.getElementName() != null) {
NameTest nodeName = elementTest.getElementName();
if (!nodeName.isWildcard()) {
where.nodeName(tableName).isEqualTo(nameFrom(nodeName));
}
}
if (pathConstraint.isEmpty()) {
where.depth(tableName).isEqualTo(1);
} else {
List path = new ArrayList(pathConstraint);
if (!path.isEmpty() && path.get(path.size() - 1) instanceof AxisStep) {
// Element test should always apply to descendants, never to self, so add a descedant
path.add(new AxisStep(new NameTest(null, null), Collections.emptyList()));
}
translatePathExpressionConstraint(new PathExpression(true, path, null), where, tableName);
}
return tableName;
}
protected void translatePredicates( List predicates,
String tableName,
ConstraintBuilder where ) {
assert tableName != null;
for (Component predicate : predicates) {
translatePredicate(predicate, tableName, where);
}
}
protected String translatePredicate( Component predicate,
String tableName,
ConstraintBuilder where ) {
predicate = predicate.collapse();
assert tableName != null;
if (predicate instanceof ParenthesizedExpression) {
ParenthesizedExpression paren = (ParenthesizedExpression)predicate;
where = where.openParen();
translatePredicate(paren.getWrapped(), tableName, where);
where.closeParen();
} else if (predicate instanceof And) {
And and = (And)predicate;
where = where.openParen();
translatePredicate(and.getLeft(), tableName, where);
where.and();
translatePredicate(and.getRight(), tableName, where);
where.closeParen();
} else if (predicate instanceof Or) {
Or or = (Or)predicate;
where = where.openParen();
translatePredicate(or.getLeft(), tableName, where);
where.or();
translatePredicate(or.getRight(), tableName, where);
where.closeParen();
} else if (predicate instanceof Union) {
Union union = (Union)predicate;
where = where.openParen();
translatePredicate(union.getLeft(), tableName, where);
where.or();
translatePredicate(union.getRight(), tableName, where);
where.closeParen();
} else if (predicate instanceof Literal) {
Literal literal = (Literal)predicate;
if (literal.isInteger()) return tableName; // do nothing, since this is a path constraint and is handled elsewhere
} else if (predicate instanceof AttributeNameTest) {
// This adds the criteria that the attribute exists, and adds it to the select ...
AttributeNameTest attribute = (AttributeNameTest)predicate;
String propertyName = nameFrom(attribute.getNameTest());
// There is nothing in the JCR 1.0 spec that says that a property constrain implies it should be included in the
// result columns
// builder.select(tableName + "." + propertyName);
where.hasProperty(tableName, propertyName);
} else if (predicate instanceof NameTest) {
// This adds the criteria that the child node exists ...
NameTest childName = (NameTest)predicate;
String alias = newAlias();
builder.joinAllNodesAs(alias).onChildNode(tableName, alias);
if (!childName.isWildcard()) where.nodeName(alias).isEqualTo(nameFrom(childName));
tableName = alias;
} else if (predicate instanceof Comparison) {
Comparison comparison = (Comparison)predicate;
Component left = comparison.getLeft();
Component right = comparison.getRight();
Operator operator = comparison.getOperator();
if (left instanceof Literal) {
Component temp = left;
left = right;
right = temp;
operator = operator.reverse();
}
if (left instanceof NodeTest) {
NodeTest nodeTest = (NodeTest)left;
String propertyName = null;
if (nodeTest instanceof AttributeNameTest) {
AttributeNameTest attribute = (AttributeNameTest)left;
propertyName = nameFrom(attribute.getNameTest());
} else if (nodeTest instanceof NameTest) {
NameTest nameTest = (NameTest)left;
propertyName = nameFrom(nameTest);
} else {
throw new InvalidQueryException(query,
"Left hand side of a comparison must be a name test or attribute name test; therefore '"
+ comparison + "' is not valid");
}
if (right instanceof Literal) {
String value = ((Literal)right).getValue();
where.propertyValue(tableName, propertyName).is(operator, value);
} else if (right instanceof FunctionCall) {
FunctionCall call = (FunctionCall)right;
NameTest functionName = call.getName();
List parameters = call.getParameters();
// Is this a cast ...
String castType = CAST_FUNCTION_NAME_TO_TYPE.get(functionName);
if (castType != null) {
if (parameters.size() == 1 && parameters.get(0).collapse() instanceof Literal) {
// The first parameter can be the type name (or table name) ...
Literal value = (Literal)parameters.get(0).collapse();
where.propertyValue(tableName, propertyName).is(operator).cast(value.getValue()).as(castType);
} else {
throw new InvalidQueryException(query, "A cast function requires one literal parameter; therefore '"
+ comparison + "' is not valid");
}
} else {
throw new InvalidQueryException(query,
"Only the 'jcr:score' function is allowed in a comparison predicate; therefore '"
+ comparison + "' is not valid");
}
}
} else if (left instanceof FunctionCall && right instanceof Literal) {
FunctionCall call = (FunctionCall)left;
NameTest functionName = call.getName();
List parameters = call.getParameters();
String value = ((Literal)right).getValue();
if (functionName.matches("jcr", "score")) {
String scoreTableName = tableName;
if (parameters.isEmpty()) {
scoreTableName = tableName;
} else if (parameters.size() == 1 && parameters.get(0) instanceof NameTest) {
// The first parameter can be the type name (or table name) ...
NameTest name = (NameTest)parameters.get(0);
if (!name.isWildcard()) scoreTableName = nameFrom(name);
} else {
throw new InvalidQueryException(query,
"The 'jcr:score' function may have no parameters or the type name as the only parameter.");
}
where.fullTextSearchScore(scoreTableName).is(operator, value);
} else if (functionName.matches("fn", "name")) {
where.nodeName(tableName);
} else {
throw new InvalidQueryException(query,
"Only the 'jcr:score' function is allowed in a comparison predicate; therefore '"
+ comparison + "' is not valid");
}
}
} else if (predicate instanceof FunctionCall) {
FunctionCall call = (FunctionCall)predicate;
NameTest functionName = call.getName();
List parameters = call.getParameters();
Component param1 = parameters.size() > 0 ? parameters.get(0) : null;
Component param2 = parameters.size() > 1 ? parameters.get(1) : null;
if (functionName.matches(null, "not")) {
if (parameters.size() != 1) {
throw new InvalidQueryException(query, "The 'not' function requires one parameter; therefore '" + predicate
+ "' is not valid");
}
where = where.not().openParen();
translatePredicate(param1, tableName, where);
where.closeParen();
} else if (functionName.matches("jcr", "like")) {
if (parameters.size() != 2) {
throw new InvalidQueryException(query, "The 'jcr:like' function requires two parameters; therefore '"
+ predicate + "' is not valid");
}
if (!(param2 instanceof Literal)) {
throw new InvalidQueryException(query, "The second parameter of 'jcr:like' must be a literal; therefore '"
+ predicate + "' is not valid");
}
String value = ((Literal)param2).getValue();
if (param1 instanceof FunctionCall) {
where.nodeName(tableName).isLike(value);
} else if (param1 instanceof AttributeNameTest) {
NameTest attributeName = ((AttributeNameTest)param1).getNameTest();
where.propertyValue(tableName, nameFrom(attributeName)).isLike(value);
}
} else if (functionName.matches("jcr", "contains")) {
if (parameters.size() != 2) {
throw new InvalidQueryException(query, "The 'jcr:contains' function requires two parameters; therefore '"
+ predicate + "' is not valid");
}
if (!(param2 instanceof Literal)) {
throw new InvalidQueryException(query,
"The second parameter of 'jcr:contains' must be a literal; therefore '"
+ predicate + "' is not valid");
}
String value = ((Literal)param2).getValue();
if (param1 instanceof ContextItem) {
// refers to the current node (or table) ...
where.search(tableName, value);
} else if (param1 instanceof AttributeNameTest) {
// refers to an attribute on the current node (or table) ...
NameTest attributeName = ((AttributeNameTest)param1).getNameTest();
where.search(tableName, nameFrom(attributeName), value);
} else if (param1 instanceof NameTest) {
// refers to child node, so we need to add a join ...
String alias = newAlias();
builder.joinAllNodesAs(alias).onChildNode(tableName, alias);
// Now add the criteria ...
where.search(alias, value);
tableName = alias;
} else if (param1 instanceof PathExpression) {
// refers to a descendant node ...
PathExpression pathExpr = (PathExpression)param1;
if (pathExpr.getLastStep().collapse() instanceof AttributeNameTest) {
AttributeNameTest attributeName = (AttributeNameTest)pathExpr.getLastStep().collapse();
pathExpr = pathExpr.withoutLast();
String searchTable = translatePredicate(pathExpr, tableName, where);
if (attributeName.getNameTest().isWildcard()) {
where.search(searchTable, value);
} else {
where.search(searchTable, nameFrom(attributeName.getNameTest()), value);
}
} else {
String searchTable = translatePredicate(param1, tableName, where);
where.search(searchTable, value);
}
} else {
throw new InvalidQueryException(query,
"The first parameter of 'jcr:contains' must be a relative path (e.g., '.', an attribute name, a child name, etc.); therefore '"
+ predicate + "' is not valid");
}
} else if (functionName.matches("jcr", "deref")) {
throw new InvalidQueryException(query,
"The 'jcr:deref' function is not required by JCR and is not currently supported; therefore '"
+ predicate + "' is not valid");
} else {
throw new InvalidQueryException(query,
"Only the 'jcr:like' and 'jcr:contains' functions are allowed in a predicate; therefore '"
+ predicate + "' is not valid");
}
} else if (predicate instanceof PathExpression) {
// Requires that the descendant node with the relative path does exist ...
PathExpression pathExpr = (PathExpression)predicate;
List steps = pathExpr.getSteps();
OrderBy orderBy = pathExpr.getOrderBy();
assert steps.size() > 1; // 1 or 0 would have been collapsed ...
Component firstStep = steps.get(0).collapse();
if (firstStep instanceof ContextItem) {
// Remove the context and retry ...
return translatePredicate(new PathExpression(true, steps.subList(1, steps.size()), orderBy), tableName, where);
}
if (firstStep instanceof NameTest) {
// Special case where this is similar to '[a/@id]'
NameTest childName = (NameTest)firstStep;
String alias = newAlias();
builder.joinAllNodesAs(alias).onChildNode(tableName, alias);
if (!childName.isWildcard()) {
where.nodeName(alias).isEqualTo(nameFrom(childName));
}
return translatePredicate(new PathExpression(true, steps.subList(1, steps.size()), orderBy), alias, where);
}
if (firstStep instanceof DescendantOrSelf) {
// Special case where this is similar to '[a/@id]'
String alias = newAlias();
builder.joinAllNodesAs(alias).onDescendant(tableName, alias);
return translatePredicate(new PathExpression(true, steps.subList(1, steps.size()), orderBy), alias, where);
}
// Add the join ...
String alias = newAlias();
builder.joinAllNodesAs(alias).onDescendant(tableName, alias);
// Now add the criteria ...
translatePathExpressionConstraint(pathExpr, where, alias);
} else {
throw new InvalidQueryException(query, "Unsupported criteria '" + predicate + "'");
}
return tableName;
}
/**
* Determine if the predicates contain any expressions that cannot be put into a LIKE constraint on the path.
*
* @param predicates the predicates
* @return true if the supplied predicates can be handled entirely in the LIKE constraint on the path, or false if they have
* to be handled as other criteria
*/
protected boolean appliesToPathConstraint( List predicates ) {
if (predicates.isEmpty()) return true;
if (predicates.size() > 1) return false;
assert predicates.size() == 1;
Component predicate = predicates.get(0);
if (predicate instanceof Literal && ((Literal)predicate).isInteger()) return true;
if (predicate instanceof NameTest && ((NameTest)predicate).isWildcard()) return true;
return false;
}
protected boolean translatePathExpressionConstraint( PathExpression pathExrp,
ConstraintBuilder where,
String tableName ) {
RelativePathLikeExpressions expressions = relativePathLikeExpressions(pathExrp);
if (expressions.isEmpty()) return false;
where = where.openParen();
boolean first = true;
int number = 0;
for (String path : expressions) {
if (path == null || path.length() == 0 || path.equals("%/") || path.equals("%/%") || path.equals("%//%")) continue;
if (first) first = false;
else where.or();
if (path.indexOf('%') != -1 || path.indexOf('_') != -1) {
where.path(tableName).isLike(path);
switch (expressions.depthMode) {
case AT_LEAST:
where.and().depth(tableName).isGreaterThanOrEqualTo().cast(expressions.depth).asLong();
break;
case EXACT:
where.and().depth(tableName).isEqualTo().cast(expressions.depth).asLong();
break;
case DEFAULT:
// don't have to add the DEPTH criteria ...
break;
}
} else {
where.path(tableName).isEqualTo(path);
}
++number;
}
if (number > 0) where.closeParen();
return true;
}
protected static enum DepthMode {
DEFAULT,
EXACT,
AT_LEAST;
}
protected static class RelativePathLikeExpressions implements Iterable {
protected final List paths;
protected final int depth;
protected final DepthMode depthMode;
protected RelativePathLikeExpressions() {
this.paths = null;
this.depth = 0;
this.depthMode = DepthMode.DEFAULT;
}
protected RelativePathLikeExpressions( String[] paths,
int depth,
DepthMode depthMode ) {
this.paths = Arrays.asList(paths);
this.depth = depth;
this.depthMode = depthMode;
}
protected boolean isEmpty() {
return paths == null || paths.isEmpty();
}
@Override
public Iterator iterator() {
return paths.iterator();
}
}
protected RelativePathLikeExpressions relativePathLikeExpressions( PathExpression pathExpression ) {
List steps = pathExpression.getSteps();
if (steps.isEmpty()) return new RelativePathLikeExpressions();
if (steps.size() == 1 && steps.get(0) instanceof DescendantOrSelf) return new RelativePathLikeExpressions();
PathLikeBuilder builder = new SinglePathLikeBuilder();
int depth = 0;
DepthMode depthMode = DepthMode.EXACT;
for (Iterator iterator = steps.iterator(); iterator.hasNext();) {
StepExpression step = iterator.next();
if (step instanceof DescendantOrSelf) {
++depth;
depthMode = DepthMode.DEFAULT;
if (builder.isEmpty()) {
builder.append("%/");
} else {
if (iterator.hasNext()) {
builder.append('/');
builder = new DualPathLikeBuilder(builder.clone(), builder.append("%"));
} else {
builder.append('/').append('%');
}
}
} else if (step instanceof AxisStep) {
++depth;
AxisStep axis = (AxisStep)step;
NodeTest nodeTest = axis.getNodeTest();
assert !(nodeTest instanceof ElementTest);
if (nodeTest instanceof NameTest) {
NameTest nameTest = (NameTest)nodeTest;
builder.append('/');
boolean addSns = true;
if (nameTest.getPrefixTest() != null) {
builder.append(nameTest.getPrefixTest()).append(':');
}
if (nameTest.getLocalTest() != null) {
builder.append(nameTest.getLocalTest());
} else {
builder.append('%');
addSns = false;
}
List predicates = axis.getPredicates();
boolean addedSns = false;
if (!predicates.isEmpty()) {
assert predicates.size() == 1;
Component predicate = predicates.get(0);
if (predicate instanceof Literal && ((Literal)predicate).isInteger()) {
builder.append('[').append(((Literal)predicate).getValue()).append(']');
addedSns = true;
}
}
if (addSns && !addedSns) {
builder.append("[%]");
}
}
} else if (step instanceof FilterStep) {
FilterStep filter = (FilterStep)step;
Component primary = filter.getPrimaryExpression();
if (primary instanceof ContextItem) {
continue; // ignore this '.'
} else if (primary instanceof ParenthesizedExpression) {
ParenthesizedExpression paren = (ParenthesizedExpression)primary;
Component wrapped = paren.getWrapped().collapse();
if (wrapped instanceof AttributeNameTest) {
// ignore this; handled earlier ...
} else if (wrapped instanceof BinaryComponent) {
List names = extractElementNames((BinaryComponent)wrapped);
if (names.size() >= 1) {
PathLikeBuilder orig = builder.clone();
builder.append('/').append(nameFrom(names.get(0)));
if (names.size() > 1) {
for (NameTest name : names.subList(1, names.size())) {
builder = new DualPathLikeBuilder(orig.clone().append('/').append(nameFrom(name)), builder);
}
}
}
} else {
throw new InvalidQueryException(query,
"A parenthesized expression of this type is not supported in the primary path expression; therefore '"
+ primary + "' is not valid");
}
}
}
}
return new RelativePathLikeExpressions(builder.getPaths(), depth, depthMode);
}
protected static interface PathLikeBuilder {
PathLikeBuilder append( String string );
PathLikeBuilder append( char c );
boolean isEmpty();
PathLikeBuilder clone();
String[] getPaths();
}
protected static class SinglePathLikeBuilder implements PathLikeBuilder {
private final StringBuilder builder = new StringBuilder();
private char lastChar;
@Override
public SinglePathLikeBuilder append( String string ) {
builder.append(string);
if (string.length() > 0) lastChar = string.charAt(string.length() - 1);
return this;
}
@Override
public SinglePathLikeBuilder append( char c ) {
if (lastChar != c) {
builder.append(c);
lastChar = c;
}
return this;
}
@Override
public boolean isEmpty() {
return builder.length() == 0;
}
@Override
public SinglePathLikeBuilder clone() {
return new SinglePathLikeBuilder().append(builder.toString());
}
@Override
public String toString() {
return builder.toString();
}
@Override
public String[] getPaths() {
return isEmpty() ? new String[] {} : new String[] {builder.toString()};
}
}
protected static class DualPathLikeBuilder implements PathLikeBuilder {
private final PathLikeBuilder builder1;
private final PathLikeBuilder builder2;
protected DualPathLikeBuilder( PathLikeBuilder builder1,
PathLikeBuilder builder2 ) {
this.builder1 = builder1;
this.builder2 = builder2;
}
@Override
public DualPathLikeBuilder append( String string ) {
builder1.append(string);
builder2.append(string);
return this;
}
@Override
public DualPathLikeBuilder append( char c ) {
builder1.append(c);
builder2.append(c);
return this;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public DualPathLikeBuilder clone() {
return new DualPathLikeBuilder(builder1.clone(), builder2.clone());
}
@Override
public String[] getPaths() {
String[] paths1 = builder1.getPaths();
String[] paths2 = builder2.getPaths();
String[] result = new String[paths1.length + paths2.length];
System.arraycopy(paths1, 0, result, 0, paths1.length);
System.arraycopy(paths2, 0, result, paths1.length, paths2.length);
return result;
}
}
protected String nameFrom( NameTest name ) {
String prefix = name.getPrefixTest();
String local = name.getLocalTest();
assert local != null;
return (prefix != null ? prefix + ":" : "") + local;
}
protected String newAlias() {
String root = "nodeSet";
int num = 1;
String alias = root + num;
while (aliases.contains(alias)) {
num += 1;
alias = root + num;
}
aliases.add(alias);
return alias;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy