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  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
77  
78  
79  
80  
81  
82  
83  
84  
85  
86  
87  
88  
89  
90  
91  
92  
93  
94  
95  
96  
97  
98  
99  
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 
118 
119 
120 
121 
122 
123 
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 
149 
150 
151 
152 
153 
154 
155 
156 
157     public static XPath constant(final Object... values) {
158         return parse(encode(values, true));
159     }
160 
161     
162 
163 
164 
165 
166 
167 
168 
169 
170 
171 
172 
173 
174 
175 
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); 
231                 } else if ("!=".equals(op)) {
232                     lhs = FACTORY.createEqualityExpr(lhs, rhs, 2); 
233                 } else if ("<".equals(op)) {
234                     lhs = FACTORY.createRelationalExpr(lhs, rhs, 3); 
235                 } else if (">".equals(op)) {
236                     lhs = FACTORY.createRelationalExpr(lhs, rhs, 4); 
237                 } else if ("<=".equals(op)) {
238                     lhs = FACTORY.createRelationalExpr(lhs, rhs, 5); 
239                 } else if (">=".equals(op)) {
240                     lhs = FACTORY.createRelationalExpr(lhs, rhs, 6); 
241                 } else if ("+".equals(op)) {
242                     lhs = FACTORY.createAdditiveExpr(lhs, rhs, 7); 
243                 } else if ("-".equals(op)) {
244                     lhs = FACTORY.createAdditiveExpr(lhs, rhs, 8); 
245                 } else if ("*".equals(op)) {
246                     lhs = FACTORY.createMultiplicativeExpr(lhs, rhs, 9); 
247                 } else if ("mod".equals(op)) {
248                     lhs = FACTORY.createMultiplicativeExpr(lhs, rhs, 10); 
249                 } else if ("div".equals(op)) {
250                     lhs = FACTORY.createMultiplicativeExpr(lhs, rhs, 11); 
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 
269 
270 
271 
272 
273 
274 
275 
276 
277 
278 
279 
280 
281 
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 
289 
290 
291 
292 
293 
294 
295 
296 
297 
298 
299 
300 
301 
302 
303 
304 
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; 
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; 
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; 
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 
727 
728 
729 
730 
731     public final String getHead() {
732         return this.support.head;
733     }
734 
735     
736 
737 
738 
739 
740 
741     public final String getBody() {
742         return this.support.body;
743     }
744 
745     
746 
747 
748 
749 
750     public final Map<String, String> getNamespaces() {
751         return this.support.namespaces;
752     }
753 
754     
755 
756 
757 
758 
759 
760 
761 
762     public final Set<URI> getProperties() {
763         return this.support.properties;
764     }
765 
766     
767 
768 
769 
770 
771 
772 
773     public abstract boolean isLenient();
774 
775     
776 
777 
778 
779 
780 
781 
782 
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 
796 
797 
798 
799 
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 
816 
817 
818 
819 
820 
821 
822 
823 
824 
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 
843 
844 
845 
846 
847 
848 
849 
850 
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 
869 
870 
871 
872 
873 
874 
875 
876 
877 
878 
879     public final List<Object> eval(final Object object) throws IllegalArgumentException {
880         return eval(object, Object.class);
881     }
882 
883     
884 
885 
886 
887 
888 
889 
890 
891 
892 
893 
894 
895 
896 
897 
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 
920 
921 
922 
923 
924 
925 
926 
927 
928 
929 
930 
931     @Nullable
932     public final Object evalUnique(final Object object) throws IllegalArgumentException {
933         return evalUnique(object, Object.class);
934     }
935 
936     
937 
938 
939 
940 
941 
942 
943 
944 
945 
946 
947 
948 
949 
950 
951 
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 
975 
976 
977 
978 
979 
980 
981 
982 
983 
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 
1050 
1051 
1052 
1053 
1054 
1055 
1056 
1057 
1058 
1059 
1060 
1061 
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(); 
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                 
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                 
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                 
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                 
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 
1307 
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 
1323 
1324 
1325     @Override
1326     public final int hashCode() {
1327         return Objects.hashCode(this.support.string, this.getClass().getSimpleName());
1328     }
1329 
1330     
1331 
1332 
1333 
1334 
1335 
1336     @Override
1337     public final String toString() {
1338         return this.support.string;
1339     }
1340 
1341     
1342 
1343 
1344 
1345 
1346 
1347 
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 }