View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.jcr.xpath;
25  
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Collections;
29  import java.util.HashMap;
30  import java.util.HashSet;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  import org.modeshape.graph.property.PropertyType;
36  import org.modeshape.graph.query.QueryBuilder;
37  import org.modeshape.graph.query.QueryBuilder.ConstraintBuilder;
38  import org.modeshape.graph.query.QueryBuilder.OrderByBuilder;
39  import org.modeshape.graph.query.QueryBuilder.OrderByOperandBuilder;
40  import org.modeshape.graph.query.model.AllNodes;
41  import org.modeshape.graph.query.model.Operator;
42  import org.modeshape.graph.query.model.Query;
43  import org.modeshape.graph.query.model.QueryCommand;
44  import org.modeshape.graph.query.model.TypeSystem;
45  import org.modeshape.graph.query.parse.InvalidQueryException;
46  import org.modeshape.jcr.xpath.XPath.And;
47  import org.modeshape.jcr.xpath.XPath.AttributeNameTest;
48  import org.modeshape.jcr.xpath.XPath.AxisStep;
49  import org.modeshape.jcr.xpath.XPath.BinaryComponent;
50  import org.modeshape.jcr.xpath.XPath.Comparison;
51  import org.modeshape.jcr.xpath.XPath.Component;
52  import org.modeshape.jcr.xpath.XPath.ContextItem;
53  import org.modeshape.jcr.xpath.XPath.DescendantOrSelf;
54  import org.modeshape.jcr.xpath.XPath.ElementTest;
55  import org.modeshape.jcr.xpath.XPath.Except;
56  import org.modeshape.jcr.xpath.XPath.FilterStep;
57  import org.modeshape.jcr.xpath.XPath.FunctionCall;
58  import org.modeshape.jcr.xpath.XPath.Intersect;
59  import org.modeshape.jcr.xpath.XPath.Literal;
60  import org.modeshape.jcr.xpath.XPath.NameTest;
61  import org.modeshape.jcr.xpath.XPath.NodeTest;
62  import org.modeshape.jcr.xpath.XPath.Or;
63  import org.modeshape.jcr.xpath.XPath.OrderBy;
64  import org.modeshape.jcr.xpath.XPath.OrderBySpec;
65  import org.modeshape.jcr.xpath.XPath.ParenthesizedExpression;
66  import org.modeshape.jcr.xpath.XPath.PathExpression;
67  import org.modeshape.jcr.xpath.XPath.StepExpression;
68  import org.modeshape.jcr.xpath.XPath.Union;
69  
70  /**
71   * A component that translates an {@link XPath} abstract syntax model representation into a {@link QueryCommand ModeShape abstract
72   * query model}.
73   */
74  public class XPathToQueryTranslator {
75  
76      protected static final Map<NameTest, String> CAST_FUNCTION_NAME_TO_TYPE;
77  
78      static {
79          Map<NameTest, String> map = new HashMap<NameTest, String>();
80          map.put(new NameTest("fn", "string"), PropertyType.STRING.getName().toUpperCase());
81          map.put(new NameTest("xs", "string"), PropertyType.STRING.getName().toUpperCase());
82          map.put(new NameTest("xs", "base64Binary"), PropertyType.BINARY.getName().toUpperCase());
83          map.put(new NameTest("xs", "double"), PropertyType.DOUBLE.getName().toUpperCase());
84          map.put(new NameTest("xs", "long"), PropertyType.LONG.getName().toUpperCase());
85          map.put(new NameTest("xs", "boolean"), PropertyType.BOOLEAN.getName().toUpperCase());
86          map.put(new NameTest("xs", "dateTime"), PropertyType.DATE.getName().toUpperCase());
87          map.put(new NameTest("xs", "string"), PropertyType.PATH.getName().toUpperCase());
88          map.put(new NameTest("xs", "string"), PropertyType.NAME.getName().toUpperCase());
89          map.put(new NameTest("xs", "IDREF"), PropertyType.REFERENCE.getName().toUpperCase());
90          CAST_FUNCTION_NAME_TO_TYPE = Collections.unmodifiableMap(map);
91      }
92  
93      private final String query;
94      private final TypeSystem typeSystem;
95      private final QueryBuilder builder;
96      private final Set<String> aliases = new HashSet<String>();
97  
98      public XPathToQueryTranslator( TypeSystem context,
99                                     String query ) {
100         this.query = query;
101         this.typeSystem = context;
102         this.builder = new QueryBuilder(this.typeSystem);
103     }
104 
105     public QueryCommand createQuery( Component xpath ) {
106         if (xpath instanceof BinaryComponent) {
107             BinaryComponent binary = (BinaryComponent)xpath;
108             if (binary instanceof Union) {
109                 createQuery(binary.getLeft());
110                 builder.union();
111                 createQuery(binary.getRight());
112                 return builder.query();
113             } else if (binary instanceof Intersect) {
114                 createQuery(binary.getLeft());
115                 builder.intersect();
116                 createQuery(binary.getRight());
117                 return builder.query();
118             } else if (binary instanceof Except) {
119                 createQuery(binary.getLeft());
120                 builder.except();
121                 createQuery(binary.getRight());
122                 return builder.query();
123             }
124         } else if (xpath instanceof PathExpression) {
125             translate((PathExpression)xpath);
126             return builder.query();
127         }
128         // unexpected component ...
129         throw new InvalidQueryException(query,
130                                         "Acceptable XPath queries must lead with a path expression or must be a union, intersect or except");
131     }
132 
133     protected void translate( PathExpression pathExpression ) {
134         List<StepExpression> steps = pathExpression.getSteps();
135         assert !steps.isEmpty();
136         if (!pathExpression.isRelative()) {
137             // Absolute path must start with "/jcr:root/" or "//" ...
138             Component first = steps.get(0).collapse();
139             // Result will be NameTest("jcr","root") or DescendantOrSelf ...
140             if (first instanceof DescendantOrSelf) {
141                 // do nothing ...
142             } else if (first instanceof NameTest && steps.size() == 1 && ((NameTest)first).matches("jcr", "root")) {
143                 // We can actually remove this first step, since relative paths are relative to the root ...
144                 steps = steps.subList(1, steps.size());
145             } else if (first instanceof NameTest && steps.size() > 1 && ((NameTest)first).matches("jcr", "root")) {
146                 // We can actually remove this first step, since relative paths are relative to the root ...
147                 steps = steps.subList(1, steps.size());
148             } else {
149                 throw new InvalidQueryException(query, "An absolute path expression must start with '//' or '/jcr:root/...'");
150             }
151         }
152 
153         // Walk the steps along the path expression ...
154         ConstraintBuilder where = builder.where();
155         List<StepExpression> path = new ArrayList<StepExpression>();
156         String tableName = null;
157         for (StepExpression step : steps) {
158             if (step instanceof AxisStep) {
159                 AxisStep axis = (AxisStep)step;
160                 NodeTest nodeTest = axis.getNodeTest();
161                 if (nodeTest instanceof NameTest) {
162                     if (appliesToPathConstraint(axis.getPredicates())) {
163                         // Everything in this axis/step applies to the path, so add it and we'll translate it below ...
164                         path.add(step);
165                     } else {
166                         // The constraints are more complicated than can be applied to the path ...
167                         // if (!nameTest.isWildcard()) {
168                         // There is a non-wildcard name test that we still need to add to the path ...
169                         path.add(step);
170                         // }
171                         // We need to define a new source/table ...
172                         tableName = translateSource(tableName, path, where);
173                         translatePredicates(axis.getPredicates(), tableName, where);
174                         path.clear();
175                     }
176                 } else if (nodeTest instanceof ElementTest) {
177                     // We need to build a new source with the partial path we have so far ...
178                     tableName = translateElementTest((ElementTest)nodeTest, path, where);
179                     translatePredicates(axis.getPredicates(), tableName, where);
180                     path.clear();
181                 } else if (nodeTest instanceof AttributeNameTest) {
182                     AttributeNameTest attributeName = (AttributeNameTest)nodeTest;
183                     builder.select(nameFrom(attributeName.getNameTest()));
184                 } else {
185                     throw new InvalidQueryException(query, "The '" + step + "' step is not supported");
186                 }
187             } else if (step instanceof FilterStep) {
188                 FilterStep filter = (FilterStep)step;
189                 Component primary = filter.getPrimaryExpression();
190                 List<Component> predicates = filter.getPredicates();
191                 if (primary instanceof ContextItem) {
192                     if (appliesToPathConstraint(predicates)) {
193                         // Can ignore the '.' ...
194                     } else {
195                         // The constraints are more complicated, so we need to define a new source/table ...
196                         path.add(step);
197                         tableName = translateSource(tableName, path, where);
198                         translatePredicates(predicates, tableName, where);
199                         path.clear();
200                     }
201                 } else if (primary instanceof Literal) {
202                     throw new InvalidQueryException(query,
203                                                     "A literal is not supported in the primary path expression; therefore '"
204                                                     + primary + "' is not valid");
205                 } else if (primary instanceof FunctionCall) {
206                     throw new InvalidQueryException(query,
207                                                     "A function call is not supported in the primary path expression; therefore '"
208                                                     + primary + "' is not valid");
209                 } else if (primary instanceof ParenthesizedExpression) {
210                     // This can be used to define an OR-ed set of expressions defining select columns ...
211                     ParenthesizedExpression paren = (ParenthesizedExpression)primary;
212                     Component wrapped = paren.getWrapped().collapse();
213                     if (wrapped instanceof AttributeNameTest) {
214                         AttributeNameTest attributeName = (AttributeNameTest)wrapped;
215                         builder.select(nameFrom(attributeName.getNameTest()));
216                     } else if (wrapped instanceof BinaryComponent) {
217                         for (AttributeNameTest attributeName : extractAttributeNames((BinaryComponent)wrapped)) {
218                             builder.select(nameFrom(attributeName.getNameTest()));
219                         }
220                         path.add(filter); // in case any element names are there
221                     } else {
222                         throw new InvalidQueryException(query,
223                                                         "A parenthesized expression of this type is not supported in the primary path expression; therefore '"
224                                                         + primary + "' is not valid");
225                     }
226                 }
227 
228             } else {
229                 path.add(step);
230             }
231         }
232         if (steps.isEmpty() || !path.isEmpty()) {
233             translateSource(tableName, path, where);
234         }
235         where.end();
236 
237         // Process the order-by clause ...
238         OrderBy orderBy = pathExpression.getOrderBy();
239         if (orderBy != null) {
240             OrderByBuilder orderByBuilder = builder.orderBy();
241             for (OrderBySpec spec : orderBy) {
242                 OrderByOperandBuilder operandBuilder = null;
243                 switch (spec.getOrder()) {
244                     case ASCENDING:
245                         operandBuilder = orderByBuilder.ascending();
246                         break;
247                     case DESCENDING:
248                         operandBuilder = orderByBuilder.descending();
249                         break;
250                 }
251                 assert operandBuilder != null;
252                 if (spec.getAttributeName() != null) {
253                     // This order by is defined by an attribute ...
254                     NameTest attribute = spec.getAttributeName();
255                     assert !attribute.isWildcard();
256                     if (attribute.matches("jcr", "path")) {
257                         String pathOf = tableName;
258                         if (pathOf == null) pathOf = aliases.iterator().next();
259                         operandBuilder.path(pathOf);
260                     } else {
261                         operandBuilder.propertyValue(tableName, attribute.toString());
262                         builder.select(tableName + "." + attribute.toString());
263                     }
264                 } else {
265                     // This order-by is defined by a "jcr:score" function ...
266                     FunctionCall scoreFunction = spec.getScoreFunction();
267                     assert scoreFunction != null;
268                     List<Component> args = scoreFunction.getParameters();
269                     String nameOfTableToScore = tableName;
270                     if (!args.isEmpty()) {
271                         if (args.size() == 1 && args.get(0) instanceof NameTest) {
272                             // Just the table name ...
273                             NameTest tableNameTest = (NameTest)args.get(0);
274                             nameOfTableToScore = tableNameTest.toString();
275                         }
276                     }
277                     operandBuilder.fullTextSearchScore(nameOfTableToScore);
278                 }
279             }
280             orderByBuilder.end();
281         }
282         // Try building this query, because we need to check the # of columns selected and the # of sources ...
283         Query query = (Query)builder.query();
284         if (query.columns().isEmpty() && query.source() instanceof AllNodes) {
285             // This is basically 'SELECT * FROM __ALLNODES__", which means that no type was explicitly specified and
286             // nothing was selected from that type. According to JCR 1.0 Section 6.6.3.1, this equates to
287             // 'SELECT * FROM [nt:base]', and since there is just one property on nt:base (but many on __ALLNODES__)
288             // this really equates to 'SELECT [jcr:primaryType] FROM __ALLNODES__'.
289             builder.select("jcr:primaryType");
290         }
291     }
292 
293     /**
294      * Find any {@link AttributeNameTest attribute names} that have been unioned together (with '|'). Any other combination of
295      * objects results in an error.
296      * 
297      * @param binary the binary component
298      * @return the list of attribute names, if that's all that's in the supplied component; may be empty
299      */
300     protected List<AttributeNameTest> extractAttributeNames( BinaryComponent binary ) {
301         List<AttributeNameTest> results = new ArrayList<AttributeNameTest>();
302         boolean failed = false;
303         if (binary instanceof Union) {
304             for (int i = 0; i != 2; ++i) {
305                 Component comp = i == 0 ? binary.getLeft() : binary.getRight();
306                 comp = comp.collapse();
307                 if (comp instanceof Union) {
308                     results.addAll(extractAttributeNames((BinaryComponent)comp));
309                 } else if (comp instanceof AttributeNameTest) {
310                     results.add((AttributeNameTest)comp);
311                 } else if (comp instanceof NameTest) {
312                     // Element names, which are fine but we'll ignore
313                 } else {
314                     failed = true;
315                     break;
316                 }
317             }
318         } else {
319             failed = true;
320         }
321         if (failed) {
322             throw new InvalidQueryException(query,
323                                             "A parenthesized expression in a path step may only contain ORed and ANDed attribute names or element names; therefore '"
324                                             + binary + "' is not valid");
325         }
326         return results;
327     }
328 
329     /**
330      * Find any {@link NameTest element names} that have been unioned together (with '|'). Any other combination of objects
331      * results in an error.
332      * 
333      * @param binary the binary component
334      * @return the list of attribute names, if that's all that's in the supplied component; may be empty
335      */
336     protected List<NameTest> extractElementNames( BinaryComponent binary ) {
337         List<NameTest> results = new ArrayList<NameTest>();
338         boolean failed = false;
339         if (binary instanceof Union) {
340             for (int i = 0; i != 2; ++i) {
341                 Component comp = i == 0 ? binary.getLeft() : binary.getRight();
342                 comp = comp.collapse();
343                 if (comp instanceof Union) {
344                     results.addAll(extractElementNames((BinaryComponent)comp));
345                 } else if (comp instanceof AttributeNameTest) {
346                     // ignore these ...
347                 } else if (comp instanceof NameTest) {
348                     results.add((NameTest)comp);
349                 } else {
350                     failed = true;
351                     break;
352                 }
353             }
354         } else {
355             failed = true;
356         }
357         if (failed) {
358             throw new InvalidQueryException(query,
359                                             "A parenthesized expression in a path step may only contain ORed element names; therefore '"
360                                             + binary + "' is not valid");
361         }
362         return results;
363     }
364 
365     protected String translateSource( String tableName,
366                                       List<StepExpression> path,
367                                       ConstraintBuilder where ) {
368         if (path.size() == 0) {
369             // This is a query against the root node ...
370             String alias = newAlias();
371             builder.fromAllNodesAs(alias);
372             where.path(alias).isEqualTo("/");
373             return alias;
374         }
375         String alias = newAlias();
376         if (tableName != null) {
377             // This is after some element(...) steps, so we need to join ...
378             builder.joinAllNodesAs(alias);
379         } else {
380             // This is the only part of the query ...
381             builder.fromAllNodesAs(alias);
382         }
383         tableName = alias;
384         if (path.size() == 1 && path.get(0).collapse() instanceof NameTest) {
385             // Node immediately below root ...
386             NameTest nodeName = (NameTest)path.get(0).collapse();
387             where.path(alias).isLike("/" + nameFrom(nodeName) + "[%]");
388         } else if (path.size() == 2 && path.get(0) instanceof DescendantOrSelf && path.get(1).collapse() instanceof NameTest) {
389             // Node anywhere ...
390             NameTest nodeName = (NameTest)path.get(1).collapse();
391             if (!nodeName.isWildcard()) {
392                 where.nodeName(alias).isEqualTo(nameFrom(nodeName));
393             }
394         } else {
395             // Must be just a bunch of descendant-or-self, axis and filter steps ...
396             translatePathExpressionConstraint(new PathExpression(true, path, null), where, alias);
397         }
398         return tableName;
399     }
400 
401     protected String translateElementTest( ElementTest elementTest,
402                                            List<StepExpression> pathConstraint,
403                                            ConstraintBuilder where ) {
404         String tableName = null;
405         NameTest typeName = elementTest.getTypeName();
406         if (typeName.isWildcard()) {
407             tableName = newAlias();
408             builder.fromAllNodesAs(tableName);
409         } else {
410             if (typeName.getLocalTest() == null) {
411                 throw new InvalidQueryException(
412                                                 query,
413                                                 "The '"
414                                                 + elementTest
415                                                 + "' clause uses a partial wildcard in the type name, but only a wildcard on the whole name is supported");
416             }
417             tableName = nameFrom(typeName);
418             builder.from(tableName);
419         }
420         if (elementTest.getElementName() != null) {
421             NameTest nodeName = elementTest.getElementName();
422             if (!nodeName.isWildcard()) {
423                 where.nodeName(tableName).isEqualTo(nameFrom(nodeName));
424             }
425         }
426         if (pathConstraint.isEmpty()) {
427             where.depth(tableName).isEqualTo(1);
428         } else {
429             List<StepExpression> path = new ArrayList<StepExpression>(pathConstraint);
430             if (!path.isEmpty() && path.get(path.size() - 1) instanceof AxisStep) {
431                 // Element test should always apply to descendants, never to self, so add a descedant
432                 path.add(new AxisStep(new NameTest(null, null), Collections.<Component>emptyList()));
433             }
434             translatePathExpressionConstraint(new PathExpression(true, path, null), where, tableName);
435         }
436         return tableName;
437     }
438 
439     protected void translatePredicates( List<Component> predicates,
440                                         String tableName,
441                                         ConstraintBuilder where ) {
442         assert tableName != null;
443         for (Component predicate : predicates) {
444             translatePredicate(predicate, tableName, where);
445         }
446     }
447 
448     protected String translatePredicate( Component predicate,
449                                          String tableName,
450                                          ConstraintBuilder where ) {
451         predicate = predicate.collapse();
452         assert tableName != null;
453         if (predicate instanceof ParenthesizedExpression) {
454             ParenthesizedExpression paren = (ParenthesizedExpression)predicate;
455             where = where.openParen();
456             translatePredicate(paren.getWrapped(), tableName, where);
457             where.closeParen();
458         } else if (predicate instanceof And) {
459             And and = (And)predicate;
460             where = where.openParen();
461             translatePredicate(and.getLeft(), tableName, where);
462             where.and();
463             translatePredicate(and.getRight(), tableName, where);
464             where.closeParen();
465         } else if (predicate instanceof Or) {
466             Or or = (Or)predicate;
467             where = where.openParen();
468             translatePredicate(or.getLeft(), tableName, where);
469             where.or();
470             translatePredicate(or.getRight(), tableName, where);
471             where.closeParen();
472         } else if (predicate instanceof Union) {
473             Union union = (Union)predicate;
474             where = where.openParen();
475             translatePredicate(union.getLeft(), tableName, where);
476             where.or();
477             translatePredicate(union.getRight(), tableName, where);
478             where.closeParen();
479         } else if (predicate instanceof Literal) {
480             Literal literal = (Literal)predicate;
481             if (literal.isInteger()) return tableName; // do nothing, since this is a path constraint and is handled elsewhere
482         } else if (predicate instanceof AttributeNameTest) {
483             // This adds the criteria that the attribute exists, and adds it to the select ...
484             AttributeNameTest attribute = (AttributeNameTest)predicate;
485             String propertyName = nameFrom(attribute.getNameTest());
486             // There is nothing in the JCR 1.0 spec that says that a property constrain implies it should be included in the
487             // result columns
488             // builder.select(tableName + "." + propertyName);
489             where.hasProperty(tableName, propertyName);
490         } else if (predicate instanceof NameTest) {
491             // This adds the criteria that the child node exists ...
492             NameTest childName = (NameTest)predicate;
493             String alias = newAlias();
494             builder.joinAllNodesAs(alias).onChildNode(tableName, alias);
495             if (!childName.isWildcard()) where.nodeName(alias).isEqualTo(nameFrom(childName));
496             tableName = alias;
497         } else if (predicate instanceof Comparison) {
498             Comparison comparison = (Comparison)predicate;
499             Component left = comparison.getLeft();
500             Component right = comparison.getRight();
501             Operator operator = comparison.getOperator();
502             if (left instanceof Literal) {
503                 Component temp = left;
504                 left = right;
505                 right = temp;
506                 operator = operator.reverse();
507             }
508             if (left instanceof NodeTest) {
509                 NodeTest nodeTest = (NodeTest)left;
510                 String propertyName = null;
511                 if (nodeTest instanceof AttributeNameTest) {
512                     AttributeNameTest attribute = (AttributeNameTest)left;
513                     propertyName = nameFrom(attribute.getNameTest());
514                 } else if (nodeTest instanceof NameTest) {
515                     NameTest nameTest = (NameTest)left;
516                     propertyName = nameFrom(nameTest);
517                 } else {
518                     throw new InvalidQueryException(query,
519                                                     "Left hand side of a comparison must be a name test or attribute name test; therefore '"
520                                                     + comparison + "' is not valid");
521                 }
522                 if (right instanceof Literal) {
523                     String value = ((Literal)right).getValue();
524                     where.propertyValue(tableName, propertyName).is(operator, value);
525                 } else if (right instanceof FunctionCall) {
526                     FunctionCall call = (FunctionCall)right;
527                     NameTest functionName = call.getName();
528                     List<Component> parameters = call.getParameters();
529                     // Is this a cast ...
530                     String castType = CAST_FUNCTION_NAME_TO_TYPE.get(functionName);
531                     if (castType != null) {
532                         if (parameters.size() == 1 && parameters.get(0).collapse() instanceof Literal) {
533                             // The first parameter can be the type name (or table name) ...
534                             Literal value = (Literal)parameters.get(0).collapse();
535                             where.propertyValue(tableName, propertyName).is(operator).cast(value.getValue()).as(castType);
536                         } else {
537                             throw new InvalidQueryException(query, "A cast function requires one literal parameter; therefore '"
538                                                                    + comparison + "' is not valid");
539                         }
540                     } else {
541                         throw new InvalidQueryException(query,
542                                                         "Only the 'jcr:score' function is allowed in a comparison predicate; therefore '"
543                                                         + comparison + "' is not valid");
544                     }
545                 }
546             } else if (left instanceof FunctionCall && right instanceof Literal) {
547                 FunctionCall call = (FunctionCall)left;
548                 NameTest functionName = call.getName();
549                 List<Component> parameters = call.getParameters();
550                 String value = ((Literal)right).getValue();
551                 if (functionName.matches("jcr", "score")) {
552                     String scoreTableName = tableName;
553                     if (parameters.isEmpty()) {
554                         scoreTableName = tableName;
555                     } else if (parameters.size() == 1 && parameters.get(0) instanceof NameTest) {
556                         // The first parameter can be the type name (or table name) ...
557                         NameTest name = (NameTest)parameters.get(0);
558                         if (!name.isWildcard()) scoreTableName = nameFrom(name);
559                     } else {
560                         throw new InvalidQueryException(query,
561                                                         "The 'jcr:score' function may have no parameters or the type name as the only parameter.");
562 
563                     }
564                     where.fullTextSearchScore(scoreTableName).is(operator, value);
565                 } else {
566                     throw new InvalidQueryException(query,
567                                                     "Only the 'jcr:score' function is allowed in a comparison predicate; therefore '"
568                                                     + comparison + "' is not valid");
569                 }
570             }
571         } else if (predicate instanceof FunctionCall) {
572             FunctionCall call = (FunctionCall)predicate;
573             NameTest functionName = call.getName();
574             List<Component> parameters = call.getParameters();
575             Component param1 = parameters.size() > 0 ? parameters.get(0) : null;
576             Component param2 = parameters.size() > 1 ? parameters.get(1) : null;
577             if (functionName.matches(null, "not")) {
578                 if (parameters.size() != 1) {
579                     throw new InvalidQueryException(query, "The 'not' function requires one parameter; therefore '" + predicate
580                                                            + "' is not valid");
581                 }
582                 where = where.not().openParen();
583                 translatePredicate(param1, tableName, where);
584                 where.closeParen();
585             } else if (functionName.matches("jcr", "like")) {
586                 if (parameters.size() != 2) {
587                     throw new InvalidQueryException(query, "The 'jcr:like' function requires two parameters; therefore '"
588                                                            + predicate + "' is not valid");
589                 }
590                 if (!(param1 instanceof AttributeNameTest)) {
591                     throw new InvalidQueryException(query,
592                                                     "The first parameter of 'jcr:like' must be an property reference with the '@' symbol; therefore '"
593                                                     + predicate + "' is not valid");
594                 }
595                 if (!(param2 instanceof Literal)) {
596                     throw new InvalidQueryException(query, "The second parameter of 'jcr:like' must be a literal; therefore '"
597                                                            + predicate + "' is not valid");
598                 }
599                 NameTest attributeName = ((AttributeNameTest)param1).getNameTest();
600                 String value = ((Literal)param2).getValue();
601                 where.propertyValue(tableName, nameFrom(attributeName)).isLike(value);
602             } else if (functionName.matches("jcr", "contains")) {
603                 if (parameters.size() != 2) {
604                     throw new InvalidQueryException(query, "The 'jcr:contains' function requires two parameters; therefore '"
605                                                            + predicate + "' is not valid");
606                 }
607                 if (!(param2 instanceof Literal)) {
608                     throw new InvalidQueryException(query,
609                                                     "The second parameter of 'jcr:contains' must be a literal; therefore '"
610                                                     + predicate + "' is not valid");
611                 }
612                 String value = ((Literal)param2).getValue();
613                 if (param1 instanceof ContextItem) {
614                     // refers to the current node (or table) ...
615                     where.search(tableName, value);
616                 } else if (param1 instanceof AttributeNameTest) {
617                     // refers to an attribute on the current node (or table) ...
618                     NameTest attributeName = ((AttributeNameTest)param1).getNameTest();
619                     where.search(tableName, nameFrom(attributeName), value);
620                 } else if (param1 instanceof NameTest) {
621                     // refers to child node, so we need to add a join ...
622                     String alias = newAlias();
623                     builder.joinAllNodesAs(alias).onChildNode(tableName, alias);
624                     // Now add the criteria ...
625                     where.search(alias, value);
626                     tableName = alias;
627                 } else if (param1 instanceof PathExpression) {
628                     // refers to a descendant node ...
629                     PathExpression pathExpr = (PathExpression)param1;
630                     if (pathExpr.getLastStep().collapse() instanceof AttributeNameTest) {
631                         AttributeNameTest attributeName = (AttributeNameTest)pathExpr.getLastStep().collapse();
632                         pathExpr = pathExpr.withoutLast();
633                         String searchTable = translatePredicate(pathExpr, tableName, where);
634                         if (attributeName.getNameTest().isWildcard()) {
635                             where.search(searchTable, value);
636                         } else {
637                             where.search(searchTable, nameFrom(attributeName.getNameTest()), value);
638                         }
639                     } else {
640                         String searchTable = translatePredicate(param1, tableName, where);
641                         where.search(searchTable, value);
642                     }
643                 } else {
644                     throw new InvalidQueryException(query,
645                                                     "The first parameter of 'jcr:contains' must be a relative path (e.g., '.', an attribute name, a child name, etc.); therefore '"
646                                                     + predicate + "' is not valid");
647                 }
648             } else if (functionName.matches("jcr", "deref")) {
649                 throw new InvalidQueryException(query,
650                                                 "The 'jcr:deref' function is not required by JCR and is not currently supported; therefore '"
651                                                 + predicate + "' is not valid");
652             } else {
653                 throw new InvalidQueryException(query,
654                                                 "Only the 'jcr:like' and 'jcr:contains' functions are allowed in a predicate; therefore '"
655                                                 + predicate + "' is not valid");
656             }
657         } else if (predicate instanceof PathExpression) {
658             // Requires that the descendant node with the relative path does exist ...
659             PathExpression pathExpr = (PathExpression)predicate;
660             List<StepExpression> steps = pathExpr.getSteps();
661             OrderBy orderBy = pathExpr.getOrderBy();
662             assert steps.size() > 1; // 1 or 0 would have been collapsed ...
663             Component firstStep = steps.get(0).collapse();
664             if (firstStep instanceof ContextItem) {
665                 // Remove the context and retry ...
666                 return translatePredicate(new PathExpression(true, steps.subList(1, steps.size()), orderBy), tableName, where);
667             }
668             if (firstStep instanceof NameTest) {
669                 // Special case where this is similar to '[a/@id]'
670                 NameTest childName = (NameTest)firstStep;
671                 String alias = newAlias();
672                 builder.joinAllNodesAs(alias).onChildNode(tableName, alias);
673                 if (!childName.isWildcard()) {
674                     where.nodeName(alias).isEqualTo(nameFrom(childName));
675                 }
676                 return translatePredicate(new PathExpression(true, steps.subList(1, steps.size()), orderBy), alias, where);
677             }
678             if (firstStep instanceof DescendantOrSelf) {
679                 // Special case where this is similar to '[a/@id]'
680                 String alias = newAlias();
681                 builder.joinAllNodesAs(alias).onDescendant(tableName, alias);
682                 return translatePredicate(new PathExpression(true, steps.subList(1, steps.size()), orderBy), alias, where);
683             }
684             // Add the join ...
685             String alias = newAlias();
686             builder.joinAllNodesAs(alias).onDescendant(tableName, alias);
687             // Now add the criteria ...
688             translatePathExpressionConstraint(pathExpr, where, alias);
689         } else {
690             throw new InvalidQueryException(query, "Unsupported criteria '" + predicate + "'");
691         }
692         return tableName;
693     }
694 
695     /**
696      * Determine if the predicates contain any expressions that cannot be put into a LIKE constraint on the path.
697      * 
698      * @param predicates the predicates
699      * @return true if the supplied predicates can be handled entirely in the LIKE constraint on the path, or false if they have
700      *         to be handled as other criteria
701      */
702     protected boolean appliesToPathConstraint( List<Component> predicates ) {
703         if (predicates.isEmpty()) return true;
704         if (predicates.size() > 1) return false;
705         assert predicates.size() == 1;
706         Component predicate = predicates.get(0);
707         if (predicate instanceof Literal && ((Literal)predicate).isInteger()) return true;
708         if (predicate instanceof NameTest && ((NameTest)predicate).isWildcard()) return true;
709         return false;
710     }
711 
712     protected boolean translatePathExpressionConstraint( PathExpression pathExrp,
713                                                          ConstraintBuilder where,
714                                                          String tableName ) {
715         RelativePathLikeExpressions expressions = relativePathLikeExpressions(pathExrp);
716         if (expressions.isEmpty()) return false;
717         where = where.openParen();
718         boolean first = true;
719         int number = 0;
720         for (String path : expressions) {
721             if (path == null || path.length() == 0 || path.equals("%/") || path.equals("%/%") || path.equals("%//%")) continue;
722             if (first) first = false;
723             else where.or();
724             if (path.indexOf('%') != -1 || path.indexOf('_') != -1) {
725                 where.path(tableName).isLike(path);
726                 switch (expressions.depthMode) {
727                     case AT_LEAST:
728                         where.and().depth(tableName).isGreaterThanOrEqualTo().cast(expressions.depth).asLong();
729                         break;
730                     case EXACT:
731                         where.and().depth(tableName).isEqualTo().cast(expressions.depth).asLong();
732                         break;
733                     case DEFAULT:
734                         // don't have to add the DEPTH criteria ...
735                         break;
736                 }
737             } else {
738                 where.path(tableName).isEqualTo(path);
739             }
740             ++number;
741         }
742         if (number > 0) where.closeParen();
743         return true;
744     }
745 
746     protected static enum DepthMode {
747         DEFAULT,
748         EXACT,
749         AT_LEAST;
750     }
751 
752     protected static class RelativePathLikeExpressions implements Iterable<String> {
753         protected final List<String> paths;
754         protected final int depth;
755         protected final DepthMode depthMode;
756 
757         protected RelativePathLikeExpressions() {
758             this.paths = null;
759             this.depth = 0;
760             this.depthMode = DepthMode.DEFAULT;
761         }
762 
763         protected RelativePathLikeExpressions( String[] paths,
764                                                int depth,
765                                                DepthMode depthMode ) {
766             this.paths = Arrays.asList(paths);
767             this.depth = depth;
768             this.depthMode = depthMode;
769         }
770 
771         protected boolean isEmpty() {
772             return paths == null || paths.isEmpty();
773         }
774 
775         public Iterator<String> iterator() {
776             return paths.iterator();
777         }
778     }
779 
780     protected RelativePathLikeExpressions relativePathLikeExpressions( PathExpression pathExpression ) {
781         List<StepExpression> steps = pathExpression.getSteps();
782         if (steps.isEmpty()) return new RelativePathLikeExpressions();
783         if (steps.size() == 1 && steps.get(0) instanceof DescendantOrSelf) return new RelativePathLikeExpressions();
784         PathLikeBuilder builder = new SinglePathLikeBuilder();
785         int depth = 0;
786         DepthMode depthMode = DepthMode.EXACT;
787         for (Iterator<StepExpression> iterator = steps.iterator(); iterator.hasNext();) {
788             StepExpression step = iterator.next();
789             if (step instanceof DescendantOrSelf) {
790                 ++depth;
791                 depthMode = DepthMode.DEFAULT;
792                 if (builder.isEmpty()) {
793                     builder.append("%/");
794                 } else {
795                     if (iterator.hasNext()) {
796                         builder.append('/');
797                         builder = new DualPathLikeBuilder(builder.clone(), builder.append("%"));
798                     } else {
799                         builder.append('/').append('%');
800                     }
801                 }
802             } else if (step instanceof AxisStep) {
803                 ++depth;
804                 AxisStep axis = (AxisStep)step;
805                 NodeTest nodeTest = axis.getNodeTest();
806                 assert !(nodeTest instanceof ElementTest);
807                 if (nodeTest instanceof NameTest) {
808                     NameTest nameTest = (NameTest)nodeTest;
809                     builder.append('/');
810                     boolean addSns = true;
811                     if (nameTest.getPrefixTest() != null) {
812                         builder.append(nameTest.getPrefixTest()).append(':');
813                     }
814                     if (nameTest.getLocalTest() != null) {
815                         builder.append(nameTest.getLocalTest());
816                     } else {
817                         builder.append('%');
818                         addSns = false;
819                     }
820                     List<Component> predicates = axis.getPredicates();
821                     boolean addedSns = false;
822                     if (!predicates.isEmpty()) {
823                         assert predicates.size() == 1;
824                         Component predicate = predicates.get(0);
825                         if (predicate instanceof Literal && ((Literal)predicate).isInteger()) {
826                             builder.append('[').append(((Literal)predicate).getValue()).append(']');
827                             addedSns = true;
828                         }
829                     }
830                     if (addSns && !addedSns) {
831                         builder.append("[%]");
832                     }
833                 }
834             } else if (step instanceof FilterStep) {
835                 FilterStep filter = (FilterStep)step;
836                 Component primary = filter.getPrimaryExpression();
837                 if (primary instanceof ContextItem) {
838                     continue; // ignore this '.'
839                 } else if (primary instanceof ParenthesizedExpression) {
840                     ParenthesizedExpression paren = (ParenthesizedExpression)primary;
841                     Component wrapped = paren.getWrapped().collapse();
842                     if (wrapped instanceof AttributeNameTest) {
843                         // ignore this; handled earlier ...
844                     } else if (wrapped instanceof BinaryComponent) {
845                         List<NameTest> names = extractElementNames((BinaryComponent)wrapped);
846                         if (names.size() >= 1) {
847                             PathLikeBuilder orig = builder.clone();
848                             builder.append('/').append(nameFrom(names.get(0)));
849                             if (names.size() > 1) {
850                                 for (NameTest name : names.subList(1, names.size())) {
851                                     builder = new DualPathLikeBuilder(orig.clone().append('/').append(nameFrom(name)), builder);
852                                 }
853                             }
854                         }
855                     } else {
856                         throw new InvalidQueryException(query,
857                                                         "A parenthesized expression of this type is not supported in the primary path expression; therefore '"
858                                                         + primary + "' is not valid");
859                     }
860                 }
861             }
862         }
863         return new RelativePathLikeExpressions(builder.getPaths(), depth, depthMode);
864     }
865 
866     protected static interface PathLikeBuilder {
867         PathLikeBuilder append( String string );
868 
869         PathLikeBuilder append( char c );
870 
871         boolean isEmpty();
872 
873         PathLikeBuilder clone();
874 
875         String[] getPaths();
876     }
877 
878     protected static class SinglePathLikeBuilder implements PathLikeBuilder {
879         private final StringBuilder builder = new StringBuilder();
880         private char lastChar;
881 
882         public SinglePathLikeBuilder append( String string ) {
883             builder.append(string);
884             if (string.length() > 0) lastChar = string.charAt(string.length() - 1);
885             return this;
886         }
887 
888         public SinglePathLikeBuilder append( char c ) {
889             if (lastChar != c) {
890                 builder.append(c);
891                 lastChar = c;
892             }
893             return this;
894         }
895 
896         public boolean isEmpty() {
897             return builder.length() == 0;
898         }
899 
900         @Override
901         public SinglePathLikeBuilder clone() {
902             return new SinglePathLikeBuilder().append(builder.toString());
903         }
904 
905         @Override
906         public String toString() {
907             return builder.toString();
908         }
909 
910         public String[] getPaths() {
911             return isEmpty() ? new String[] {} : new String[] {builder.toString()};
912         }
913     }
914 
915     protected static class DualPathLikeBuilder implements PathLikeBuilder {
916         private final PathLikeBuilder builder1;
917         private final PathLikeBuilder builder2;
918 
919         protected DualPathLikeBuilder( PathLikeBuilder builder1,
920                                        PathLikeBuilder builder2 ) {
921             this.builder1 = builder1;
922             this.builder2 = builder2;
923         }
924 
925         public DualPathLikeBuilder append( String string ) {
926             builder1.append(string);
927             builder2.append(string);
928             return this;
929         }
930 
931         public DualPathLikeBuilder append( char c ) {
932             builder1.append(c);
933             builder2.append(c);
934             return this;
935         }
936 
937         public boolean isEmpty() {
938             return false;
939         }
940 
941         @Override
942         public DualPathLikeBuilder clone() {
943             return new DualPathLikeBuilder(builder1.clone(), builder2.clone());
944         }
945 
946         public String[] getPaths() {
947             String[] paths1 = builder1.getPaths();
948             String[] paths2 = builder2.getPaths();
949             String[] result = new String[paths1.length + paths2.length];
950             System.arraycopy(paths1, 0, result, 0, paths1.length);
951             System.arraycopy(paths2, 0, result, paths1.length, paths2.length);
952             return result;
953         }
954     }
955 
956     protected String nameFrom( NameTest name ) {
957         String prefix = name.getPrefixTest();
958         String local = name.getLocalTest();
959         assert local != null;
960         return (prefix != null ? prefix + ":" : "") + local;
961     }
962 
963     protected String newAlias() {
964         String root = "nodeSet";
965         int num = 1;
966         String alias = root + num;
967         while (aliases.contains(alias)) {
968             num += 1;
969             alias = root + num;
970         }
971         aliases.add(alias);
972         return alias;
973     }
974 }