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 }