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                     operandBuilder.propertyValue(tableName, attribute.toString());
257                     builder.select(tableName + "." + attribute.toString());
258                 } else {
259                     // This order-by is defined by a "jcr:score" function ...
260                     FunctionCall scoreFunction = spec.getScoreFunction();
261                     assert scoreFunction != null;
262                     List<Component> args = scoreFunction.getParameters();
263                     String nameOfTableToScore = tableName;
264                     if (!args.isEmpty()) {
265                         if (args.size() == 1 && args.get(0) instanceof NameTest) {
266                             // Just the table name ...
267                             NameTest tableNameTest = (NameTest)args.get(0);
268                             nameOfTableToScore = tableNameTest.toString();
269                         }
270                     }
271                     operandBuilder.fullTextSearchScore(nameOfTableToScore);
272                 }
273             }
274             orderByBuilder.end();
275         }
276         // Try building this query, because we need to check the # of columns selected and the # of sources ...
277         Query query = (Query)builder.query();
278         if (query.columns().isEmpty() && query.source() instanceof AllNodes) {
279             // This is basically 'SELECT * FROM __ALLNODES__", which means that no type was explicitly specified and
280             // nothing was selected from that type. According to JCR 1.0 Section 6.6.3.1, this equates to
281             // 'SELECT * FROM [nt:base]', and since there is just one property on nt:base (but many on __ALLNODES__)
282             // this really equates to 'SELECT [jcr:primaryType] FROM __ALLNODES__'.
283             builder.select("jcr:primaryType");
284         }
285     }
286 
287     /**
288      * Find any {@link AttributeNameTest attribute names} that have been unioned together (with '|'). Any other combination of
289      * objects results in an error.
290      * 
291      * @param binary the binary component
292      * @return the list of attribute names, if that's all that's in the supplied component; may be empty
293      */
294     protected List<AttributeNameTest> extractAttributeNames( BinaryComponent binary ) {
295         List<AttributeNameTest> results = new ArrayList<AttributeNameTest>();
296         boolean failed = false;
297         if (binary instanceof Union) {
298             for (int i = 0; i != 2; ++i) {
299                 Component comp = i == 0 ? binary.getLeft() : binary.getRight();
300                 comp = comp.collapse();
301                 if (comp instanceof Union) {
302                     results.addAll(extractAttributeNames((BinaryComponent)comp));
303                 } else if (comp instanceof AttributeNameTest) {
304                     results.add((AttributeNameTest)comp);
305                 } else if (comp instanceof NameTest) {
306                     // Element names, which are fine but we'll ignore
307                 } else {
308                     failed = true;
309                     break;
310                 }
311             }
312         } else {
313             failed = true;
314         }
315         if (failed) {
316             throw new InvalidQueryException(query,
317                                             "A parenthesized expression in a path step may only contain ORed and ANDed attribute names or element names; therefore '"
318                                             + binary + "' is not valid");
319         }
320         return results;
321     }
322 
323     /**
324      * Find any {@link NameTest element names} that have been unioned together (with '|'). Any other combination of objects
325      * results in an error.
326      * 
327      * @param binary the binary component
328      * @return the list of attribute names, if that's all that's in the supplied component; may be empty
329      */
330     protected List<NameTest> extractElementNames( BinaryComponent binary ) {
331         List<NameTest> results = new ArrayList<NameTest>();
332         boolean failed = false;
333         if (binary instanceof Union) {
334             for (int i = 0; i != 2; ++i) {
335                 Component comp = i == 0 ? binary.getLeft() : binary.getRight();
336                 comp = comp.collapse();
337                 if (comp instanceof Union) {
338                     results.addAll(extractElementNames((BinaryComponent)comp));
339                 } else if (comp instanceof AttributeNameTest) {
340                     // ignore these ...
341                 } else if (comp instanceof NameTest) {
342                     results.add((NameTest)comp);
343                 } else {
344                     failed = true;
345                     break;
346                 }
347             }
348         } else {
349             failed = true;
350         }
351         if (failed) {
352             throw new InvalidQueryException(query,
353                                             "A parenthesized expression in a path step may only contain ORed element names; therefore '"
354                                             + binary + "' is not valid");
355         }
356         return results;
357     }
358 
359     protected String translateSource( String tableName,
360                                       List<StepExpression> path,
361                                       ConstraintBuilder where ) {
362         if (path.size() == 0) {
363             // This is a query against the root node ...
364             String alias = newAlias();
365             builder.fromAllNodesAs(alias);
366             where.path(alias).isEqualTo("/");
367             return alias;
368         }
369         String alias = newAlias();
370         if (tableName != null) {
371             // This is after some element(...) steps, so we need to join ...
372             builder.joinAllNodesAs(alias);
373         } else {
374             // This is the only part of the query ...
375             builder.fromAllNodesAs(alias);
376         }
377         tableName = alias;
378         if (path.size() == 1 && path.get(0).collapse() instanceof NameTest) {
379             // Node immediately below root ...
380             NameTest nodeName = (NameTest)path.get(0).collapse();
381             where.path(alias).isEqualTo("/" + nameFrom(nodeName));
382         } else if (path.size() == 2 && path.get(0) instanceof DescendantOrSelf && path.get(1).collapse() instanceof NameTest) {
383             // Node anywhere ...
384             NameTest nodeName = (NameTest)path.get(1).collapse();
385             if (!nodeName.isWildcard()) {
386                 where.nodeName(alias).isEqualTo(nameFrom(nodeName));
387             }
388         } else {
389             // Must be just a bunch of descendant-or-self, axis and filter steps ...
390             translatePathExpressionConstraint(new PathExpression(true, path, null), where, alias);
391         }
392         return tableName;
393     }
394 
395     protected String translateElementTest( ElementTest elementTest,
396                                            List<StepExpression> pathConstraint,
397                                            ConstraintBuilder where ) {
398         String tableName = null;
399         NameTest typeName = elementTest.getTypeName();
400         if (typeName.isWildcard()) {
401             tableName = newAlias();
402             builder.fromAllNodesAs(tableName);
403         } else {
404             if (typeName.getLocalTest() == null) {
405                 throw new InvalidQueryException(
406                                                 query,
407                                                 "The '"
408                                                 + elementTest
409                                                 + "' clause uses a partial wildcard in the type name, but only a wildcard on the whole name is supported");
410             }
411             tableName = nameFrom(typeName);
412             builder.from(tableName);
413         }
414         if (elementTest.getElementName() != null) {
415             NameTest nodeName = elementTest.getElementName();
416             if (!nodeName.isWildcard()) {
417                 where.nodeName(tableName).isEqualTo(nameFrom(nodeName));
418             }
419         }
420         if (pathConstraint.isEmpty()) {
421             where.depth(tableName).isEqualTo(1);
422         } else {
423             List<StepExpression> path = new ArrayList<StepExpression>(pathConstraint);
424             if (!path.isEmpty() && path.get(path.size() - 1) instanceof AxisStep) {
425                 // Element test should always apply to descendants, never to self, so add a descedant
426                 path.add(new AxisStep(new NameTest(null, null), Collections.<Component>emptyList()));
427             }
428             translatePathExpressionConstraint(new PathExpression(true, path, null), where, tableName);
429         }
430         return tableName;
431     }
432 
433     protected void translatePredicates( List<Component> predicates,
434                                         String tableName,
435                                         ConstraintBuilder where ) {
436         assert tableName != null;
437         for (Component predicate : predicates) {
438             translatePredicate(predicate, tableName, where);
439         }
440     }
441 
442     protected String translatePredicate( Component predicate,
443                                          String tableName,
444                                          ConstraintBuilder where ) {
445         predicate = predicate.collapse();
446         assert tableName != null;
447         if (predicate instanceof ParenthesizedExpression) {
448             ParenthesizedExpression paren = (ParenthesizedExpression)predicate;
449             where = where.openParen();
450             translatePredicate(paren.getWrapped(), tableName, where);
451             where.closeParen();
452         } else if (predicate instanceof And) {
453             And and = (And)predicate;
454             where = where.openParen();
455             translatePredicate(and.getLeft(), tableName, where);
456             where.and();
457             translatePredicate(and.getRight(), tableName, where);
458             where.closeParen();
459         } else if (predicate instanceof Or) {
460             Or or = (Or)predicate;
461             where = where.openParen();
462             translatePredicate(or.getLeft(), tableName, where);
463             where.or();
464             translatePredicate(or.getRight(), tableName, where);
465             where.closeParen();
466         } else if (predicate instanceof Union) {
467             Union union = (Union)predicate;
468             where = where.openParen();
469             translatePredicate(union.getLeft(), tableName, where);
470             where.or();
471             translatePredicate(union.getRight(), tableName, where);
472             where.closeParen();
473         } else if (predicate instanceof Literal) {
474             Literal literal = (Literal)predicate;
475             if (literal.isInteger()) return tableName; // do nothing, since this is a path constraint and is handled elsewhere
476         } else if (predicate instanceof AttributeNameTest) {
477             // This adds the criteria that the attribute exists, and adds it to the select ...
478             AttributeNameTest attribute = (AttributeNameTest)predicate;
479             String propertyName = nameFrom(attribute.getNameTest());
480             // There is nothing in the JCR 1.0 spec that says that a property constrain implies it should be included in the
481             // result columns
482             // builder.select(tableName + "." + propertyName);
483             where.hasProperty(tableName, propertyName);
484         } else if (predicate instanceof NameTest) {
485             // This adds the criteria that the child node exists ...
486             NameTest childName = (NameTest)predicate;
487             String alias = newAlias();
488             builder.joinAllNodesAs(alias).onChildNode(tableName, alias);
489             if (!childName.isWildcard()) where.nodeName(alias).isEqualTo(nameFrom(childName));
490             tableName = alias;
491         } else if (predicate instanceof Comparison) {
492             Comparison comparison = (Comparison)predicate;
493             Component left = comparison.getLeft();
494             Component right = comparison.getRight();
495             Operator operator = comparison.getOperator();
496             if (left instanceof Literal) {
497                 Component temp = left;
498                 left = right;
499                 right = temp;
500                 operator = operator.reverse();
501             }
502             if (left instanceof NodeTest) {
503                 NodeTest nodeTest = (NodeTest)left;
504                 String propertyName = null;
505                 if (nodeTest instanceof AttributeNameTest) {
506                     AttributeNameTest attribute = (AttributeNameTest)left;
507                     propertyName = nameFrom(attribute.getNameTest());
508                 } else if (nodeTest instanceof NameTest) {
509                     NameTest nameTest = (NameTest)left;
510                     propertyName = nameFrom(nameTest);
511                 } else {
512                     throw new InvalidQueryException(query,
513                                                     "Left hand side of a comparison must be a name test or attribute name test; therefore '"
514                                                     + comparison + "' is not valid");
515                 }
516                 if (right instanceof Literal) {
517                     String value = ((Literal)right).getValue();
518                     where.propertyValue(tableName, propertyName).is(operator, value);
519                 } else if (right instanceof FunctionCall) {
520                     FunctionCall call = (FunctionCall)right;
521                     NameTest functionName = call.getName();
522                     List<Component> parameters = call.getParameters();
523                     // Is this a cast ...
524                     String castType = CAST_FUNCTION_NAME_TO_TYPE.get(functionName);
525                     if (castType != null) {
526                         if (parameters.size() == 1 && parameters.get(0).collapse() instanceof Literal) {
527                             // The first parameter can be the type name (or table name) ...
528                             Literal value = (Literal)parameters.get(0).collapse();
529                             where.propertyValue(tableName, propertyName).is(operator).cast(value.getValue()).as(castType);
530                         } else {
531                             throw new InvalidQueryException(query, "A cast function requires one literal parameter; therefore '"
532                                                                    + comparison + "' is not valid");
533                         }
534                     } else {
535                         throw new InvalidQueryException(query,
536                                                         "Only the 'jcr:score' function is allowed in a comparison predicate; therefore '"
537                                                         + comparison + "' is not valid");
538                     }
539                 }
540             } else if (left instanceof FunctionCall && right instanceof Literal) {
541                 FunctionCall call = (FunctionCall)left;
542                 NameTest functionName = call.getName();
543                 List<Component> parameters = call.getParameters();
544                 String value = ((Literal)right).getValue();
545                 if (functionName.matches("jcr", "score")) {
546                     String scoreTableName = tableName;
547                     if (parameters.isEmpty()) {
548                         scoreTableName = tableName;
549                     } else if (parameters.size() == 1 && parameters.get(0) instanceof NameTest) {
550                         // The first parameter can be the type name (or table name) ...
551                         NameTest name = (NameTest)parameters.get(0);
552                         if (!name.isWildcard()) scoreTableName = nameFrom(name);
553                     } else {
554                         throw new InvalidQueryException(query,
555                                                         "The 'jcr:score' function may have no parameters or the type name as the only parameter.");
556 
557                     }
558                     where.fullTextSearchScore(scoreTableName).is(operator, value);
559                 } else {
560                     throw new InvalidQueryException(query,
561                                                     "Only the 'jcr:score' function is allowed in a comparison predicate; therefore '"
562                                                     + comparison + "' is not valid");
563                 }
564             }
565         } else if (predicate instanceof FunctionCall) {
566             FunctionCall call = (FunctionCall)predicate;
567             NameTest functionName = call.getName();
568             List<Component> parameters = call.getParameters();
569             Component param1 = parameters.size() > 0 ? parameters.get(0) : null;
570             Component param2 = parameters.size() > 1 ? parameters.get(1) : null;
571             if (functionName.matches(null, "not")) {
572                 if (parameters.size() != 1) {
573                     throw new InvalidQueryException(query, "The 'not' function requires one parameter; therefore '" + predicate
574                                                            + "' is not valid");
575                 }
576                 where = where.not().openParen();
577                 translatePredicate(param1, tableName, where);
578                 where.closeParen();
579             } else if (functionName.matches("jcr", "like")) {
580                 if (parameters.size() != 2) {
581                     throw new InvalidQueryException(query, "The 'jcr:like' function requires two parameters; therefore '"
582                                                            + predicate + "' is not valid");
583                 }
584                 if (!(param1 instanceof AttributeNameTest)) {
585                     throw new InvalidQueryException(query,
586                                                     "The first parameter of 'jcr:like' must be an property reference with the '@' symbol; therefore '"
587                                                     + predicate + "' is not valid");
588                 }
589                 if (!(param2 instanceof Literal)) {
590                     throw new InvalidQueryException(query, "The second parameter of 'jcr:like' must be a literal; therefore '"
591                                                            + predicate + "' is not valid");
592                 }
593                 NameTest attributeName = ((AttributeNameTest)param1).getNameTest();
594                 String value = ((Literal)param2).getValue();
595                 where.propertyValue(tableName, nameFrom(attributeName)).isLike(value);
596             } else if (functionName.matches("jcr", "contains")) {
597                 if (parameters.size() != 2) {
598                     throw new InvalidQueryException(query, "The 'jcr:contains' function requires two parameters; therefore '"
599                                                            + predicate + "' is not valid");
600                 }
601                 if (!(param2 instanceof Literal)) {
602                     throw new InvalidQueryException(query,
603                                                     "The second parameter of 'jcr:contains' must be a literal; therefore '"
604                                                     + predicate + "' is not valid");
605                 }
606                 String value = ((Literal)param2).getValue();
607                 if (param1 instanceof ContextItem) {
608                     // refers to the current node (or table) ...
609                     where.search(tableName, value);
610                 } else if (param1 instanceof AttributeNameTest) {
611                     // refers to an attribute on the current node (or table) ...
612                     NameTest attributeName = ((AttributeNameTest)param1).getNameTest();
613                     where.search(tableName, nameFrom(attributeName), value);
614                 } else if (param1 instanceof NameTest) {
615                     // refers to child node, so we need to add a join ...
616                     String alias = newAlias();
617                     builder.joinAllNodesAs(alias).onChildNode(tableName, alias);
618                     // Now add the criteria ...
619                     where.search(alias, value);
620                     tableName = alias;
621                 } else if (param1 instanceof PathExpression) {
622                     // refers to a descendant node ...
623                     PathExpression pathExpr = (PathExpression)param1;
624                     if (pathExpr.getLastStep().collapse() instanceof AttributeNameTest) {
625                         AttributeNameTest attributeName = (AttributeNameTest)pathExpr.getLastStep().collapse();
626                         pathExpr = pathExpr.withoutLast();
627                         String searchTable = translatePredicate(pathExpr, tableName, where);
628                         if (attributeName.getNameTest().isWildcard()) {
629                             where.search(searchTable, value);
630                         } else {
631                             where.search(searchTable, nameFrom(attributeName.getNameTest()), value);
632                         }
633                     } else {
634                         String searchTable = translatePredicate(param1, tableName, where);
635                         where.search(searchTable, value);
636                     }
637                 } else {
638                     throw new InvalidQueryException(query,
639                                                     "The first parameter of 'jcr:contains' must be a relative path (e.g., '.', an attribute name, a child name, etc.); therefore '"
640                                                     + predicate + "' is not valid");
641                 }
642             } else if (functionName.matches("jcr", "deref")) {
643                 throw new InvalidQueryException(query,
644                                                 "The 'jcr:deref' function is not required by JCR and is not currently supported; therefore '"
645                                                 + predicate + "' is not valid");
646             } else {
647                 throw new InvalidQueryException(query,
648                                                 "Only the 'jcr:like' and 'jcr:contains' functions are allowed in a predicate; therefore '"
649                                                 + predicate + "' is not valid");
650             }
651         } else if (predicate instanceof PathExpression) {
652             // Requires that the descendant node with the relative path does exist ...
653             PathExpression pathExpr = (PathExpression)predicate;
654             List<StepExpression> steps = pathExpr.getSteps();
655             OrderBy orderBy = pathExpr.getOrderBy();
656             assert steps.size() > 1; // 1 or 0 would have been collapsed ...
657             Component firstStep = steps.get(0).collapse();
658             if (firstStep instanceof ContextItem) {
659                 // Remove the context and retry ...
660                 return translatePredicate(new PathExpression(true, steps.subList(1, steps.size()), orderBy), tableName, where);
661             }
662             if (firstStep instanceof NameTest) {
663                 // Special case where this is similar to '[a/@id]'
664                 NameTest childName = (NameTest)firstStep;
665                 String alias = newAlias();
666                 builder.joinAllNodesAs(alias).onChildNode(tableName, alias);
667                 if (!childName.isWildcard()) {
668                     where.nodeName(alias).isEqualTo(nameFrom(childName));
669                 }
670                 return translatePredicate(new PathExpression(true, steps.subList(1, steps.size()), orderBy), alias, where);
671             }
672             if (firstStep instanceof DescendantOrSelf) {
673                 // Special case where this is similar to '[a/@id]'
674                 String alias = newAlias();
675                 builder.joinAllNodesAs(alias).onDescendant(tableName, alias);
676                 return translatePredicate(new PathExpression(true, steps.subList(1, steps.size()), orderBy), alias, where);
677             }
678             // Add the join ...
679             String alias = newAlias();
680             builder.joinAllNodesAs(alias).onDescendant(tableName, alias);
681             // Now add the criteria ...
682             translatePathExpressionConstraint(pathExpr, where, alias);
683         } else {
684             throw new InvalidQueryException(query, "Unsupported criteria '" + predicate + "'");
685         }
686         return tableName;
687     }
688 
689     /**
690      * Determine if the predicates contain any expressions that cannot be put into a LIKE constraint on the path.
691      * 
692      * @param predicates the predicates
693      * @return true if the supplied predicates can be handled entirely in the LIKE constraint on the path, or false if they have
694      *         to be handled as other criteria
695      */
696     protected boolean appliesToPathConstraint( List<Component> predicates ) {
697         if (predicates.isEmpty()) return true;
698         if (predicates.size() > 1) return false;
699         assert predicates.size() == 1;
700         Component predicate = predicates.get(0);
701         if (predicate instanceof Literal && ((Literal)predicate).isInteger()) return true;
702         if (predicate instanceof NameTest && ((NameTest)predicate).isWildcard()) return true;
703         return false;
704     }
705 
706     protected boolean translatePathExpressionConstraint( PathExpression pathExrp,
707                                                          ConstraintBuilder where,
708                                                          String tableName ) {
709         RelativePathLikeExpressions expressions = relativePathLikeExpressions(pathExrp);
710         if (expressions.isEmpty()) return false;
711         where = where.openParen();
712         boolean first = true;
713         int number = 0;
714         for (String path : expressions) {
715             if (path == null || path.length() == 0 || path.equals("%/") || path.equals("%/%") || path.equals("%//%")) continue;
716             if (first) first = false;
717             else where.or();
718             if (path.indexOf('%') != -1) {
719                 where.path(tableName).isLike(path);
720                 switch (expressions.depthMode) {
721                     case AT_LEAST:
722                         where.and().depth(tableName).isGreaterThanOrEqualTo().cast(expressions.depth).asLong();
723                         break;
724                     case EXACT:
725                         where.and().depth(tableName).isEqualTo().cast(expressions.depth).asLong();
726                         break;
727                     case DEFAULT:
728                         // don't have to add the DEPTH criteria ...
729                         break;
730                 }
731             } else {
732                 where.path(tableName).isEqualTo(path);
733             }
734             ++number;
735         }
736         if (number > 0) where.closeParen();
737         return true;
738     }
739 
740     protected static enum DepthMode {
741         DEFAULT,
742         EXACT,
743         AT_LEAST;
744     }
745 
746     protected static class RelativePathLikeExpressions implements Iterable<String> {
747         protected final List<String> paths;
748         protected final int depth;
749         protected final DepthMode depthMode;
750 
751         protected RelativePathLikeExpressions() {
752             this.paths = null;
753             this.depth = 0;
754             this.depthMode = DepthMode.DEFAULT;
755         }
756 
757         protected RelativePathLikeExpressions( String[] paths,
758                                                int depth,
759                                                DepthMode depthMode ) {
760             this.paths = Arrays.asList(paths);
761             this.depth = depth;
762             this.depthMode = depthMode;
763         }
764 
765         protected boolean isEmpty() {
766             return paths == null || paths.isEmpty();
767         }
768 
769         public Iterator<String> iterator() {
770             return paths.iterator();
771         }
772     }
773 
774     protected RelativePathLikeExpressions relativePathLikeExpressions( PathExpression pathExpression ) {
775         List<StepExpression> steps = pathExpression.getSteps();
776         if (steps.isEmpty()) return new RelativePathLikeExpressions();
777         if (steps.size() == 1 && steps.get(0) instanceof DescendantOrSelf) return new RelativePathLikeExpressions();
778         PathLikeBuilder builder = new SinglePathLikeBuilder();
779         int depth = 0;
780         DepthMode depthMode = DepthMode.EXACT;
781         for (Iterator<StepExpression> iterator = steps.iterator(); iterator.hasNext();) {
782             StepExpression step = iterator.next();
783             if (step instanceof DescendantOrSelf) {
784                 ++depth;
785                 depthMode = DepthMode.DEFAULT;
786                 if (builder.isEmpty()) {
787                     builder.append("%/");
788                 } else {
789                     if (iterator.hasNext()) {
790                         builder.append('/');
791                         builder = new DualPathLikeBuilder(builder.clone(), builder.append("%"));
792                     } else {
793                         builder.append('/').append('%');
794                     }
795                 }
796             } else if (step instanceof AxisStep) {
797                 ++depth;
798                 AxisStep axis = (AxisStep)step;
799                 NodeTest nodeTest = axis.getNodeTest();
800                 assert !(nodeTest instanceof ElementTest);
801                 if (nodeTest instanceof NameTest) {
802                     NameTest nameTest = (NameTest)nodeTest;
803                     builder.append('/');
804                     if (nameTest.getPrefixTest() != null) {
805                         builder.append(nameTest.getPrefixTest()).append(':');
806                     }
807                     if (nameTest.getLocalTest() != null) {
808                         builder.append(nameTest.getLocalTest());
809                     } else {
810                         builder.append('%');
811                     }
812                     List<Component> predicates = axis.getPredicates();
813                     if (!predicates.isEmpty()) {
814                         assert predicates.size() == 1;
815                         Component predicate = predicates.get(0);
816                         if (predicate instanceof Literal && ((Literal)predicate).isInteger()) {
817                             builder.append('[').append(((Literal)predicate).getValue()).append(']');
818                         }
819                     }
820                 }
821             } else if (step instanceof FilterStep) {
822                 FilterStep filter = (FilterStep)step;
823                 Component primary = filter.getPrimaryExpression();
824                 if (primary instanceof ContextItem) {
825                     continue; // ignore this '.'
826                 } else if (primary instanceof ParenthesizedExpression) {
827                     ParenthesizedExpression paren = (ParenthesizedExpression)primary;
828                     Component wrapped = paren.getWrapped().collapse();
829                     if (wrapped instanceof AttributeNameTest) {
830                         // ignore this; handled earlier ...
831                     } else if (wrapped instanceof BinaryComponent) {
832                         List<NameTest> names = extractElementNames((BinaryComponent)wrapped);
833                         if (names.size() >= 1) {
834                             PathLikeBuilder orig = builder.clone();
835                             builder.append('/').append(nameFrom(names.get(0)));
836                             if (names.size() > 1) {
837                                 for (NameTest name : names.subList(1, names.size())) {
838                                     builder = new DualPathLikeBuilder(orig.clone().append('/').append(nameFrom(name)), builder);
839                                 }
840                             }
841                         }
842                     } else {
843                         throw new InvalidQueryException(query,
844                                                         "A parenthesized expression of this type is not supported in the primary path expression; therefore '"
845                                                         + primary + "' is not valid");
846                     }
847                 }
848             }
849         }
850         return new RelativePathLikeExpressions(builder.getPaths(), depth, depthMode);
851     }
852 
853     protected static interface PathLikeBuilder {
854         PathLikeBuilder append( String string );
855 
856         PathLikeBuilder append( char c );
857 
858         boolean isEmpty();
859 
860         PathLikeBuilder clone();
861 
862         String[] getPaths();
863     }
864 
865     protected static class SinglePathLikeBuilder implements PathLikeBuilder {
866         private final StringBuilder builder = new StringBuilder();
867         private char lastChar;
868 
869         public SinglePathLikeBuilder append( String string ) {
870             builder.append(string);
871             if (string.length() > 0) lastChar = string.charAt(string.length() - 1);
872             return this;
873         }
874 
875         public SinglePathLikeBuilder append( char c ) {
876             if (lastChar != c) {
877                 builder.append(c);
878                 lastChar = c;
879             }
880             return this;
881         }
882 
883         public boolean isEmpty() {
884             return builder.length() == 0;
885         }
886 
887         @Override
888         public SinglePathLikeBuilder clone() {
889             return new SinglePathLikeBuilder().append(builder.toString());
890         }
891 
892         @Override
893         public String toString() {
894             return builder.toString();
895         }
896 
897         public String[] getPaths() {
898             return isEmpty() ? new String[] {} : new String[] {builder.toString()};
899         }
900     }
901 
902     protected static class DualPathLikeBuilder implements PathLikeBuilder {
903         private final PathLikeBuilder builder1;
904         private final PathLikeBuilder builder2;
905 
906         protected DualPathLikeBuilder( PathLikeBuilder builder1,
907                                        PathLikeBuilder builder2 ) {
908             this.builder1 = builder1;
909             this.builder2 = builder2;
910         }
911 
912         public DualPathLikeBuilder append( String string ) {
913             builder1.append(string);
914             builder2.append(string);
915             return this;
916         }
917 
918         public DualPathLikeBuilder append( char c ) {
919             builder1.append(c);
920             builder2.append(c);
921             return this;
922         }
923 
924         public boolean isEmpty() {
925             return false;
926         }
927 
928         @Override
929         public DualPathLikeBuilder clone() {
930             return new DualPathLikeBuilder(builder1.clone(), builder2.clone());
931         }
932 
933         public String[] getPaths() {
934             String[] paths1 = builder1.getPaths();
935             String[] paths2 = builder2.getPaths();
936             String[] result = new String[paths1.length + paths2.length];
937             System.arraycopy(paths1, 0, result, 0, paths1.length);
938             System.arraycopy(paths2, 0, result, paths1.length, paths2.length);
939             return result;
940         }
941     }
942 
943     protected String nameFrom( NameTest name ) {
944         String prefix = name.getPrefixTest();
945         String local = name.getLocalTest();
946         assert local != null;
947         return (prefix != null ? prefix + ":" : "") + local;
948     }
949 
950     protected String newAlias() {
951         String root = "nodeSet";
952         int num = 1;
953         String alias = root + num;
954         while (aliases.contains(alias)) {
955             num += 1;
956             alias = root + num;
957         }
958         aliases.add(alias);
959         return alias;
960     }
961 }