1   package eu.fbk.knowledgestore.data;
2   
3   import java.io.ObjectStreamException;
4   import java.io.Serializable;
5   import java.lang.reflect.Array;
6   import java.util.Date;
7   import java.util.GregorianCalendar;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Set;
11  import java.util.regex.Matcher;
12  import java.util.regex.Pattern;
13  
14  import javax.annotation.Nullable;
15  import javax.xml.datatype.XMLGregorianCalendar;
16  
17  import com.google.common.base.Function;
18  import com.google.common.base.Objects;
19  import com.google.common.base.Preconditions;
20  import com.google.common.base.Predicate;
21  import com.google.common.cache.Cache;
22  import com.google.common.cache.CacheBuilder;
23  import com.google.common.collect.ImmutableBiMap;
24  import com.google.common.collect.ImmutableList;
25  import com.google.common.collect.ImmutableSet;
26  import com.google.common.collect.Iterables;
27  import com.google.common.collect.Lists;
28  import com.google.common.collect.Maps;
29  import com.google.common.collect.Ordering;
30  import com.google.common.collect.Range;
31  import com.google.common.collect.Sets;
32  
33  import org.jaxen.Context;
34  import org.jaxen.ContextSupport;
35  import org.jaxen.JaxenException;
36  import org.jaxen.JaxenHandler;
37  import org.jaxen.NamespaceContext;
38  import org.jaxen.SimpleVariableContext;
39  import org.jaxen.VariableContext;
40  import org.jaxen.expr.AllNodeStep;
41  import org.jaxen.expr.BinaryExpr;
42  import org.jaxen.expr.DefaultNameStep;
43  import org.jaxen.expr.DefaultXPathFactory;
44  import org.jaxen.expr.Expr;
45  import org.jaxen.expr.FilterExpr;
46  import org.jaxen.expr.FunctionCallExpr;
47  import org.jaxen.expr.LiteralExpr;
48  import org.jaxen.expr.LocationPath;
49  import org.jaxen.expr.NameStep;
50  import org.jaxen.expr.NumberExpr;
51  import org.jaxen.expr.PathExpr;
52  import org.jaxen.expr.Step;
53  import org.jaxen.expr.UnaryExpr;
54  import org.jaxen.expr.XPathFactory;
55  import org.jaxen.saxpath.XPathReader;
56  import org.jaxen.saxpath.helpers.XPathReaderFactory;
57  import org.openrdf.model.Literal;
58  import org.openrdf.model.URI;
59  import org.openrdf.model.impl.URIImpl;
60  import org.openrdf.model.vocabulary.XMLSchema;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  /**
65   * An XPath-like expression that computes/extracts a list of objects starting from an object of
66   * the data model.
67   * <p>
68   * This class allows to express and evaluate generic XPath-like expressions on nodes of the data
69   * model. An {@code XPath} expression can be created in three ways:
70   * </p>
71   * <ul>
72   * <li>with methods {@link #parse(String, Object...)} and {@link #parse(Map, String, Object...)},
73   * which parse an {@code XPath} string, possibly using a map of namespace declarations replacing
74   * supplied values to optional <tt>$$</tt> placeholders in the string;</li>
75   * <li>with method {@link #constant(Object...)} that produces the {@code XPath} expression
76   * returning a supplied constant value or sequence of values;</li>
77   * <li>with method {@link #compose(String, Object...)} that composes multiple {@code XPath} or
78   * scalar sequence operands using a supplied operator.</li>
79   * </ul>
80   * Parsed expression strings are validated and then stored in the created {@code XPath} object,
81   * accessible via {@link #toString()}, {@link #getHead()} and {@link #getBody()}. The syntax of
82   * the expression string is reported in the package documentation. ### TODO ### </p>
83   * <p>
84   * Evaluation of an {@code XPath} expression is performed via a number of {@code eval} methods
85   * that all accept an object as input but differ in their output and additional arguments:
86   * <ul>
87   * <li>{@link #eval(Object)} and {@link #eval(Object, Class)} return a list result consisting
88   * either of objects of the data model or of objects obtained from their conversion to a specific
89   * type {@code T};</li>
90   * <li>{@link #evalUnique(Object)} and {@link #evalUnique(Object, Class)} return a unique result
91   * consisting either in a generic object or an object converted to a specific type; their outcome
92   * is null if the evaluation produced no result, while an {@link IllegalArgumentException} is
93   * thrown if multiple results are produced;</li>
94   * <li>{@link #evalBoolean(Object)} return a boolean result.</li>
95   * </ul>
96   * <p>
97   * Note that the evaluation may fail for a specific input object for a number of reasons, e.g.,
98   * because it performs some arithmetic operation on data that is of an incompatible type. Failure
99   * is reported by throwing an {@code IllegalArgumentException} if the {@code XPath} expression is
100  * not lenient (see {@link #isLenient()}), or by returning a default value otherwise (respectively
101  * an empty list, null or false for the three classes of {@code eval()} methods). An {@code XPath}
102  * expression is created not-lenient by default, but a lenient version of it can be obtained by
103  * calling method {@link #lenient(boolean)}.
104  * </p>
105  * <p>
106  * Apart from evaluation, it is possible to query an {@code XPath} object for the set of
107  * properties accessed by the expression, using method {@link #getProperties()}, and for the
108  * prefix-to-namespace mappings referenced by the expression string. Methods
109  * {@link #asPredicate()}, {@link #asFunction(Class)} and {@link #asFunctionUnique(Class)}
110  * generate respectively a {@link Predicate}, single-valued and multi-valued {@link Function} view
111  * of an {@code XPath} expression, thus supporting interoperability with the utility classes and
112  * methods of the Guava library. Another method {@link #decompose(Map)} attempts to decompose a
113  * boolean {@code XPath} expression into a conjunction of property restrictions of the form
114  * {@code property in rangeset} plus an optional remaining {@code XPath} expression, where
115  * {@code rangesets} are scalar sets of {@link Range}s s (e.g., (-1, 5], (6, 9)). This kind of
116  * decomposition allows to extract simple restrictions on individual properties that can be
117  * efficiently evaluated using indexes.
118  * </p>
119  * <p>
120  * {@code XPath} objects are immutable and thread safe. Methods {@link #equals(Object)} and
121  * {@link #hashCode()} are based exclusively on the expression string (accessible via
122  * {@link #toString()}) and on the lenient mode of the {@code XPath} object.
123  * </p>
124  */
125 @SuppressWarnings("deprecation")
126 public abstract class XPath implements Serializable {
127 
128     private static final Logger LOGGER = LoggerFactory.getLogger(XPath.class);
129 
130     private static final Cache<String, XPath> CACHE = CacheBuilder.newBuilder().maximumSize(1024)
131             .build();
132 
133     private static final Pattern PATTERN_PLACEHOLDER = Pattern
134             .compile("(?:\\A|[^\\\\])([$][$])(?:\\z|.)");
135 
136     private static final Pattern PATTERN_WITH = Pattern.compile("\\s*with\\s+");
137 
138     private static final Pattern PATTERN_MAPPING = Pattern
139             .compile("\\s*(\\w+)\\:\\s*\\<([^\\>]+)\\>\\s*([\\:\\,])\\s*");
140 
141     private static final VariableContext VARIABLES = new SimpleVariableContext();
142 
143     private static final XPathFactory FACTORY = new DefaultXPathFactory();
144 
145     private final transient Support support;
146 
147     /**
148      * Creates an {@code XPath} expression returning a sequence with the constant value(s)
149      * specified. Values must be scalars, arrays or iterables; the latter two are recursively
150      * exploded and their elements added to the sequence produced by the returned {@code XPath}
151      * expression.
152      *
153      * @param values
154      *            the values
155      * @return the produced {@code XPath} expression
156      */
157     public static XPath constant(final Object... values) {
158         return parse(encode(values, true));
159     }
160 
161     /**
162      * Creates an {@code XPath} expression by composing a number operands with the operator
163      * specified. Operands can be either {@code XPath} expressions, scalars, arrays or iterables
164      * of scalars. Supported operators are {@code not}, {@code and}, {@code or}, {@code =},
165      * {@code !=}, {@code <}, {@code >}, {@code <=}, {@code >=}, {@code +}, {@code -}, {@code *},
166      * {@code mod}, {@code div}, {@code |}.
167      *
168      * @param operator
169      *            the operator
170      * @param operands
171      *            the operands
172      * @return the produced {@code XPath} expression, or null if no operand was supplied
173      * @throws IllegalArgumentException
174      *             in case multiple {@code XPath} expressions using incompatible namespaces are
175      *             composed.
176      */
177     @Nullable
178     public static XPath compose(final String operator, final Object... operands)
179             throws IllegalArgumentException {
180 
181         final String op = operator.toLowerCase();
182 
183         try {
184             if (operands.length == 0) {
185                 return null;
186             }
187 
188             if (operands.length == 1) {
189                 final XPath xpath = operands[0] instanceof XPath ? (XPath) operands[0]
190                         : constant(operands[0]);
191                 if ("not".equals(op)) {
192                     final Expr expr = FACTORY.createFunctionCallExpr(null, "not");
193                     ((FunctionCallExpr) expr).addParameter(xpath.support.expr);
194                     return new StrictXPath(new Support(expr,
195                             expr.getText().replace("child::", ""), xpath.support.properties,
196                             xpath.support.namespaces));
197                 } else if ("=".equals(op) || "!=".equals(op) || "<".equals(op) || ">".equals(op)
198                         || "<=".equals(op) || ">=".equals(op)) {
199                     throw new IllegalArgumentException(
200                             "At least two arguments required for operator " + op);
201                 }
202                 return xpath;
203             }
204 
205             final List<Expr> expressions = Lists.newArrayListWithCapacity(operands.length);
206             final Set<URI> properties = Sets.newHashSet();
207             final Map<String, String> namespaces = Maps.newHashMap();
208 
209             for (final Object operand : operands) {
210                 final XPath xpath = operand instanceof XPath ? (XPath) operand : constant(operand);
211                 expressions.add(xpath.support.expr);
212                 properties.addAll(xpath.support.properties);
213                 for (final Map.Entry<String, String> entry : xpath.support.namespaces.entrySet()) {
214                     final String oldNamespace = namespaces.put(entry.getKey(), entry.getValue());
215                     Preconditions.checkArgument(
216                             oldNamespace == null || oldNamespace.equals(entry.getValue()),
217                             "Namespace conflict for prefix '" + entry.getKey() + "': <"
218                                     + entry.getValue() + "> vs <" + oldNamespace + ">");
219                 }
220             }
221 
222             Expr lhs = expressions.get(0);
223             for (int i = 1; i < expressions.size(); ++i) {
224                 final Expr rhs = expressions.get(i);
225                 if ("and".equals(op)) {
226                     lhs = FACTORY.createAndExpr(lhs, rhs);
227                 } else if ("or".equals(op)) {
228                     lhs = FACTORY.createOrExpr(lhs, rhs);
229                 } else if ("=".equals(op)) {
230                     lhs = FACTORY.createEqualityExpr(lhs, rhs, 1); // 1 stands for =
231                 } else if ("!=".equals(op)) {
232                     lhs = FACTORY.createEqualityExpr(lhs, rhs, 2); // 2 stands for !=
233                 } else if ("<".equals(op)) {
234                     lhs = FACTORY.createRelationalExpr(lhs, rhs, 3); // 3 stands for <
235                 } else if (">".equals(op)) {
236                     lhs = FACTORY.createRelationalExpr(lhs, rhs, 4); // 4 stands for >
237                 } else if ("<=".equals(op)) {
238                     lhs = FACTORY.createRelationalExpr(lhs, rhs, 5); // 5 stands for <=
239                 } else if (">=".equals(op)) {
240                     lhs = FACTORY.createRelationalExpr(lhs, rhs, 6); // 6 stands for >=
241                 } else if ("+".equals(op)) {
242                     lhs = FACTORY.createAdditiveExpr(lhs, rhs, 7); // 7 stands for +
243                 } else if ("-".equals(op)) {
244                     lhs = FACTORY.createAdditiveExpr(lhs, rhs, 8); // 8 stands for -
245                 } else if ("*".equals(op)) {
246                     lhs = FACTORY.createMultiplicativeExpr(lhs, rhs, 9); // 9 stands for *
247                 } else if ("mod".equals(op)) {
248                     lhs = FACTORY.createMultiplicativeExpr(lhs, rhs, 10); // 10 = mod
249                 } else if ("div".equals(op)) {
250                     lhs = FACTORY.createMultiplicativeExpr(lhs, rhs, 11); // 11 = div
251                 } else if ("|".equals(op)) {
252                     lhs = FACTORY.createUnionExpr(lhs, rhs);
253                 } else {
254                     throw new IllegalArgumentException("Unsupported operator " + op);
255                 }
256             }
257 
258             return new StrictXPath(new Support(lhs, lhs.getText().replace("child::", ""),
259                     properties, namespaces));
260 
261         } catch (final JaxenException ex) {
262             throw new IllegalArgumentException("Could not compose operands " + operands
263                     + " of operator " + op + ": " + ex.getMessage(), ex);
264         }
265     }
266 
267     /**
268      * Creates a new {@code XPath} expression based on the expression string specified with
269      * optional <tt>$$</tt> placeholders replaced by supplied values. Note that i-th placeholder
270      * is replaced with i-th value of the {@code values} vararg array, while namespaces referenced
271      * in the string must be defined in the {@code WITH} clause of the string itself.
272      *
273      * @param string
274      *            the expression string
275      * @param values
276      *            the values for optional placeholders appearing in {@code string}
277      * @return the created {@code XPath} expression object, on success
278      * @throws ParseException
279      *             if the expression string supplied is not syntactically correct, or if it
280      *             references a namespace not defined in the string itself or in common
281      *             (prefix.cc) namespaces
282      */
283     public static XPath parse(final String string, final Object... values) throws ParseException {
284         return parse(Data.getNamespaceMap(), string, values);
285     }
286 
287     /**
288      * Creates a new {@code XPath} expression based on the namespace mappings and the expression
289      * string specified, with optional <tt>$$</tt> placeholders replaced by supplied values. Note
290      * that i-th placeholder is replaced with i-th value of the {@code values} vararg array, while
291      * namespaces occurring in the string can be defined either in the {@code WITH} clause of the
292      * string or in the supplied namespace {@code Map}.
293      *
294      * @param namespaces
295      *            the namespace map
296      * @param expression
297      *            the expression string
298      * @param values
299      *            the values for optional placeholders appearing in {@code string}
300      * @return the created {@code XPath} expression object, on success
301      * @throws ParseException
302      *             if the expression string supplied is not syntactically correct, or if it
303      *             references a namespace not defined in the string itself or in the supplied
304      *             namespace map
305      */
306     public static XPath parse(final Map<String, String> namespaces, final String expression,
307             final Object... values) throws ParseException {
308 
309         Preconditions.checkNotNull(namespaces);
310         Preconditions.checkNotNull(expression);
311 
312         final Map<String, String> baseNamespaces = Data.newNamespaceMap(namespaces,
313                 Data.getNamespaceMap());
314 
315         final String expandedString = expand(expression, values);
316 
317         XPath xpath = CACHE.getIfPresent(expandedString);
318         if (xpath != null) {
319             for (final Map.Entry<String, String> entry : xpath.getNamespaces().entrySet()) {
320                 if (!entry.getValue().equals(baseNamespaces.get(entry.getKey()))) {
321                     xpath = null;
322                     break;
323                 }
324             }
325             if (xpath != null) {
326                 return xpath;
327             }
328         }
329 
330         final Map<String, String> usedNamespaces = Maps.newHashMap();
331         final Map<String, String> declaredNamespaces = Maps.newHashMap();
332         final Map<String, String> combinedNamespaces = Data.newNamespaceMap(declaredNamespaces,
333                 baseNamespaces);
334 
335         final String xpathString;
336 
337         Expr expr;
338         final Set<URI> properties = Sets.newHashSet();
339         try {
340             xpathString = extractNamespaces(expandedString, declaredNamespaces);
341             String rewrittenXpathString = rewriteLiterals(xpathString, combinedNamespaces,
342                     usedNamespaces);
343             rewrittenXpathString = rewriteEscapedURIs(rewrittenXpathString, combinedNamespaces,
344                     usedNamespaces);
345             LOGGER.debug("XPath '{}' rewritten to '{}'", expression, rewrittenXpathString);
346             final JaxenHandler handler = new JaxenHandler();
347             final XPathReader reader = XPathReaderFactory.createReader();
348             reader.setXPathHandler(handler);
349             reader.parse(rewrittenXpathString);
350             expr = handler.getXPathExpr().getRootExpr();
351             assert expr != null;
352             analyzeExpr(expr, combinedNamespaces, usedNamespaces, properties, true);
353         } catch (final Exception ex) {
354             throw new ParseException(expression, "Invalid XPath expression - " + ex.getMessage(),
355                     ex);
356         }
357 
358         xpath = new StrictXPath(new Support(expr, xpathString, properties, usedNamespaces));
359         CACHE.put(expression, xpath);
360         return xpath;
361     }
362 
363     private static String expand(final String expression, final Object... arguments)
364             throws ParseException {
365         int expansions = 0;
366         String result = expression;
367         final Matcher matcher = PATTERN_PLACEHOLDER.matcher(expression);
368         try {
369             if (matcher.find()) {
370                 final StringBuilder builder = new StringBuilder();
371                 int last = 0;
372                 do {
373                     builder.append(expression.substring(last, matcher.start(1)));
374                     builder.append(encode(arguments[expansions++], true));
375                     last = matcher.end(1);
376                 } while (matcher.find());
377                 builder.append(expression.substring(last, expression.length()));
378                 result = builder.toString();
379             }
380         } catch (final IndexOutOfBoundsException ex) {
381             throw new ParseException(expression, "No argument supplied for placeholder #"
382                     + expansions);
383         }
384         if (expansions != arguments.length) {
385             throw new ParseException(expression, "XPath expression contains " + expansions
386                     + " placholders, but " + arguments.length + " arguments where supplied");
387         }
388         return result;
389     }
390 
391     private static String extractNamespaces(final String expression,
392             final Map<String, String> namespaces) throws IllegalArgumentException {
393         String xpathString = expression;
394         final Matcher matcher = PATTERN_WITH.matcher(expression);
395         if (matcher.lookingAt()) {
396             matcher.usePattern(PATTERN_MAPPING);
397             while (true) {
398                 matcher.region(matcher.end(), expression.length());
399                 if (!matcher.lookingAt()) {
400                     throw new IllegalArgumentException("Invalid WITH clause");
401                 }
402                 final String prefix = matcher.group(1);
403                 final String uri = matcher.group(2);
404                 namespaces.put(prefix, uri);
405                 if (matcher.group(3).equals(":")) {
406                     break;
407                 }
408             }
409             xpathString = expression.substring(matcher.end());
410         }
411         return xpathString;
412     }
413 
414     private static String rewriteEscapedURIs(final String string,
415             final Map<String, String> inNamespaces, final Map<String, String> outNamespaces) {
416 
417         final StringBuilder builder = new StringBuilder();
418         final int length = string.length();
419         int i = 0;
420 
421         try {
422             while (i < length) {
423                 char c = string.charAt(i);
424                 if (c == '\\') {
425                     c = string.charAt(++i);
426                     if (c == '\'' || c == '\"' || c == '<') {
427                         final char d = c == '<' ? '>' : c; // delimiter;
428                         final int start = i + 1;
429                         do {
430                             c = string.charAt(++i);
431                         } while (c != d || string.charAt(i - 1) == '\\');
432                         builder.append("uri(\"" + string.substring(start, i) + "\")");
433                         ++i;
434                     } else {
435                         final int start = i;
436                         while (i < length && (string.charAt(i) == ':' //
437                                 || string.charAt(i) == '_' //
438                                 || string.charAt(i) == '-' //
439                                 || string.charAt(i) == '.' //
440                         || Character.isLetterOrDigit(string.charAt(i)))) {
441                             ++i;
442                         }
443                         final String qname = string.substring(start, i);
444                         final String prefix = qname.substring(0, qname.indexOf(':'));
445                         final URI uri = (URI) Data.parseValue(qname, inNamespaces);
446                         outNamespaces.put(prefix, uri.getNamespace());
447                         builder.append("uri(\"" + uri + "\")");
448                     }
449                 } else if (c == '\'' || c == '\"') {
450                     final char d = c; // delimiter
451                     builder.append(d);
452                     do {
453                         c = string.charAt(++i);
454                         builder.append(c);
455                     } while (c != d || string.charAt(i - 1) == '\\');
456                     ++i;
457 
458                 } else {
459                     builder.append(c);
460                     ++i;
461                 }
462             }
463         } catch (final Exception ex) {
464             throw new IllegalArgumentException("Illegal URI escaping near offset " + i, ex);
465         }
466 
467         return builder.toString();
468     }
469 
470     private static String rewriteLiterals(final String string,
471             final Map<String, String> inNamespaces, final Map<String, String> outNamespaces) {
472 
473         final StringBuilder builder = new StringBuilder();
474         final int length = string.length();
475         int i = 0;
476 
477         try {
478             while (i < length) {
479                 char c = string.charAt(i);
480                 if (c == '\'' || c == '\"') {
481                     final char d = c; // delimiter
482                     if (i > 0 && string.charAt(i - 1) == '\\' //
483                             || i >= 4 && string.startsWith("uri(", i - 4) //
484                             || i >= 4 && string.startsWith("str(", i - 4) //
485                             || i >= 6 && string.startsWith("strdt(", i - 6) //
486                             || i >= 8 && string.startsWith("strlang(", i - 8)) {
487                         do {
488                             builder.append(c);
489                             c = string.charAt(++i);
490                         } while (c != d || string.charAt(i - 1) == '\\');
491                         builder.append(c);
492                         ++i;
493                     } else {
494                         int start = i + 1;
495                         do {
496                             c = string.charAt(++i);
497                         } while (c != d || string.charAt(i - 1) == '\\');
498                         final String label = string.substring(start, i);
499                         String lang = null;
500                         String dt = null;
501                         ++i;
502                         if (i < length) {
503                             if (string.charAt(i) == '@') {
504                                 start = ++i;
505                                 do {
506                                     c = string.charAt(i++);
507                                 } while (i < length && Character.isLetter(c));
508                                 lang = string.substring(start, i);
509                             } else if (string.charAt(i) == '^' && i + 1 < length
510                                     && string.charAt(i + 1) == '^') {
511                                 i += 2;
512                                 if (string.charAt(i) == '<') {
513                                     start = i + 1;
514                                     do {
515                                         c = string.charAt(++i);
516                                     } while (c != '>');
517                                     dt = string.substring(start, i);
518                                 } else {
519                                     start = i;
520                                     while (i < length && (string.charAt(i) == ':' //
521                                             || string.charAt(i) == '_' //
522                                             || string.charAt(i) == '-' //
523                                             || string.charAt(i) == '.' //
524                                     || Character.isLetterOrDigit(string.charAt(i)))) {
525                                         ++i;
526                                     }
527                                     final String qname = string.substring(start, i);
528                                     final String prefix = qname.substring(0, qname.indexOf(':'));
529                                     final URI uri = (URI) Data.parseValue(qname, inNamespaces);
530                                     outNamespaces.put(prefix, uri.getNamespace());
531                                     dt = uri.stringValue();
532                                 }
533                             }
534                         }
535                         if (lang != null) {
536                             builder.append("strlang(\"").append(label).append("\", \"")
537                                     .append(lang).append("\")");
538                         } else if (dt != null) {
539                             builder.append("strdt(\"").append(label).append("\", uri(\"")
540                                     .append(dt).append("\"))");
541                         } else {
542                             builder.append("str(\"").append(label).append("\")");
543                         }
544                     }
545                 } else if (c == 't'
546                         && string.startsWith("true", i)
547                         && (i == 0 || !Character.isLetterOrDigit(string.charAt(i - 1)))
548                         && (i + 4 == length || !Character.isLetterOrDigit(string.charAt(i + 4))
549                                 && string.charAt(i + 4) != '(')) {
550                     builder.append("strdt(\"true\", uri(\"")
551                             .append(XMLSchema.BOOLEAN.stringValue()).append("\"))");
552                     i += 4;
553 
554                 } else if (c == 'f'
555                         && string.startsWith("false", i)
556                         && (i == 0 || !Character.isLetterOrDigit(string.charAt(i - 1)))
557                         && (i + 5 == length || !Character.isLetterOrDigit(string.charAt(i + 5))
558                                 && string.charAt(i + 5) != '(')) {
559                     builder.append("strdt(\"false\", uri(\"")
560                             .append(XMLSchema.BOOLEAN.stringValue()).append("\"))");
561                     i += 5;
562 
563                 } else {
564                     builder.append(c);
565                     ++i;
566                 }
567             }
568         } catch (final Exception ex) {
569             throw new IllegalArgumentException("Illegal URI escaping near offset " + i, ex);
570         }
571 
572         return builder.toString();
573     }
574 
575     private static void analyzeExpr(final Expr expr, final Map<String, String> inNamespaces,
576             final Map<String, String> outNamespaces, final Set<URI> outProperties,
577             final boolean root) {
578 
579         if (expr instanceof UnaryExpr) {
580             analyzeExpr(((UnaryExpr) expr).getExpr(), inNamespaces, outNamespaces, outProperties,
581                     root);
582 
583         } else if (expr instanceof BinaryExpr) {
584             final BinaryExpr binary = (BinaryExpr) expr;
585             analyzeExpr(binary.getLHS(), inNamespaces, outNamespaces, outProperties, root);
586             analyzeExpr(binary.getRHS(), inNamespaces, outNamespaces, outProperties, root);
587 
588         } else if (expr instanceof FilterExpr) {
589             final FilterExpr filter = (FilterExpr) expr;
590             analyzeExpr(filter.getExpr(), inNamespaces, outNamespaces, outProperties, root);
591             for (final Object predicate : filter.getPredicates()) {
592                 analyzeExpr(((org.jaxen.expr.Predicate) predicate).getExpr(), inNamespaces,
593                         outNamespaces, outProperties, evalToRoot(filter.getExpr(), root));
594             }
595 
596         } else if (expr instanceof FunctionCallExpr) {
597             for (final Object parameter : ((FunctionCallExpr) expr).getParameters()) {
598                 analyzeExpr((Expr) parameter, inNamespaces, outNamespaces, outProperties, root);
599             }
600 
601         } else if (expr instanceof PathExpr) {
602             final PathExpr path = (PathExpr) expr;
603             if (path.getFilterExpr() != null) {
604                 analyzeExpr(path.getFilterExpr(), inNamespaces, outNamespaces, outProperties, true);
605             }
606             if (path.getLocationPath() != null) {
607                 final Expr filter = path.getFilterExpr();
608                 analyzeExpr(path.getLocationPath(), inNamespaces, outNamespaces, outProperties,
609                         evalToRoot(filter, root));
610             }
611 
612         } else if (expr instanceof LocationPath) {
613             final LocationPath l = (LocationPath) expr;
614             @SuppressWarnings("unchecked")
615             final List<Step> steps = l.getSteps();
616             for (int i = 0; i < l.getSteps().size(); ++i) {
617                 if (steps.get(i) instanceof DefaultNameStep) {
618                     final DefaultNameStep step = (DefaultNameStep) steps.get(i);
619                     final String prefix = step.getPrefix();
620                     final String namespace = inNamespaces.get(prefix);
621                     if (namespace == null) {
622                         throw new IllegalArgumentException("Unknown prefix '" + step.getPrefix()
623                                 + ":'");
624                     }
625                     outNamespaces.put(prefix, namespace);
626                     if ((i == 0 || steps.get(i - 1) instanceof AllNodeStep)
627                             && (l.isAbsolute() || root)) {
628                         final URI uri = Data.getValueFactory().createURI(namespace,
629                                 step.getLocalName());
630                         outProperties.add(uri);
631                     }
632                 }
633                 for (final Object predicate : steps.get(i).getPredicates()) {
634                     analyzeExpr(((org.jaxen.expr.Predicate) predicate).getExpr(), inNamespaces,
635                             outNamespaces, outProperties, false);
636                 }
637             }
638         }
639     }
640 
641     private static boolean evalToRoot(final Expr expr, final boolean root) {
642 
643         if (expr instanceof LocationPath) {
644             final LocationPath l = (LocationPath) expr;
645             return (root || l.isAbsolute()) && l.getSteps().size() == 1
646                     && l.getSteps().get(0) instanceof AllNodeStep;
647 
648         } else if (expr instanceof FilterExpr) {
649             return evalToRoot(((FilterExpr) expr).getExpr(), root);
650 
651         } else if (expr instanceof PathExpr) {
652             final PathExpr p = (PathExpr) expr;
653             final boolean atRoot = evalToRoot(p.getFilterExpr(), root);
654             return evalToRoot(p.getLocationPath(), atRoot);
655 
656         } else {
657             return false;
658         }
659     }
660 
661     private static String encode(@Nullable final Object object, final boolean canEmitSequence) {
662 
663         if (object == null) {
664             return "sequence()";
665 
666         } else if (object.equals(Boolean.TRUE)) {
667             return "true()";
668 
669         } else if (object.equals(Boolean.FALSE)) {
670             return "false()";
671 
672         } else if (object instanceof Number) {
673             return object.toString();
674 
675         } else if (object instanceof Date || object instanceof GregorianCalendar
676                 || object instanceof XMLGregorianCalendar) {
677             return "dateTime(\'" + Data.convert(object, XMLGregorianCalendar.class) + "\')";
678 
679         } else if (object instanceof URI) {
680             return "\\'" + object.toString().replace("\'", "\\\'") + "\'";
681 
682         } else if (object.getClass().isArray()) {
683             final int size = Array.getLength(object);
684             if (size == 0) {
685                 return "sequence()";
686             } else if (size == 1) {
687                 return encode(Array.get(object, 0), canEmitSequence);
688             } else {
689                 final StringBuilder builder = new StringBuilder(canEmitSequence ? "sequence(" : "");
690                 for (int i = 0; i < size; ++i) {
691                     builder.append(i == 0 ? "" : ", ").append(encode(Array.get(object, i), false));
692                 }
693                 builder.append(canEmitSequence ? ")" : "");
694                 return builder.toString();
695             }
696 
697         } else if (object instanceof Iterable<?>) {
698             final Iterable<?> iterable = (Iterable<?>) object;
699             final int size = Iterables.size(iterable);
700             if (size == 0) {
701                 return "sequence()";
702             } else if (size == 1) {
703                 return encode(Iterables.get(iterable, 0), canEmitSequence);
704             } else {
705                 final StringBuilder builder = new StringBuilder(canEmitSequence ? "sequence(" : "");
706                 String separator = "";
707                 for (final Object element : iterable) {
708                     builder.append(separator).append(encode(element, false));
709                     separator = ", ";
710                 }
711                 builder.append(canEmitSequence ? ")" : "");
712                 return builder.toString();
713             }
714 
715         } else {
716             return '\'' + object.toString().replace("\'", "\\\'") + '\'';
717         }
718     }
719 
720     private XPath(final Support support) {
721 
722         this.support = support;
723     }
724 
725     /**
726      * Returns the head of the {@code XPath} expression, i.e., the content of the {@code WITH}
727      * clause. An empty string is returned in case the with clause is not used.
728      *
729      * @return the head
730      */
731     public final String getHead() {
732         return this.support.head;
733     }
734 
735     /**
736      * Returns the body of the {@code XPath} expression, i.e., the {@code XPath} string without
737      * the {@code WITH} clause.
738      *
739      * @return the body
740      */
741     public final String getBody() {
742         return this.support.body;
743     }
744 
745     /**
746      * Returns the namespace declarations referenced by this {@code XPath} expression.
747      *
748      * @return an immutable bidirectional map of {@code prefix - namespace URI} mappings
749      */
750     public final Map<String, String> getNamespaces() {
751         return this.support.namespaces;
752     }
753 
754     /**
755      * Returns the properties referenced by this {@code XPath} expression. Note that this method
756      * returns only the properties accessed on the <i>root</i> record the condition is evaluated
757      * on, ignoring properties of nested records that can be reached via property traversal
758      * starting from the root record.
759      *
760      * @return an immutable set with the referenced properties, possibly empty
761      */
762     public final Set<URI> getProperties() {
763         return this.support.properties;
764     }
765 
766     /**
767      * Returns true if this {@code XPath} expression is lenient, i.e., evaluation never throws
768      * exceptions. {@code XPath} expressions are non-lenient by default; a lenient version of an
769      * expression can be obtained by calling method {@link #lenient(boolean)}.
770      *
771      * @return true if this expression is lenient
772      */
773     public abstract boolean isLenient();
774 
775     /**
776      * Returns a lenient / not lenient version of this {@code XPath} expression.
777      *
778      * @param lenient
779      *            the requested lenient mode
780      * @return an {@code XPath} expression with the same xpath string of this expression but the
781      *         requested lenient mode; note that this {@code XPath} instance is returned in case
782      *         the requested lenient mode matches the mode of this expression
783      */
784     public final XPath lenient(final boolean lenient) {
785         if (lenient == isLenient()) {
786             return this;
787         } else if (!lenient) {
788             return new StrictXPath(this.support);
789         } else {
790             return new LenientXPath(this.support);
791         }
792     }
793 
794     /**
795      * Returns a predicate view of this {@code XPath} expression that accept an object input. If
796      * this {@code XPath} is lenient, evaluation of the predicate will return false on failure,
797      * rather than throwing an {@link IllegalArgumentException}.
798      *
799      * @return the requested predicate view
800      */
801     public final Predicate<Object> asPredicate() {
802 
803         return new Predicate<Object>() {
804 
805             @Override
806             public boolean apply(@Nullable final Object object) {
807                 Preconditions.checkNotNull(object);
808                 return evalBoolean(object);
809             }
810 
811         };
812     }
813 
814     /**
815      * Returns a function view of this {@code XPath} expression that produces a {@code List<T>}
816      * result given an input object. If this {@code XPath} is lenient, evaluation of the function
817      * will return an empty list on failure, rather than throwing an
818      * {@link IllegalArgumentException}.
819      *
820      * @param resultClass
821      *            the {@code Class} object for the list elements of the expected function result
822      * @param <T>
823      *            the type of result list element
824      * @return the requested function view
825      */
826     public final <T> Function<Object, List<T>> asFunction(final Class<T> resultClass) {
827 
828         Preconditions.checkNotNull(resultClass);
829 
830         return new Function<Object, List<T>>() {
831 
832             @Override
833             public List<T> apply(@Nullable final Object object) {
834                 Preconditions.checkNotNull(object);
835                 return eval(object, resultClass);
836             }
837 
838         };
839     }
840 
841     /**
842      * Returns a function view of this {@code XPath} expression that produces a unique {@code T}
843      * result given an input object. If this {@code XPath} is lenient, evaluation of the function
844      * will return null on failure, rather than throwing an {@link IllegalArgumentException}.
845      *
846      * @param resultClass
847      *            the {@code Class} object for the expected function result
848      * @param <T>
849      *            the type of result
850      * @return the requested function view
851      */
852     public final <T> Function<Object, T> asFunctionUnique(final Class<T> resultClass) {
853 
854         Preconditions.checkNotNull(resultClass);
855 
856         return new Function<Object, T>() {
857 
858             @Override
859             public T apply(@Nullable final Object object) {
860                 Preconditions.checkNotNull(object);
861                 return evalUnique(object, resultClass);
862             }
863 
864         };
865     }
866 
867     /**
868      * Evaluates this {@code XPath} expression on the object supplied, producing as result a list
869      * of objects.
870      *
871      * @param object
872      *            the object to evaluate this expression on
873      * @return the list of objects produced as result, on success; an empty list on failure if on
874      *         lenient mode
875      * @throws IllegalArgumentException
876      *             if this {@code XPath} expression is not lenient and evaluation fails for the
877      *             object supplied
878      */
879     public final List<Object> eval(final Object object) throws IllegalArgumentException {
880         return eval(object, Object.class);
881     }
882 
883     /**
884      * Evaluates this {@code XPath} expression on the object supplied, producing as result a list
885      * of objects of the type {@code T} specified.
886      *
887      * @param object
888      *            the object to evaluate this expression on
889      * @param resultClass
890      *            the {@code Class} object for the elements of the result list
891      * @param <T>
892      *            the type of element of the result list
893      * @return the list of objects of the requested type produced by the evaluation, on success;
894      *         an empty list on failure if on lenient mode
895      * @throws IllegalArgumentException
896      *             if this {@code XPath} expression is not lenient and evaluation fails for the
897      *             object supplied
898      */
899     public final <T> List<T> eval(final Object object, final Class<T> resultClass)
900             throws IllegalArgumentException {
901 
902         Preconditions.checkNotNull(object);
903         Preconditions.checkNotNull(resultClass);
904 
905         try {
906             return toList(doEval(object), resultClass);
907 
908         } catch (final Exception ex) {
909             if (isLenient()) {
910                 return ImmutableList.of();
911             }
912             throw new IllegalArgumentException("Evaluation of XPath failed: " + ex.getMessage()
913                     + "\nXPath is: " + this.support.string + "\nInput is: " + object
914                     + "\nExpected result is: List<" + resultClass.getSimpleName() + ">", ex);
915         }
916     }
917 
918     /**
919      * Evaluates this {@code XPath} expression on the object supplied, producing as result a
920      * unique object.
921      *
922      * @param object
923      *            the object to evaluate this expression on
924      * @return on success, the unique object resulting from the evaluation, or null if evaluation
925      *         produced no results; on failure, null is returned if this {@code XPath} expression
926      *         is lenient
927      * @throws IllegalArgumentException
928      *             if this {@code XPath} expression is not lenient and evaluation fails for the
929      *             object supplied
930      */
931     @Nullable
932     public final Object evalUnique(final Object object) throws IllegalArgumentException {
933         return evalUnique(object, Object.class);
934     }
935 
936     /**
937      * Evaluates this {@code XPath} expression on the object supplied, producing as result a
938      * unique object of the type {@code T} specified.
939      *
940      * @param object
941      *            the object to evaluate this expression on
942      * @param resultClass
943      *            the {@code Class} object for the result object
944      * @param <T>
945      *            the type of result
946      * @return on success, the unique object of the requested type resulting from the evaluation,
947      *         or null if evaluation produced no results; on failure, null is returned if this
948      *         {@code XPath} expression is lenient
949      * @throws IllegalArgumentException
950      *             if this {@code XPath} expression is not lenient and evaluation fails for the
951      *             object supplied
952      */
953     @Nullable
954     public final <T> T evalUnique(final Object object, final Class<T> resultClass)
955             throws IllegalArgumentException {
956 
957         Preconditions.checkNotNull(object);
958         Preconditions.checkNotNull(resultClass);
959 
960         try {
961             return toUnique(doEval(object), resultClass);
962 
963         } catch (final Exception ex) {
964             if (isLenient()) {
965                 return null;
966             }
967             throw new IllegalArgumentException("Evaluation of XPath failed: " + ex.getMessage()
968                     + "\nXPath is: " + this.support.string + "\nInput is: " + object
969                     + "\nExpected result is: " + resultClass.getSimpleName(), ex);
970         }
971     }
972 
973     /**
974      * Evaluates this {@code XPath} expression on the object supplied, producing a boolean value
975      * as result.
976      *
977      * @param object
978      *            the object to evaluate this expression on
979      * @return the boolean value resulting from the evaluation, on success; on failure, false is
980      *         returned if this {@code XPath} expression is lenient
981      * @throws IllegalArgumentException
982      *             if this {@code XPath} expression is not lenient and evaluation fails for the
983      *             object supplied
984      */
985     public final boolean evalBoolean(final Object object) throws IllegalArgumentException {
986         final Boolean result = evalUnique(object, Boolean.class);
987         return result == null ? false : result;
988     }
989 
990     private Object doEval(final Object object) {
991         try {
992             final Context context = new Context(this.support);
993             context.setNodeSet(ImmutableList.of(XPathNavigator.INSTANCE.wrap(object)));
994             return this.support.expr.evaluate(context);
995 
996         } catch (final JaxenException ex) {
997             throw new IllegalArgumentException(ex.getMessage(), ex);
998         }
999     }
1000 
1001     private static <T> List<T> toList(final Object object, final Class<T> resultClass) {
1002 
1003         if (object == null) {
1004             return ImmutableList.of();
1005 
1006         } else if (object instanceof List<?>) {
1007             final List<?> list = (List<?>) object;
1008             final int size = list.size();
1009             if (size == 0) {
1010                 return ImmutableList.of();
1011             } else if (size == 1) {
1012                 return ImmutableList.of(toUnique(list.get(0), resultClass));
1013             } else {
1014                 final List<T> result = Lists.newArrayListWithCapacity(list.size());
1015                 for (final Object element : list) {
1016                     result.add(toUnique(element, resultClass));
1017                 }
1018                 return result;
1019             }
1020 
1021         } else {
1022             return ImmutableList.of(toUnique(object, resultClass));
1023         }
1024     }
1025 
1026     @Nullable
1027     private static <T> T toUnique(final Object object, final Class<T> resultClass) {
1028 
1029         if (object == null) {
1030             return null;
1031 
1032         } else if (object instanceof List<?>) {
1033             final List<?> list = (List<?>) object;
1034             final int size = list.size();
1035             if (size == 0) {
1036                 return null;
1037             } else if (size == 1) {
1038                 return toUnique(list.get(0), resultClass);
1039             } else {
1040                 throw new IllegalArgumentException("Expected unique "
1041                         + resultClass.getSimpleName() + " object, found: " + list);
1042             }
1043         } else {
1044             return Data.convert(XPathNavigator.INSTANCE.unwrap(object), resultClass);
1045         }
1046     }
1047 
1048     /**
1049      * Attempts at decomposing the {@code XPath} expression into the conjunction of a number of
1050      * property restrictions and (optionally) a remaining {@code XPath} expression. More in
1051      * details, the method tries to transform the condition in the following form:
1052      * {@code p1 in restriction1 AND ... AND pN in restrictionN AND remainingXPath}, where
1053      * {@code p1 ... pN} are property URIs, {@code restriction1 ... restrictionN} are sets of
1054      * scalar values or scalar {@link Range}s (e.g., {@code (1, 5], [7, 9]})), whose union should
1055      * be taken, and {@code remainingXPath} contains all the clauses of the original {@code XPath}
1056      * that cannot be decomposed in property restrictions.
1057      *
1058      * @param restrictions
1059      *            a modifiable map where to store the {@code property in restriction} restrictions
1060      * @return the remaining {@code XPath} expression, if any, or null if it was possible to
1061      *         completely express this expression as a conjunction of property restrictions
1062      */
1063     @SuppressWarnings({ "rawtypes", "unchecked" })
1064     @Nullable
1065     public final XPath decompose(final Map<URI, Set<Object>> restrictions) {
1066 
1067         Preconditions.checkNotNull(restrictions);
1068 
1069         try {
1070             Expr remaining = null;
1071 
1072             for (final Expr node : toCNF(this.support.expr)) {
1073                 URI property = null;
1074                 Object valueOrRange = null;
1075 
1076                 if (node instanceof LocationPath) {
1077                     property = extractProperty(node);
1078                     valueOrRange = Boolean.TRUE;
1079 
1080                 } else if (node instanceof FunctionCallExpr) {
1081                     final FunctionCallExpr call = (FunctionCallExpr) node;
1082                     if ("not".equals(call.getFunctionName())) {
1083                         property = extractProperty((Expr) call.getParameters().get(0));
1084                         valueOrRange = Boolean.FALSE;
1085                     }
1086 
1087                 } else if (node instanceof BinaryExpr) {
1088                     final BinaryExpr binary = (BinaryExpr) node;
1089 
1090                     property = extractProperty(binary.getLHS());
1091                     Object value = extractValue(binary.getRHS());
1092                     boolean swap = false;
1093 
1094                     if (property == null || value == null) {
1095                         property = extractProperty(binary.getRHS());
1096                         value = extractValue(binary.getLHS());
1097                         swap = true;
1098                     }
1099 
1100                     if (value instanceof Literal) {
1101                         final Literal lit = (Literal) value;
1102                         final URI dt = lit.getDatatype();
1103                         if (dt == null || dt.equals(XMLSchema.STRING)) {
1104                             value = lit.stringValue();
1105                         } else if (dt.equals(XMLSchema.BOOLEAN)) {
1106                             value = lit.booleanValue();
1107                         } else if (dt.equals(XMLSchema.DATE) || dt.equals(XMLSchema.DATETIME)) {
1108                             value = lit.calendarValue().toGregorianCalendar().getTime();
1109                         } else if (dt.equals(XMLSchema.INT) || dt.equals(XMLSchema.LONG)
1110                                 || dt.equals(XMLSchema.DOUBLE) || dt.equals(XMLSchema.FLOAT)
1111                                 || dt.equals(XMLSchema.SHORT) || dt.equals(XMLSchema.BYTE)
1112                                 || dt.equals(XMLSchema.DECIMAL) || dt.equals(XMLSchema.INTEGER)
1113                                 || dt.equals(XMLSchema.NON_NEGATIVE_INTEGER)
1114                                 || dt.equals(XMLSchema.NON_POSITIVE_INTEGER)
1115                                 || dt.equals(XMLSchema.NEGATIVE_INTEGER)
1116                                 || dt.equals(XMLSchema.POSITIVE_INTEGER)) {
1117                             value = lit.doubleValue();
1118                         } else if (dt.equals(XMLSchema.NORMALIZEDSTRING)
1119                                 || dt.equals(XMLSchema.TOKEN) || dt.equals(XMLSchema.NMTOKEN)
1120                                 || dt.equals(XMLSchema.LANGUAGE) || dt.equals(XMLSchema.NAME)
1121                                 || dt.equals(XMLSchema.NCNAME)) {
1122                             value = lit.getLabel();
1123                         }
1124                     }
1125 
1126                     if (property != null && value != null) {
1127                         if ("=".equals(binary.getOperator())) {
1128                             valueOrRange = value;
1129                         } else if (value instanceof Comparable<?>) {
1130                             final Comparable<?> comp = (Comparable<?>) value;
1131                             if (">".equals(binary.getOperator())) {
1132                                 valueOrRange = swap ? Range.lessThan(comp) : Range
1133                                         .greaterThan(comp);
1134                             } else if (">=".equals(binary.getOperator())) {
1135                                 valueOrRange = swap ? Range.atMost(comp) : Range.atLeast(comp);
1136                             } else if ("<".equals(binary.getOperator())) {
1137                                 valueOrRange = swap ? Range.greaterThan(comp) : Range
1138                                         .lessThan(comp);
1139                             } else if ("<=".equals(binary.getOperator())) {
1140                                 valueOrRange = swap ? Range.atLeast(comp) : Range.atMost(comp);
1141                             }
1142                         }
1143                     }
1144                 }
1145 
1146                 boolean processed = false;
1147                 if (property != null && valueOrRange != null) {
1148                     Set<Object> set = restrictions.get(property);
1149                     if (set == null) {
1150                         set = ImmutableSet.of(valueOrRange);
1151                         restrictions.put(property, set);
1152                         processed = true;
1153                     } else {
1154                         final Object oldValue = set.iterator().next();
1155                         if (oldValue instanceof Range) {
1156                             final Range oldRange = (Range) oldValue;
1157                             if (valueOrRange instanceof Range) {
1158                                 final Range newRange = (Range) valueOrRange;
1159                                 if (oldRange.isConnected(newRange)) {
1160                                     restrictions.put(property,
1161                                             ImmutableSet.of(oldRange.intersection(newRange)));
1162                                     processed = true;
1163                                 }
1164                             } else if (valueOrRange instanceof Comparable) {
1165                                 if (oldRange.contains((Comparable) valueOrRange)) {
1166                                     restrictions.put(property, ImmutableSet.of(valueOrRange));
1167                                     processed = true;
1168                                 }
1169                             }
1170                         }
1171                     }
1172                 }
1173 
1174                 if (!processed) {
1175                     remaining = remaining == null ? node : FACTORY.createAndExpr(remaining, node);
1176                 }
1177             }
1178 
1179             return remaining == null ? null : parse(this.support.namespaces, remaining.getText());
1180 
1181         } catch (final JaxenException ex) {
1182             throw new RuntimeException(ex.getMessage(), ex);
1183         }
1184     }
1185 
1186     @SuppressWarnings("rawtypes")
1187     @Nullable
1188     private Object extractValue(final Expr node) {
1189         if (node instanceof LiteralExpr) {
1190             return ((LiteralExpr) node).getLiteral();
1191         } else if (node instanceof NumberExpr) {
1192             final Number number = ((NumberExpr) node).getNumber();
1193             return number instanceof Double || number instanceof Float ? number.doubleValue()
1194                     : number.longValue(); // always return a Double
1195         } else if (node instanceof FunctionCallExpr) {
1196             final FunctionCallExpr function = (FunctionCallExpr) node;
1197             final String name = function.getFunctionName();
1198             String arg0 = null;
1199             String arg1 = null;
1200             final List params = function.getParameters();
1201             final int numParams = params.size();
1202             if (numParams > 0 && params.get(0) instanceof LiteralExpr) {
1203                 arg0 = ((LiteralExpr) params.get(0)).getLiteral();
1204             }
1205             if (numParams > 1 && params.get(1) instanceof LiteralExpr) {
1206                 arg1 = ((LiteralExpr) params.get(1)).getLiteral();
1207             }
1208             if (name.equals("uri") && numParams == 1 && arg0 != null) {
1209                 return new URIImpl(arg0);
1210             } else if (name.equals("dateTime") && numParams == 1 && arg0 != null) {
1211                 return Data.convert(arg0, Date.class);
1212             } else if (name.equals("true") && numParams == 0) {
1213                 return true;
1214             } else if (name.equals("false") && numParams == 0) {
1215                 return false;
1216             } else if (name.equals("str") && numParams == 1 && arg0 != null) {
1217                 return Data.getValueFactory().createLiteral(arg0);
1218             } else if (name.equals("strdt") && numParams == 2 && arg0 != null) {
1219                 final Object dt = extractValue((Expr) params.get(1));
1220                 if (dt instanceof URI) {
1221                     return Data.getValueFactory().createLiteral(arg0, (URI) dt);
1222                 }
1223             } else if (name.equals("strlang") && numParams == 2 && arg0 != null && arg1 != null) {
1224                 return Data.getValueFactory().createLiteral(arg0, arg1);
1225 
1226             }
1227         }
1228         return null;
1229     }
1230 
1231     @Nullable
1232     private URI extractProperty(final Expr node) {
1233         if (!(node instanceof LocationPath)) {
1234             return null;
1235         }
1236         final LocationPath path = (LocationPath) node;
1237         Step step = null;
1238         if (path.getSteps().size() == 1) {
1239             step = (Step) path.getSteps().get(0);
1240         } else if (path.getSteps().size() == 2) {
1241             if (path.getSteps().get(0) instanceof AllNodeStep) {
1242                 step = (Step) path.getSteps().get(1);
1243             }
1244         }
1245         if (!(step instanceof NameStep) || !step.getPredicates().isEmpty()) {
1246             return null;
1247         }
1248         return new URIImpl(this.support.translateNamespacePrefixToUri(((NameStep) step)
1249                 .getPrefix()) + ((NameStep) step).getLocalName());
1250     }
1251 
1252     private static List<Expr> toCNF(final Expr node) throws JaxenException {
1253 
1254         if (node instanceof BinaryExpr) {
1255 
1256             final BinaryExpr binary = (BinaryExpr) node;
1257             if ("and".equals(binary.getOperator())) {
1258                 // toCNF(A and B) = toCNF(A) and toCNF(B)
1259                 final List<Expr> result = Lists.newArrayList();
1260                 result.addAll(toCNF(binary.getLHS()));
1261                 result.addAll(toCNF(binary.getRHS()));
1262                 return result;
1263 
1264             } else if ("or".equals(binary.getOperator())) {
1265                 // toCNF(A or B) = and of {x or y | x in toCNF(A), y in toCNF(B)}
1266                 final List<Expr> result = Lists.newArrayList();
1267                 final List<Expr> leftArgs = toCNF(binary.getLHS());
1268                 final List<Expr> rightArgs = toCNF(binary.getRHS());
1269                 for (final Expr leftArg : leftArgs) {
1270                     for (final Expr rightArg : rightArgs) {
1271                         result.add(FACTORY.createOrExpr(leftArg, rightArg));
1272                     }
1273                 }
1274                 return result;
1275             }
1276 
1277         } else if (node instanceof FunctionCallExpr
1278                 && "not".equals(((FunctionCallExpr) node).getFunctionName())
1279                 && ((FunctionCallExpr) node).getParameters().get(0) instanceof BinaryExpr) {
1280 
1281             final FunctionCallExpr call = (FunctionCallExpr) node;
1282             final BinaryExpr binary = (BinaryExpr) call.getParameters().get(0);
1283             if ("or".equals(binary.getOperator())) {
1284                 // toCNF(not(A or B)) = toCNF(not(A)) and toCNF(not(B))
1285                 final FunctionCallExpr leftNot = FACTORY.createFunctionCallExpr(null, "not");
1286                 leftNot.addParameter(binary.getLHS());
1287                 final FunctionCallExpr rightNot = FACTORY.createFunctionCallExpr(null, "not");
1288                 rightNot.addParameter(binary.getRHS());
1289                 return ImmutableList.<Expr>builder().addAll(toCNF(leftNot))
1290                         .addAll(toCNF(rightNot)).build();
1291 
1292             } else if ("and".equals(binary.getOperator())) {
1293                 // toCNF(not(A and B)) = toCNF(not(A) or not(B))
1294                 final FunctionCallExpr leftNot = FACTORY.createFunctionCallExpr(null, "not");
1295                 leftNot.addParameter(binary.getLHS());
1296                 final FunctionCallExpr rightNot = FACTORY.createFunctionCallExpr(null, "not");
1297                 rightNot.addParameter(binary.getRHS());
1298                 return toCNF(FACTORY.createOrExpr(leftNot, rightNot));
1299             }
1300         }
1301 
1302         return ImmutableList.of(node);
1303     }
1304 
1305     /**
1306      * {@inheritDoc} Two {@code XPath} expressions are equal if they have the same string and
1307      * lenient mode.
1308      */
1309     @Override
1310     public final boolean equals(final Object object) {
1311         if (object == this) {
1312             return true;
1313         }
1314         if (object == null || object.getClass() != getClass()) {
1315             return false;
1316         }
1317         final XPath other = (XPath) object;
1318         return this.support.string.equals(other.support.string);
1319     }
1320 
1321     /**
1322      * {@inheritDoc} The returned hash code is based exclusively on the string and lenient mode of
1323      * this {@code XPath} expression.
1324      */
1325     @Override
1326     public final int hashCode() {
1327         return Objects.hashCode(this.support.string, this.getClass().getSimpleName());
1328     }
1329 
1330     /**
1331      * {@inheritDoc} The method returns the {@code XPath} string. Note this is not the string
1332      * originally supplied, but a string obtained from it during the validation step with a
1333      * {@code WITH} clause that contains all the namespace declarations necessary to make this
1334      * expression independent from external mappings.
1335      */
1336     @Override
1337     public final String toString() {
1338         return this.support.string;
1339     }
1340 
1341     /**
1342      * Returns the {@code XPath} string corresponding to the sequence of values specified. This
1343      * utility method can be used when programmatically composing an {@code XPath} string.
1344      *
1345      * @param values
1346      *            the values to format
1347      * @return the corresponding {@code XPath} string
1348      */
1349     public static String toString(final Object... values) {
1350         return encode(values, true);
1351     }
1352 
1353     public static Object unwrap(final Object object) {
1354         return XPathNavigator.INSTANCE.unwrap(object);
1355     }
1356 
1357     private Object writeReplace() throws ObjectStreamException {
1358         return new SerializedForm(toString(), isLenient());
1359     }
1360 
1361     private static final class SerializedForm {
1362 
1363         private final String string;
1364 
1365         private final boolean lenient;
1366 
1367         SerializedForm(final String string, final boolean lenient) {
1368             this.string = string;
1369             this.lenient = lenient;
1370         }
1371 
1372         private Object readResolve() throws ObjectStreamException {
1373             return parse(this.string).lenient(this.lenient);
1374         }
1375 
1376     }
1377 
1378     private static final class LenientXPath extends XPath {
1379 
1380         LenientXPath(final Support support) {
1381             super(support);
1382         }
1383 
1384         @Override
1385         public boolean isLenient() {
1386             return true;
1387         }
1388 
1389     }
1390 
1391     private static final class StrictXPath extends XPath {
1392 
1393         StrictXPath(final Support support) {
1394             super(support);
1395         }
1396 
1397         @Override
1398         public boolean isLenient() {
1399             return false;
1400         }
1401 
1402     }
1403 
1404     private static final class Support extends ContextSupport implements NamespaceContext {
1405 
1406         private static final long serialVersionUID = -2960336999829855818L;
1407 
1408         final String string;
1409 
1410         final String head;
1411 
1412         final String body;
1413 
1414         final Expr expr;
1415 
1416         final Set<URI> properties;
1417 
1418         final Map<String, String> namespaces;
1419 
1420         Support(final Expr expr, final String body, final Set<URI> properties,
1421                 final Map<String, String> namespaces) {
1422 
1423             super(null, XPathFunction.CONTEXT, VARIABLES, XPathNavigator.INSTANCE);
1424 
1425             final StringBuilder builder = new StringBuilder();
1426             for (final String prefix : Ordering.natural().sortedCopy(namespaces.keySet())) {
1427                 final String namespace = namespaces.get(prefix);
1428                 if (!namespace.equals(Data.getNamespaceMap().get(prefix))) {
1429                     builder.append(builder.length() == 0 ? "" : ", ").append(prefix).append(": ")
1430                             .append("<").append(namespace).append(">");
1431                 }
1432             }
1433             final String head = builder.toString();
1434 
1435             this.string = head.isEmpty() ? body : "with " + head + " : " + body;
1436             this.head = head.isEmpty() ? "" : this.string.substring(5, 5 + head.length());
1437             this.body = head.isEmpty() ? body : this.string.substring(8 + head.length());
1438             this.expr = expr;
1439             this.properties = properties;
1440             this.namespaces = ImmutableBiMap.copyOf(namespaces);
1441         }
1442 
1443         @Override
1444         public String translateNamespacePrefixToUri(final String prefix) {
1445             return this.namespaces.get(prefix);
1446         }
1447 
1448     }
1449 
1450 }