1   /*
2    * RDFpro - An extensible tool for building stream-oriented RDF processing libraries.
3    * 
4    * Written in 2014 by Francesco Corcoglioniti with support by Marco Amadori, Michele Mostarda,
5    * Alessio Palmero Aprosio and Marco Rospocher. Contact info on http://rdfpro.fbk.eu/
6    * 
7    * To the extent possible under law, the authors have dedicated all copyright and related and
8    * neighboring rights to this software to the public domain worldwide. This software is
9    * distributed without any warranty.
10   * 
11   * You should have received a copy of the CC0 Public Domain Dedication along with this software.
12   * If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
13   */
14  package eu.fbk.rdfpro.util;
15  
16  import java.io.File;
17  import java.io.IOException;
18  import java.math.BigDecimal;
19  import java.math.BigInteger;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collections;
23  import java.util.Comparator;
24  import java.util.Date;
25  import java.util.GregorianCalendar;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Objects;
29  import java.util.Set;
30  import java.util.concurrent.atomic.AtomicInteger;
31  import java.util.concurrent.atomic.AtomicLong;
32  import java.util.function.Function;
33  import java.util.function.Predicate;
34  
35  import javax.annotation.Nullable;
36  import javax.xml.datatype.DatatypeFactory;
37  import javax.xml.datatype.XMLGregorianCalendar;
38  
39  import org.openrdf.model.BNode;
40  import org.openrdf.model.Literal;
41  import org.openrdf.model.Resource;
42  import org.openrdf.model.Statement;
43  import org.openrdf.model.URI;
44  import org.openrdf.model.Value;
45  import org.openrdf.model.ValueFactory;
46  import org.openrdf.model.impl.ContextStatementImpl;
47  import org.openrdf.model.impl.StatementImpl;
48  import org.openrdf.model.impl.ValueFactoryImpl;
49  import org.openrdf.model.vocabulary.OWL;
50  import org.openrdf.model.vocabulary.RDF;
51  import org.openrdf.model.vocabulary.RDFS;
52  import org.openrdf.model.vocabulary.XMLSchema;
53  import org.openrdf.rio.RDFFormat;
54  import org.openrdf.rio.Rio;
55  
56  public final class Statements {
57  
58      public static final ValueFactory VALUE_FACTORY;
59  
60      static {
61          final boolean hashfactory = Boolean.parseBoolean(Environment.getProperty(
62                  "rdfpro.hashfactory", "true"));
63          if (hashfactory) {
64              VALUE_FACTORY = HashValueFactory.INSTANCE;
65          } else {
66              VALUE_FACTORY = ValueFactoryImpl.getInstance();
67          }
68      }
69  
70      public static final Function<Value, Value> VALUE_NORMALIZER = new Function<Value, Value>() {
71  
72          @Override
73          public Value apply(final Value value) {
74              return normalize(value);
75          }
76  
77      };
78  
79      public static final DatatypeFactory DATATYPE_FACTORY;
80  
81      public static final Set<URI> TBOX_CLASSES = Collections.unmodifiableSet(new HashSet<URI>(
82              Arrays.asList(RDFS.CLASS, RDFS.DATATYPE, RDF.PROPERTY,
83                      VALUE_FACTORY.createURI(OWL.NAMESPACE, "AllDisjointClasses"),
84                      VALUE_FACTORY.createURI(OWL.NAMESPACE, "AllDisjointProperties"),
85                      OWL.ANNOTATIONPROPERTY,
86                      VALUE_FACTORY.createURI(OWL.NAMESPACE, "AsymmetricProperty"), OWL.CLASS,
87                      OWL.DATATYPEPROPERTY, OWL.FUNCTIONALPROPERTY, OWL.INVERSEFUNCTIONALPROPERTY,
88                      VALUE_FACTORY.createURI(OWL.NAMESPACE, "IrreflexiveProperty"),
89                      OWL.OBJECTPROPERTY, OWL.ONTOLOGY,
90                      VALUE_FACTORY.createURI(OWL.NAMESPACE, "ReflexiveProperty"), OWL.RESTRICTION,
91                      OWL.SYMMETRICPROPERTY, OWL.TRANSITIVEPROPERTY)));
92  
93      // NOTE: rdf:first and rdf:rest considered as TBox statements as used (essentially) for
94      // encoding OWL axioms
95  
96      public static final Set<URI> TBOX_PROPERTIES = Collections.unmodifiableSet(new HashSet<URI>(
97              Arrays.asList(RDF.FIRST, RDF.REST, RDFS.DOMAIN, RDFS.RANGE, RDFS.SUBCLASSOF,
98                      RDFS.SUBPROPERTYOF, OWL.ALLVALUESFROM, OWL.CARDINALITY, OWL.COMPLEMENTOF,
99                      VALUE_FACTORY.createURI(OWL.NAMESPACE, "datatypeComplementOf"),
100                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "disjointUnionOf"), OWL.DISJOINTWITH,
101                     OWL.EQUIVALENTCLASS, OWL.EQUIVALENTPROPERTY,
102                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "hasKey"),
103                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "hasSelf"), OWL.HASVALUE, OWL.IMPORTS,
104                     OWL.INTERSECTIONOF, OWL.INVERSEOF, OWL.MAXCARDINALITY,
105                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "maxQualifiedCardinality"),
106                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "members"), OWL.MINCARDINALITY,
107                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "minQualifiedCardinality"),
108                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "onClass"),
109                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "onDataRange"),
110                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "onDataType"),
111                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "onProperties"), OWL.ONPROPERTY,
112                     OWL.ONEOF, VALUE_FACTORY.createURI(OWL.NAMESPACE, "propertyChainAxiom"),
113                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "propertyDisjointWith"),
114                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "qualifiedCardinality"),
115                     OWL.SOMEVALUESFROM, OWL.UNIONOF, OWL.VERSIONIRI,
116                     VALUE_FACTORY.createURI(OWL.NAMESPACE, "withRestrictions"))));
117 
118     private static final Comparator<Value> DEFAULT_VALUE_ORDERING = new ValueComparator();
119 
120     private static final Comparator<Statement> DEFAULT_STATEMENT_ORDERING = new StatementComparator(
121             "spoc", new ValueComparator(RDF.NAMESPACE));
122     static {
123         try {
124             DATATYPE_FACTORY = DatatypeFactory.newInstance();
125         } catch (final Throwable ex) {
126             throw new Error("Unexpected exception (!): " + ex.getMessage(), ex);
127         }
128     }
129 
130     private static final Hash NIL_HASH = Hash.murmur3("\u0001");
131 
132     public static Comparator<Value> valueComparator(final String... rankedNamespaces) {
133         return rankedNamespaces == null || rankedNamespaces.length == 0 ? DEFAULT_VALUE_ORDERING
134                 : new ValueComparator(rankedNamespaces);
135     }
136 
137     public static Comparator<Statement> statementComparator(@Nullable final String components,
138             @Nullable final Comparator<? super Value> valueComparator) {
139         if (components == null) {
140             return valueComparator == null ? DEFAULT_STATEMENT_ORDERING //
141                     : new StatementComparator("spoc", valueComparator);
142         } else {
143             return new StatementComparator(components,
144                     valueComparator == null ? DEFAULT_VALUE_ORDERING : valueComparator);
145         }
146     }
147 
148     @SuppressWarnings("unchecked")
149     @Nullable
150     public static Predicate<Statement> statementMatcher(@Nullable final String spec) {
151         if (spec == null) {
152             return null;
153         } else if (Scripting.isScript(spec)) {
154             return Scripting.compile(Predicate.class, spec, "q");
155         } else {
156             return new StatementMatcher(spec);
157         }
158     }
159 
160     public static Statement normalize(final Statement statement) {
161         final Resource subj = normalize(statement.getSubject());
162         final URI pred = normalize(statement.getPredicate());
163         final Value obj = normalize(statement.getObject());
164         final Resource ctx = normalize(statement.getContext());
165         if (subj == statement.getSubject() && pred == statement.getPredicate()
166                 && obj == statement.getObject() && ctx == statement.getContext()) {
167             return statement;
168         } else if (ctx != null) {
169             return new ContextStatementImpl(subj, pred, obj, ctx);
170         } else {
171             return new StatementImpl(subj, pred, obj);
172         }
173     }
174 
175     @Nullable
176     public static <T extends Value> T normalize(@Nullable final T value) {
177         if (VALUE_FACTORY instanceof HashValueFactory) {
178             return HashValueFactory.normalize(value);
179         }
180         return value;
181     }
182 
183     public static Hash getHash(final Statement statement) {
184         return statement instanceof Hashable ? ((Hashable) statement).getHash()
185                 : computeHash(statement);
186     }
187 
188     public static Hash getHash(@Nullable final Value value) {
189         return value instanceof Hashable ? ((Hashable) value).getHash() : computeHash(value);
190     }
191 
192     public static Hash computeHash(final Statement statement) {
193         final Hash subjHash = getHash(statement.getSubject());
194         final Hash predHash = getHash(statement.getPredicate());
195         final Hash objHash = getHash(statement.getObject());
196         final Hash ctxHash = getHash(statement.getContext());
197         return Hash.combine(subjHash, predHash, objHash, ctxHash);
198     }
199 
200     public static Hash computeHash(@Nullable final Value value) {
201         if (value == null) {
202             return NIL_HASH;
203         }
204         Hash hash;
205         if (value instanceof URI) {
206             hash = Hash.murmur3("\u0001", value.stringValue());
207         } else if (value instanceof BNode) {
208             hash = Hash.murmur3("\u0002", ((BNode) value).getID());
209         } else {
210             final Literal l = (Literal) value;
211             if (l.getLanguage() != null) {
212                 hash = Hash.murmur3("\u0003", l.getLanguage(), l.getLabel());
213             } else if (l.getDatatype() != null) {
214                 hash = Hash.murmur3("\u0004", l.getDatatype().stringValue(), l.getLabel());
215             } else {
216                 hash = Hash.murmur3("\u0005", l.getLabel());
217             }
218         }
219         if (hash.getLow() == 0) {
220             hash = Hash.fromLongs(hash.getHigh(), 1L);
221         }
222         return hash;
223     }
224 
225     @Nullable
226     public static File toRDFFile(final String fileSpec) {
227         final int index = fileSpec.indexOf(':');
228         if (index > 0) {
229             final String name = "test." + fileSpec.substring(0, index);
230             if (Rio.getParserFormatForFileName(name) != null
231                     || Rio.getWriterFormatForFileName(name) != null) {
232                 return new File(fileSpec.substring(index + 1));
233             }
234         }
235         return new File(fileSpec);
236     }
237 
238     public static RDFFormat toRDFFormat(final String fileSpec) {
239         final int index = fileSpec.indexOf(':');
240         RDFFormat format = null;
241         if (index > 0) {
242             final String name = "test." + fileSpec.substring(0, index);
243             format = Rio.getParserFormatForFileName(name);
244             if (format == null) {
245                 format = Rio.getWriterFormatForFileName(name);
246             }
247         }
248         if (format == null) {
249             format = Rio.getParserFormatForFileName(fileSpec);
250             if (format == null) {
251                 format = Rio.getWriterFormatForFileName(fileSpec);
252             }
253         }
254         if (format == null) {
255             throw new IllegalArgumentException("Unknown RDF format for " + fileSpec);
256         }
257         return format;
258     }
259 
260     public static boolean isRDFFormatTextBased(final RDFFormat format) {
261         for (final String ext : format.getFileExtensions()) {
262             if (ext.equalsIgnoreCase("rdf") || ext.equalsIgnoreCase("rj")
263                     || ext.equalsIgnoreCase("jsonld") || ext.equalsIgnoreCase("nt")
264                     || ext.equalsIgnoreCase("nq") || ext.equalsIgnoreCase("trix")
265                     || ext.equalsIgnoreCase("trig") || ext.equalsIgnoreCase("tql")
266                     || ext.equalsIgnoreCase("ttl") || ext.equalsIgnoreCase("n3")) {
267                 return true;
268             }
269         }
270         return false;
271     }
272 
273     public static boolean isRDFFormatLineBased(final RDFFormat format) {
274         for (final String ext : format.getFileExtensions()) {
275             if (ext.equalsIgnoreCase("nt") || ext.equalsIgnoreCase("nq")
276                     || ext.equalsIgnoreCase("tql")) {
277                 return true;
278             }
279         }
280         return false;
281     }
282 
283     public static Value shortenValue(final Value value, final int threshold) {
284         if (value instanceof Literal) {
285             final Literal literal = (Literal) value;
286             final URI datatype = literal.getDatatype();
287             final String language = literal.getLanguage();
288             final String label = ((Literal) value).getLabel();
289             if (label.length() > threshold
290                     && (datatype == null || datatype.equals(XMLSchema.STRING))) {
291                 int offset = threshold;
292                 for (int i = threshold; i >= 0; --i) {
293                     if (Character.isWhitespace(label.charAt(i))) {
294                         offset = i;
295                         break;
296                     }
297                 }
298                 final String newLabel = label.substring(0, offset) + "...";
299                 if (language != null) {
300                     return VALUE_FACTORY.createLiteral(newLabel, language);
301                 } else if (datatype != null) {
302                     return VALUE_FACTORY.createLiteral(newLabel, datatype);
303                 } else {
304                     return VALUE_FACTORY.createLiteral(newLabel);
305                 }
306             }
307         }
308         return value;
309     }
310 
311     @Nullable
312     public static String formatValue(@Nullable final Value value) {
313         return formatValue(value, null);
314     }
315 
316     public static String formatValue(@Nullable final Value value,
317             @Nullable final Namespaces namespaces) {
318         if (value == null) {
319             return null;
320         }
321         try {
322             final StringBuilder builder = new StringBuilder(value.stringValue().length() * 2);
323             formatValue(value, namespaces, builder);
324             return builder.toString();
325         } catch (final Throwable ex) {
326             throw new Error("Unexpected exception (!)", ex);
327         }
328     }
329 
330     public static void formatValue(final Value value, @Nullable final Namespaces namespaces,
331             final Appendable out) throws IOException {
332         if (value instanceof URI) {
333             formatURI((URI) value, out, namespaces);
334         } else if (value instanceof BNode) {
335             formatBNode((BNode) value, out);
336         } else if (value instanceof Literal) {
337             formatLiteral((Literal) value, out, namespaces);
338         } else {
339             throw new Error("Unexpected value class (!): " + value.getClass().getName());
340         }
341     }
342 
343     private static boolean isGoodQName(final String prefix, final String name) { // good to emit
344 
345         final int prefixLen = prefix.length();
346         if (prefixLen > 0) {
347             if (!isPN_CHARS_BASE(prefix.charAt(0)) || !isPN_CHARS(prefix.charAt(prefixLen - 1))) {
348                 return false;
349             }
350             for (int i = 1; i < prefixLen - 1; ++i) {
351                 final char c = prefix.charAt(i);
352                 if (!isPN_CHARS(c) && c != '.') {
353                     return false;
354                 }
355             }
356         }
357 
358         final int nameLen = name.length();
359         if (nameLen > 0) {
360             int i = 0;
361             while (i < nameLen) {
362                 final char c = name.charAt(i++);
363                 if (!isPN_CHARS_BASE(c) && c != ':' && (i != 1 || c != '_' && !isNumber(c))
364                         && (i == 1 || !isPN_CHARS(c) && (i == nameLen || c != '.'))) {
365                     return false;
366                 }
367             }
368         }
369 
370         return true;
371     }
372 
373     private static void formatURI(final URI uri, final Appendable out,
374             @Nullable final Namespaces namespaces) throws IOException {
375 
376         if (namespaces != null) {
377             final String prefix = namespaces.prefixFor(uri.getNamespace());
378             if (prefix != null) {
379                 final String name = uri.getLocalName();
380                 if (isGoodQName(prefix, name)) {
381                     out.append(prefix);
382                     out.append(":");
383                     out.append(name);
384                     return;
385                 }
386             }
387         }
388 
389         final String string = uri.stringValue();
390         final int len = string.length();
391         out.append('<');
392         for (int i = 0; i < len; ++i) {
393             final char ch = string.charAt(i);
394             switch (ch) {
395             case 0x22: // "
396                 out.append("\\u0022");
397                 break;
398             case 0x3C: // <
399                 out.append("\\u003C");
400                 break;
401             case 0x3E: // >
402                 out.append("\\u003E");
403                 break;
404             case 0x5C: // \
405                 out.append("\\u005C");
406                 break;
407             case 0x5E: // ^
408                 out.append("\\u005E");
409                 break;
410             case 0x60: // `
411                 out.append("\\u0060");
412                 break;
413             case 0x7B: // {
414                 out.append("\\u007B");
415                 break;
416             case 0x7C: // |
417                 out.append("\\u007C");
418                 break;
419             case 0x7D: // }
420                 out.append("\\u007D");
421                 break;
422             case 0x7F: // delete control char (not strictly necessary)
423                 out.append("\\u007F");
424                 break;
425             default:
426                 if (ch <= 32) { // control char and ' '
427                     out.append("\\u00").append(Character.forDigit(ch / 16, 16))
428                             .append(Character.forDigit(ch % 16, 16));
429                 } else {
430                     out.append(ch);
431                 }
432             }
433         }
434         out.append('>');
435     }
436 
437     private static void formatBNode(final BNode bnode, final Appendable out) throws IOException {
438         final String id = bnode.getID();
439         final int last = id.length() - 1;
440         out.append('_').append(':');
441         if (last < 0) {
442             out.append("genid-hash-").append(Integer.toHexString(System.identityHashCode(bnode)));
443         } else {
444             char ch = id.charAt(0);
445             if (isPN_CHARS_U(ch) || isNumber(ch)) {
446                 out.append(ch);
447             } else {
448                 out.append("genid-start-").append(ch);
449             }
450             if (last > 0) {
451                 for (int i = 1; i < last; ++i) {
452                     ch = id.charAt(i);
453                     if (isPN_CHARS(ch) || ch == '.') {
454                         out.append(ch);
455                     } else {
456                         out.append(Integer.toHexString(ch));
457                     }
458                 }
459                 ch = id.charAt(last);
460                 if (isPN_CHARS(ch)) {
461                     out.append(ch);
462                 } else {
463                     out.append(Integer.toHexString(ch));
464                 }
465             }
466         }
467     }
468 
469     private static void formatLiteral(final Literal literal, final Appendable out,
470             @Nullable final Namespaces namespaces) throws IOException {
471         final String label = literal.getLabel();
472         final int length = label.length();
473         out.append('"');
474         for (int i = 0; i < length; ++i) {
475             final char ch = label.charAt(i);
476             switch (ch) {
477             case 0x08: // \b
478                 out.append('\\');
479                 out.append('b');
480                 break;
481             case 0x09: // \t
482                 out.append('\\');
483                 out.append('t');
484                 break;
485             case 0x0A: // \n
486                 out.append('\\');
487                 out.append('n');
488                 break;
489             case 0x0C: // \f
490                 out.append('\\');
491                 out.append('f');
492                 break;
493             case 0x0D: // \r
494                 out.append('\\');
495                 out.append('r');
496                 break;
497             case 0x22: // "
498                 out.append('\\');
499                 out.append('"');
500                 break;
501             case 0x5C: // \
502                 out.append('\\');
503                 out.append('\\');
504                 break;
505             case 0x7F: // delete control char
506                 out.append("\\u007F");
507                 break;
508             default:
509                 if (ch < 32) { // other control char (not strictly necessary)
510                     out.append("\\u00");
511                     out.append(Character.forDigit(ch / 16, 16));
512                     out.append(Character.forDigit(ch % 16, 16));
513                 } else {
514                     out.append(ch);
515                 }
516             }
517         }
518         out.append('"');
519         final String language = literal.getLanguage();
520         if (language != null) {
521             out.append('@');
522             final int len = language.length();
523             boolean minusFound = false;
524             boolean valid = true;
525             for (int i = 0; i < len; ++i) {
526                 final char ch = language.charAt(i);
527                 if (ch == '-') {
528                     minusFound = true;
529                     if (i == 0) {
530                         valid = false;
531                     } else {
532                         final char prev = language.charAt(i - 1);
533                         valid &= isLetter(prev) || isNumber(prev);
534                     }
535                 } else if (isNumber(ch)) {
536                     valid &= minusFound;
537                 } else {
538                     valid &= isLetter(ch);
539                 }
540                 out.append(ch);
541             }
542             if (!valid || language.charAt(len - 1) == '-') {
543                 throw new IllegalArgumentException("Invalid language tag '" + language + "' in '"
544                         + literal + "'");
545             }
546         } else {
547             final URI datatype = literal.getDatatype();
548             if (datatype != null && !XMLSchema.STRING.equals(datatype)) {
549                 out.append('^');
550                 out.append('^');
551                 formatURI(datatype, out, namespaces);
552             }
553         }
554     }
555 
556     @Nullable
557     public static Value parseValue(@Nullable final CharSequence sequence) {
558         return parseValue(sequence, null);
559     }
560 
561     public static Value parseValue(@Nullable final CharSequence sequence,
562             @Nullable final Namespaces namespaces) {
563         if (sequence == null) {
564             return null;
565         }
566         final int c = sequence.charAt(0);
567         if (c == '_') {
568             return parseBNode(sequence);
569         } else if (c == '"' || c == '\'') {
570             return parseLiteral(sequence, namespaces);
571         } else {
572             return parseURI(sequence, namespaces);
573         }
574     }
575 
576     private static URI parseURI(final CharSequence sequence, @Nullable final Namespaces namespaces) {
577 
578         if (sequence.charAt(0) == '<') {
579             final int last = sequence.length() - 1;
580             final StringBuilder builder = new StringBuilder(last - 1);
581             if (sequence.charAt(last) != '>') {
582                 throw new IllegalArgumentException("Invalid URI: " + sequence);
583             }
584             int i = 1;
585             while (i < last) {
586                 char c = sequence.charAt(i++);
587                 if (c < 32) { // discard control chars but accept other chars forbidden by W3C
588                     throw new IllegalArgumentException("Invalid char '" + c + "' in URI: "
589                             + sequence);
590                 } else if (c != '\\') {
591                     builder.append(c);
592                 } else {
593                     if (i == last) {
594                         throw new IllegalArgumentException("Invalid URI: " + sequence);
595                     }
596                     c = sequence.charAt(i++);
597                     if (c == 'u') {
598                         builder.append(parseHex(sequence, i, 4));
599                         i += 4;
600                     } else if (c == 'U') {
601                         builder.append(parseHex(sequence, i, 8));
602                         i += 8;
603                     } else {
604                         builder.append(c); // accept \> and \\ plus others
605                     }
606                 }
607             }
608             return VALUE_FACTORY.createURI(builder.toString());
609 
610         } else if (namespaces != null) {
611             final int len = sequence.length();
612             final StringBuilder builder = new StringBuilder(len);
613 
614             int i = 0;
615             while (i < len) {
616                 final char c = sequence.charAt(i++);
617                 if (c == ':') {
618                     if (i > 2 && !isPN_CHARS(sequence.charAt(i - 2))) {
619                         throw new IllegalArgumentException("Invalid qname " + sequence);
620                     }
621                     break;
622                 } else if (i == 1 && !isPN_CHARS_BASE(c) || i > 1 && !isPN_CHARS(c) && c != '.') {
623                     throw new IllegalArgumentException("Invalid qname " + sequence);
624                 } else {
625                     builder.append(c);
626                 }
627             }
628             final String prefix = builder.toString();
629             final String namespace = namespaces.uriFor(prefix);
630             if (namespace == null) {
631                 throw new IllegalArgumentException("Unknown prefix " + prefix);
632             }
633             builder.setLength(0);
634 
635             while (i < len) {
636                 final char c = sequence.charAt(i++);
637                 if (c == '%') {
638                     builder.append(parseHex(sequence, i, 2));
639                     i += 2;
640                 } else if (c == '\\') {
641                     final char d = sequence.charAt(i++);
642                     if (d == '_' || d == '~' || d == '.' || d == '-' || d == '!' || d == '$'
643                             || d == '&' || d == '\'' || d == '(' || d == ')' || d == '*'
644                             || d == '+' || d == ',' || d == ';' || d == '=' || d == '/'
645                             || d == '?' || d == '#' || d == '@' || d == '%') {
646                         builder.append(d);
647                     } else {
648                         throw new IllegalArgumentException("Invalid qname " + sequence);
649                     }
650                 } else if (isPN_CHARS_BASE(c) || c == ':' || i == 1 && (c == '_' || isNumber(c))
651                         || i > 1 && (isPN_CHARS(c) || i < len && c == '.')) {
652                     builder.append(c);
653                 } else {
654                     throw new IllegalArgumentException("Invalid qname " + sequence);
655                 }
656             }
657             final String name = builder.toString();
658 
659             return VALUE_FACTORY.createURI(namespace, name);
660         }
661 
662         throw new IllegalArgumentException("Unsupported qname " + sequence);
663     }
664 
665     private static BNode parseBNode(final CharSequence sequence) {
666         final int len = sequence.length();
667         if (len > 2 && sequence.charAt(1) == ':') {
668             final StringBuilder builder = new StringBuilder(len - 2);
669             boolean ok = true;
670             char c = sequence.charAt(2);
671             builder.append(c);
672             ok &= isPN_CHARS_U(c) || isNumber(c);
673             for (int i = 2; i < len - 1; ++i) {
674                 c = sequence.charAt(i);
675                 builder.append(c);
676                 ok &= isPN_CHARS(c) || c == '.';
677             }
678             c = sequence.charAt(len - 1);
679             builder.append(c);
680             ok &= isPN_CHARS(c);
681             if (ok) {
682                 return VALUE_FACTORY.createBNode(builder.toString());
683             }
684         }
685         throw new IllegalArgumentException("Invalid BNode '" + sequence + "'");
686     }
687 
688     private static Literal parseLiteral(final CharSequence sequence,
689             @Nullable final Namespaces namespaces) {
690 
691         final StringBuilder builder = new StringBuilder(sequence.length());
692         final int len = sequence.length();
693 
694         final char delim = sequence.charAt(0);
695         char c = 0;
696         int i = 1;
697         while (i < len && (c = sequence.charAt(i++)) != delim) {
698             if (c == '\\' && i < len) {
699                 c = sequence.charAt(i++);
700                 switch (c) {
701                 case 'b':
702                     builder.append('\b');
703                     break;
704                 case 'f':
705                     builder.append('\f');
706                     break;
707                 case 'n':
708                     builder.append('\n');
709                     break;
710                 case 'r':
711                     builder.append('\r');
712                     break;
713                 case 't':
714                     builder.append('\t');
715                     break;
716                 case 'u':
717                     builder.append(parseHex(sequence, i, 4));
718                     i += 4;
719                     break;
720                 case 'U':
721                     builder.append(parseHex(sequence, i, 8));
722                     i += 8;
723                     break;
724                 default:
725                     builder.append(c); // handles ' " \
726                     break;
727                 }
728             } else {
729                 builder.append(c);
730             }
731         }
732         final String label = builder.toString();
733 
734         if (i == len && c == delim) {
735             return VALUE_FACTORY.createLiteral(label);
736 
737         } else if (i < len - 2 && sequence.charAt(i) == '^' && sequence.charAt(i + 1) == '^') {
738             final URI datatype = parseURI(sequence.subSequence(i + 2, len), namespaces);
739             return VALUE_FACTORY.createLiteral(label, datatype);
740 
741         } else if (i < len - 1 && sequence.charAt(i) == '@') {
742             builder.setLength(0);
743             boolean minusFound = false;
744             for (int j = i + 1; j < len; ++j) {
745                 c = sequence.charAt(j);
746                 if (!isLetter(c) && (c != '-' || j == i + 1 || j == len - 1)
747                         && (!isNumber(c) || !minusFound)) {
748                     throw new IllegalArgumentException("Invalid lang in '" + sequence + "'");
749                 }
750                 minusFound |= c == '-';
751                 builder.append(c);
752             }
753             return VALUE_FACTORY.createLiteral(label, builder.toString());
754         }
755 
756         throw new IllegalArgumentException("Invalid literal '" + sequence + "'");
757     }
758 
759     private static char parseHex(final CharSequence sequence, final int index, final int count) {
760         int code = 0;
761         final int len = sequence.length();
762         if (index + count >= len) {
763             throw new IllegalArgumentException("Incomplete hex code '"
764                     + sequence.subSequence(index, len) + "' in RDF value '" + sequence + "'");
765         }
766         for (int i = 0; i < count; ++i) {
767             final char c = sequence.charAt(index + i);
768             final int digit = Character.digit(c, 16);
769             if (digit < 0) {
770                 throw new IllegalArgumentException("Invalid hex digit '" + c + "' in RDF value '"
771                         + sequence + "'");
772             }
773             code = code * 16 + digit;
774         }
775         return (char) code;
776     }
777 
778     private static boolean isPN_CHARS(final int c) { // ok
779         return isPN_CHARS_U(c) || isNumber(c) || c == '-' || c == 0x00B7 || c >= 0x0300
780                 && c <= 0x036F || c >= 0x203F && c <= 0x2040;
781     }
782 
783     private static boolean isPN_CHARS_U(final int c) { // ok
784         return isPN_CHARS_BASE(c) || c == '_';
785     }
786 
787     private static boolean isPN_CHARS_BASE(final int c) { // ok
788         return isLetter(c) || c >= 0x00C0 && c <= 0x00D6 || c >= 0x00D8 && c <= 0x00F6
789                 || c >= 0x00F8 && c <= 0x02FF || c >= 0x0370 && c <= 0x037D || c >= 0x037F
790                 && c <= 0x1FFF || c >= 0x200C && c <= 0x200D || c >= 0x2070 && c <= 0x218F
791                 || c >= 0x2C00 && c <= 0x2FEF || c >= 0x3001 && c <= 0xD7FF || c >= 0xF900
792                 && c <= 0xFDCF || c >= 0xFDF0 && c <= 0xFFFD || c >= 0x10000 && c <= 0xEFFFF;
793     }
794 
795     private static boolean isLetter(final int c) {
796         return c >= 65 && c <= 90 || c >= 97 && c <= 122;
797     }
798 
799     private static boolean isNumber(final int c) {
800         return c >= 48 && c <= 57;
801     }
802 
803     /**
804      * General conversion facility. This method attempts to convert a supplied {@code object} to
805      * an instance of the class specified. If the input is null, null is returned. If conversion
806      * is unsupported or fails, an exception is thrown. The following table lists the supported
807      * conversions: <blockquote>
808      * <table border="1" summary="supported conversions">
809      * <thead>
810      * <tr>
811      * <th>From classes (and sub-classes)</th>
812      * <th>To classes (and super-classes)</th>
813      * </tr>
814      * </thead><tbody>
815      * <tr>
816      * <td>{@link Boolean}, {@link Literal} ({@code xsd:boolean})</td>
817      * <td>{@link Boolean}, {@link Literal} ({@code xsd:boolean}), {@link String}</td>
818      * </tr>
819      * <tr>
820      * <td>{@link String}, {@link Literal} (plain, {@code xsd:string})</td>
821      * <td>{@link String}, {@link Literal} (plain, {@code xsd:string}), {@code URI} (as uri
822      * string), {@code BNode} (as BNode ID), {@link Integer}, {@link Long}, {@link Double},
823      * {@link Float}, {@link Short}, {@link Byte}, {@link BigDecimal}, {@link BigInteger},
824      * {@link AtomicInteger}, {@link AtomicLong}, {@link Boolean}, {@link XMLGregorianCalendar},
825      * {@link GregorianCalendar}, {@link Date} (via parsing), {@link Character} (length &gt;= 1)</td>
826      * </tr>
827      * <tr>
828      * <td>{@link Number}, {@link Literal} (any numeric {@code xsd:} type)</td>
829      * <td>{@link Literal} (top-level numeric {@code xsd:} type), {@link Integer}, {@link Long},
830      * {@link Double}, {@link Float}, {@link Short}, {@link Byte}, {@link BigDecimal},
831      * {@link BigInteger}, {@link AtomicInteger}, {@link AtomicLong}, {@link String}</td>
832      * </tr>
833      * <tr>
834      * <td>{@link Date}, {@link GregorianCalendar}, {@link XMLGregorianCalendar}, {@link Literal}
835      * ({@code xsd:dateTime}, {@code xsd:date})</td>
836      * <td>{@link Date}, {@link GregorianCalendar}, {@link XMLGregorianCalendar}, {@link Literal}
837      * ({@code xsd:dateTime}), {@link String}</td>
838      * </tr>
839      * <tr>
840      * <td>{@link URI}</td>
841      * <td>{@link URI}, {@link String}</td>
842      * </tr>
843      * <tr>
844      * <td>{@link BNode}</td>
845      * <td>{@link BNode}, {@link URI} (skolemization), {@link String}</td>
846      * </tr>
847      * <tr>
848      * <td>{@link Statement}</td>
849      * <td>{@link Statement}, {@link String}</td>
850      * </tr>
851      * </tbody>
852      * </table>
853      * </blockquote>
854      *
855      * @param object
856      *            the object to convert, possibly null
857      * @param clazz
858      *            the class to convert to, not null
859      * @param <T>
860      *            the type of result
861      * @return the result of the conversion, or null if {@code object} was null
862      * @throws IllegalArgumentException
863      *             in case conversion fails or is unsupported for the {@code object} and class
864      *             specified
865      */
866     @SuppressWarnings("unchecked")
867     @Nullable
868     public static <T> T convert(@Nullable final Object object, final Class<T> clazz)
869             throws IllegalArgumentException {
870         if (object == null) {
871             Objects.requireNonNull(clazz);
872             return null;
873         }
874         if (clazz.isInstance(object)) {
875             return (T) object;
876         }
877         final T result = (T) convertObject(object, clazz);
878         if (result != null) {
879             return result;
880         }
881         throw new IllegalArgumentException("Unsupported conversion of " + object + " to " + clazz);
882     }
883 
884     /**
885      * General conversion facility, with fall back to default value. This method operates as
886      * {@link #convert(Object, Class)}, but in case the input is null or conversion is not
887      * supported returns the specified default value.
888      *
889      * @param object
890      *            the object to convert, possibly null
891      * @param clazz
892      *            the class to convert to, not null
893      * @param defaultValue
894      *            the default value to fall back to
895      * @param <T>
896      *            the type of result
897      * @return the result of the conversion, or the default value if {@code object} was null,
898      *         conversion failed or is unsupported
899      */
900     @SuppressWarnings("unchecked")
901     @Nullable
902     public static <T> T convert(@Nullable final Object object, final Class<T> clazz,
903             @Nullable final T defaultValue) {
904         if (object == null) {
905             Objects.requireNonNull(clazz);
906             return defaultValue;
907         }
908         if (clazz.isInstance(object)) {
909             return (T) object;
910         }
911         try {
912             final T result = (T) convertObject(object, clazz);
913             return result != null ? result : defaultValue;
914         } catch (final RuntimeException ex) {
915             return defaultValue;
916         }
917     }
918 
919     @Nullable
920     private static Object convertObject(final Object object, final Class<?> clazz) {
921         if (object instanceof Literal) {
922             return convertLiteral((Literal) object, clazz);
923         } else if (object instanceof URI) {
924             return convertURI((URI) object, clazz);
925         } else if (object instanceof String) {
926             return convertString((String) object, clazz);
927         } else if (object instanceof Number) {
928             return convertNumber((Number) object, clazz);
929         } else if (object instanceof Boolean) {
930             return convertBoolean((Boolean) object, clazz);
931         } else if (object instanceof XMLGregorianCalendar) {
932             return convertCalendar((XMLGregorianCalendar) object, clazz);
933         } else if (object instanceof BNode) {
934             return convertBNode((BNode) object, clazz);
935         } else if (object instanceof Statement) {
936             return convertStatement((Statement) object, clazz);
937         } else if (object instanceof GregorianCalendar) {
938             final XMLGregorianCalendar calendar = DATATYPE_FACTORY
939                     .newXMLGregorianCalendar((GregorianCalendar) object);
940             return clazz == XMLGregorianCalendar.class ? calendar : convertCalendar(calendar,
941                     clazz);
942         } else if (object instanceof Date) {
943             final GregorianCalendar calendar = new GregorianCalendar();
944             calendar.setTime((Date) object);
945             final XMLGregorianCalendar xmlCalendar = DATATYPE_FACTORY
946                     .newXMLGregorianCalendar(calendar);
947             return clazz == XMLGregorianCalendar.class ? xmlCalendar : convertCalendar(
948                     xmlCalendar, clazz);
949         } else if (object instanceof Enum<?>) {
950             return convertEnum((Enum<?>) object, clazz);
951         } else if (object instanceof File) {
952             return convertFile((File) object, clazz);
953         }
954         return null;
955     }
956 
957     @Nullable
958     private static Object convertStatement(final Statement statement, final Class<?> clazz) {
959         if (clazz.isAssignableFrom(String.class)) {
960             return statement.toString();
961         }
962         return null;
963     }
964 
965     @Nullable
966     private static Object convertLiteral(final Literal literal, final Class<?> clazz) {
967         final URI datatype = literal.getDatatype();
968         if (datatype == null || datatype.equals(XMLSchema.STRING)) {
969             return convertString(literal.getLabel(), clazz);
970         } else if (datatype.equals(XMLSchema.BOOLEAN)) {
971             return convertBoolean(literal.booleanValue(), clazz);
972         } else if (datatype.equals(XMLSchema.DATE) || datatype.equals(XMLSchema.DATETIME)) {
973             return convertCalendar(literal.calendarValue(), clazz);
974         } else if (datatype.equals(XMLSchema.INT)) {
975             return convertNumber(literal.intValue(), clazz);
976         } else if (datatype.equals(XMLSchema.LONG)) {
977             return convertNumber(literal.longValue(), clazz);
978         } else if (datatype.equals(XMLSchema.DOUBLE)) {
979             return convertNumber(literal.doubleValue(), clazz);
980         } else if (datatype.equals(XMLSchema.FLOAT)) {
981             return convertNumber(literal.floatValue(), clazz);
982         } else if (datatype.equals(XMLSchema.SHORT)) {
983             return convertNumber(literal.shortValue(), clazz);
984         } else if (datatype.equals(XMLSchema.BYTE)) {
985             return convertNumber(literal.byteValue(), clazz);
986         } else if (datatype.equals(XMLSchema.DECIMAL)) {
987             return convertNumber(literal.decimalValue(), clazz);
988         } else if (datatype.equals(XMLSchema.INTEGER)) {
989             return convertNumber(literal.integerValue(), clazz);
990         } else if (datatype.equals(XMLSchema.NON_NEGATIVE_INTEGER)
991                 || datatype.equals(XMLSchema.NON_POSITIVE_INTEGER)
992                 || datatype.equals(XMLSchema.NEGATIVE_INTEGER)
993                 || datatype.equals(XMLSchema.POSITIVE_INTEGER)) {
994             return convertNumber(literal.integerValue(), clazz); // infrequent integer cases
995         } else if (datatype.equals(XMLSchema.NORMALIZEDSTRING) || datatype.equals(XMLSchema.TOKEN)
996                 || datatype.equals(XMLSchema.NMTOKEN) || datatype.equals(XMLSchema.LANGUAGE)
997                 || datatype.equals(XMLSchema.NAME) || datatype.equals(XMLSchema.NCNAME)) {
998             return convertString(literal.getLabel(), clazz); // infrequent string cases
999         }
1000         return null;
1001     }
1002 
1003     @Nullable
1004     private static Object convertBoolean(final Boolean bool, final Class<?> clazz) {
1005         if (clazz == Boolean.class || clazz == boolean.class) {
1006             return bool;
1007         } else if (clazz.isAssignableFrom(Literal.class)) {
1008             return VALUE_FACTORY.createLiteral(bool);
1009         } else if (clazz.isAssignableFrom(String.class)) {
1010             return bool.toString();
1011         }
1012         return null;
1013     }
1014 
1015     @Nullable
1016     private static Object convertString(final String string, final Class<?> clazz) {
1017         if (clazz.isInstance(string)) {
1018             return string;
1019         } else if (clazz.isAssignableFrom(Literal.class)) {
1020             return VALUE_FACTORY.createLiteral(string, XMLSchema.STRING);
1021         } else if (clazz.isAssignableFrom(URI.class)) {
1022             return VALUE_FACTORY.createURI(string);
1023         } else if (clazz.isAssignableFrom(BNode.class)) {
1024             return VALUE_FACTORY.createBNode(string.startsWith("_:") ? string.substring(2)
1025                     : string);
1026         } else if (clazz == Boolean.class || clazz == boolean.class) {
1027             return Boolean.valueOf(string);
1028         } else if (clazz == Integer.class || clazz == int.class) {
1029             return Integer.valueOf((int) toLong(string));
1030         } else if (clazz == Long.class || clazz == long.class) {
1031             return Long.valueOf(toLong(string));
1032         } else if (clazz == Double.class || clazz == double.class) {
1033             return Double.valueOf(string);
1034         } else if (clazz == Float.class || clazz == float.class) {
1035             return Float.valueOf(string);
1036         } else if (clazz == Short.class || clazz == short.class) {
1037             return Short.valueOf((short) toLong(string));
1038         } else if (clazz == Byte.class || clazz == byte.class) {
1039             return Byte.valueOf((byte) toLong(string));
1040         } else if (clazz == BigDecimal.class) {
1041             return new BigDecimal(string);
1042         } else if (clazz == BigInteger.class) {
1043             return new BigInteger(string);
1044         } else if (clazz == AtomicInteger.class) {
1045             return new AtomicInteger(Integer.parseInt(string));
1046         } else if (clazz == AtomicLong.class) {
1047             return new AtomicLong(Long.parseLong(string));
1048         } else if (clazz == Date.class) {
1049             final String fixed = string.contains("T") ? string : string + "T00:00:00";
1050             return DATATYPE_FACTORY.newXMLGregorianCalendar(fixed).toGregorianCalendar().getTime();
1051         } else if (clazz.isAssignableFrom(GregorianCalendar.class)) {
1052             final String fixed = string.contains("T") ? string : string + "T00:00:00";
1053             return DATATYPE_FACTORY.newXMLGregorianCalendar(fixed).toGregorianCalendar();
1054         } else if (clazz.isAssignableFrom(XMLGregorianCalendar.class)) {
1055             final String fixed = string.contains("T") ? string : string + "T00:00:00";
1056             return DATATYPE_FACTORY.newXMLGregorianCalendar(fixed);
1057         } else if (clazz == Character.class || clazz == char.class) {
1058             return string.isEmpty() ? null : string.charAt(0);
1059         } else if (clazz.isEnum()) {
1060             for (final Object constant : clazz.getEnumConstants()) {
1061                 if (string.equalsIgnoreCase(((Enum<?>) constant).name())) {
1062                     return constant;
1063                 }
1064             }
1065             throw new IllegalArgumentException("Illegal " + clazz.getSimpleName() + " constant: "
1066                     + string);
1067         } else if (clazz == File.class) {
1068             return new File(string);
1069         }
1070         return null;
1071     }
1072 
1073     @Nullable
1074     private static Object convertNumber(final Number number, final Class<?> clazz) {
1075         if (clazz.isAssignableFrom(Literal.class)) {
1076             if (number instanceof Integer || number instanceof AtomicInteger) {
1077                 return VALUE_FACTORY.createLiteral(number.intValue());
1078             } else if (number instanceof Long || number instanceof AtomicLong) {
1079                 return VALUE_FACTORY.createLiteral(number.longValue());
1080             } else if (number instanceof Double) {
1081                 return VALUE_FACTORY.createLiteral(number.doubleValue());
1082             } else if (number instanceof Float) {
1083                 return VALUE_FACTORY.createLiteral(number.floatValue());
1084             } else if (number instanceof Short) {
1085                 return VALUE_FACTORY.createLiteral(number.shortValue());
1086             } else if (number instanceof Byte) {
1087                 return VALUE_FACTORY.createLiteral(number.byteValue());
1088             } else if (number instanceof BigDecimal) {
1089                 return VALUE_FACTORY.createLiteral(number.toString(), XMLSchema.DECIMAL);
1090             } else if (number instanceof BigInteger) {
1091                 return VALUE_FACTORY.createLiteral(number.toString(), XMLSchema.INTEGER);
1092             }
1093         } else if (clazz.isAssignableFrom(String.class)) {
1094             return number.toString();
1095         } else if (clazz == Integer.class || clazz == int.class) {
1096             return Integer.valueOf(number.intValue());
1097         } else if (clazz == Long.class || clazz == long.class) {
1098             return Long.valueOf(number.longValue());
1099         } else if (clazz == Double.class || clazz == double.class) {
1100             return Double.valueOf(number.doubleValue());
1101         } else if (clazz == Float.class || clazz == float.class) {
1102             return Float.valueOf(number.floatValue());
1103         } else if (clazz == Short.class || clazz == short.class) {
1104             return Short.valueOf(number.shortValue());
1105         } else if (clazz == Byte.class || clazz == byte.class) {
1106             return Byte.valueOf(number.byteValue());
1107         } else if (clazz == BigDecimal.class) {
1108             return toBigDecimal(number);
1109         } else if (clazz == BigInteger.class) {
1110             return toBigInteger(number);
1111         } else if (clazz == AtomicInteger.class) {
1112             return new AtomicInteger(number.intValue());
1113         } else if (clazz == AtomicLong.class) {
1114             return new AtomicLong(number.longValue());
1115         }
1116         return null;
1117     }
1118 
1119     @Nullable
1120     private static Object convertCalendar(final XMLGregorianCalendar calendar, //
1121             final Class<?> clazz) {
1122         if (clazz.isInstance(calendar)) {
1123             return calendar;
1124         } else if (clazz.isAssignableFrom(Literal.class)) {
1125             return VALUE_FACTORY.createLiteral(calendar);
1126         } else if (clazz.isAssignableFrom(String.class)) {
1127             return calendar.toXMLFormat();
1128         } else if (clazz == Date.class) {
1129             return calendar.toGregorianCalendar().getTime();
1130         } else if (clazz.isAssignableFrom(GregorianCalendar.class)) {
1131             return calendar.toGregorianCalendar();
1132         }
1133         return null;
1134     }
1135 
1136     @Nullable
1137     private static Object convertURI(final URI uri, final Class<?> clazz) {
1138         if (clazz.isInstance(uri)) {
1139             return uri;
1140         } else if (clazz.isAssignableFrom(String.class)) {
1141             return uri.stringValue();
1142         } else if (clazz == File.class && uri.stringValue().startsWith("file://")) {
1143             return new File(uri.stringValue().substring(7));
1144         }
1145         return null;
1146     }
1147 
1148     @Nullable
1149     private static Object convertBNode(final BNode bnode, final Class<?> clazz) {
1150         if (clazz.isInstance(bnode)) {
1151             return bnode;
1152         } else if (clazz.isAssignableFrom(URI.class)) {
1153             return VALUE_FACTORY.createURI("bnode:" + bnode.getID());
1154         } else if (clazz.isAssignableFrom(String.class)) {
1155             return "_:" + bnode.getID();
1156         }
1157         return null;
1158     }
1159 
1160     @Nullable
1161     private static Object convertEnum(final Enum<?> constant, final Class<?> clazz) {
1162         if (clazz.isInstance(constant)) {
1163             return constant;
1164         } else if (clazz.isAssignableFrom(String.class)) {
1165             return constant.name();
1166         } else if (clazz.isAssignableFrom(Literal.class)) {
1167             return VALUE_FACTORY.createLiteral(constant.name(), XMLSchema.STRING);
1168         }
1169         return null;
1170     }
1171 
1172     @Nullable
1173     private static Object convertFile(final File file, final Class<?> clazz) {
1174         if (clazz.isInstance(file)) {
1175             return clazz.cast(file);
1176         } else if (clazz.isAssignableFrom(URI.class)) {
1177             return VALUE_FACTORY.createURI("file://" + file.getAbsolutePath());
1178         } else if (clazz.isAssignableFrom(String.class)) {
1179             return file.getAbsolutePath();
1180         }
1181         return null;
1182     }
1183 
1184     private static BigDecimal toBigDecimal(final Number number) {
1185         if (number instanceof BigDecimal) {
1186             return (BigDecimal) number;
1187         } else if (number instanceof BigInteger) {
1188             return new BigDecimal((BigInteger) number);
1189         } else if (number instanceof Double || number instanceof Float) {
1190             final double value = number.doubleValue();
1191             return Double.isInfinite(value) || Double.isNaN(value) ? null : new BigDecimal(value);
1192         } else {
1193             return new BigDecimal(number.longValue());
1194         }
1195     }
1196 
1197     private static BigInteger toBigInteger(final Number number) {
1198         if (number instanceof BigInteger) {
1199             return (BigInteger) number;
1200         } else if (number instanceof BigDecimal) {
1201             return ((BigDecimal) number).toBigInteger();
1202         } else if (number instanceof Double || number instanceof Float) {
1203             return new BigDecimal(number.doubleValue()).toBigInteger();
1204         } else {
1205             return BigInteger.valueOf(number.longValue());
1206         }
1207     }
1208 
1209     private static long toLong(final String string) {
1210         long multiplier = 1;
1211         final char c = string.charAt(string.length() - 1);
1212         if (c == 'k' || c == 'K') {
1213             multiplier = 1024;
1214         } else if (c == 'm' || c == 'M') {
1215             multiplier = 1024 * 1024;
1216         } else if (c == 'g' || c == 'G') {
1217             multiplier = 1024 * 1024 * 1024;
1218         }
1219         return Long.parseLong(multiplier == 1 ? string : string.substring(0, string.length() - 1))
1220                 * multiplier;
1221     }
1222 
1223     private Statements() {
1224     }
1225 
1226     private static final class ValueComparator implements Comparator<Value> {
1227 
1228         private final List<String> rankedNamespaces;
1229 
1230         public ValueComparator(@Nullable final String... rankedNamespaces) {
1231             this.rankedNamespaces = Arrays.asList(rankedNamespaces);
1232         }
1233 
1234         @Override
1235         public int compare(final Value v1, final Value v2) {
1236             if (v1 instanceof URI) {
1237                 if (v2 instanceof URI) {
1238                     final int rank1 = rankOf(((URI) v1).getNamespace());
1239                     final int rank2 = rankOf(((URI) v2).getNamespace());
1240                     if (rank1 >= 0 && (rank1 < rank2 || rank2 < 0)) {
1241                         return -1;
1242                     } else if (rank2 >= 0 && (rank2 < rank1 || rank1 < 0)) {
1243                         return 1;
1244                     }
1245                     final String string1 = Statements.formatValue(v1, Namespaces.DEFAULT);
1246                     final String string2 = Statements.formatValue(v2, Namespaces.DEFAULT);
1247                     return string1.compareTo(string2);
1248                 } else {
1249                     return -1;
1250                 }
1251             } else if (v1 instanceof BNode) {
1252                 if (v2 instanceof BNode) {
1253                     return ((BNode) v1).getID().compareTo(((BNode) v2).getID());
1254                 } else if (v2 instanceof URI) {
1255                     return 1;
1256                 } else {
1257                     return -1;
1258                 }
1259             } else if (v1 instanceof Literal) {
1260                 if (v2 instanceof Literal) {
1261                     return ((Literal) v1).getLabel().compareTo(((Literal) v2).getLabel());
1262                 } else if (v2 instanceof Resource) {
1263                     return 1;
1264                 } else {
1265                     return -1;
1266                 }
1267             } else {
1268                 if (v1 == v2) {
1269                     return 0;
1270                 } else {
1271                     return 1;
1272                 }
1273             }
1274         }
1275 
1276         private int rankOf(final String ns) {
1277             for (int i = 0; i < this.rankedNamespaces.size(); ++i) {
1278                 if (ns.startsWith(this.rankedNamespaces.get(i))) {
1279                     return i;
1280                 }
1281             }
1282             return -1;
1283         }
1284 
1285     }
1286 
1287     private static final class StatementComparator implements Comparator<Statement> {
1288 
1289         private final String components;
1290 
1291         private final Comparator<? super Value> valueComparator;
1292 
1293         public StatementComparator(final String components,
1294                 final Comparator<? super Value> valueComparator) {
1295             this.components = components.trim().toLowerCase();
1296             this.valueComparator = Objects.requireNonNull(valueComparator);
1297             for (int i = 0; i < this.components.length(); ++i) {
1298                 final char c = this.components.charAt(i);
1299                 if (c != 's' && c != 'p' && c != 'o' && c != 'c') {
1300                     throw new IllegalArgumentException("Invalid components: " + components);
1301                 }
1302             }
1303         }
1304 
1305         @Override
1306         public int compare(final Statement s1, final Statement s2) {
1307             for (int i = 0; i < this.components.length(); ++i) {
1308                 final char c = this.components.charAt(i);
1309                 final Value v1 = getValue(s1, c);
1310                 final Value v2 = getValue(s2, c);
1311                 final int result = this.valueComparator.compare(v1, v2);
1312                 if (result != 0) {
1313                     return result;
1314                 }
1315             }
1316             return 0;
1317         }
1318 
1319         private Value getValue(final Statement statement, final char component) {
1320             switch (component) {
1321             case 's':
1322                 return statement.getSubject();
1323             case 'p':
1324                 return statement.getPredicate();
1325             case 'o':
1326                 return statement.getObject();
1327             case 'c':
1328                 return statement.getContext();
1329             default:
1330                 throw new Error();
1331             }
1332         }
1333 
1334     }
1335 
1336     private static final class StatementMatcher implements Predicate<Statement> {
1337 
1338         @Nullable
1339         private final ValueMatcher subjMatcher;
1340 
1341         @Nullable
1342         private final ValueMatcher predMatcher;
1343 
1344         @Nullable
1345         private final ValueMatcher objMatcher;
1346 
1347         @Nullable
1348         private final ValueMatcher ctxMatcher;
1349 
1350         @SuppressWarnings("unchecked")
1351         public StatementMatcher(final String spec) {
1352 
1353             // Initialize the arrays used to create the ValueTransformers
1354             final List<?>[] expressions = new List<?>[4];
1355             final Boolean[] includes = new Boolean[4];
1356             for (int i = 0; i < 4; ++i) {
1357                 expressions[i] = new ArrayList<String>();
1358             }
1359 
1360             // Parse the specification string
1361             char action = 0;
1362             final List<Integer> components = new ArrayList<Integer>();
1363             for (final String token : spec.split("\\s+")) {
1364                 final char ch0 = token.charAt(0);
1365                 if (ch0 == '+' || ch0 == '-') {
1366                     action = ch0;
1367                     if (token.length() == 1) {
1368                         throw new IllegalArgumentException("No component(s) specified in '" + spec
1369                                 + "'");
1370                     }
1371                     components.clear();
1372                     for (int i = 1; i < token.length(); ++i) {
1373                         final char ch1 = Character.toLowerCase(token.charAt(i));
1374                         final int component = ch1 == 's' ? 0 : ch1 == 'p' ? 1 : ch1 == 'o' ? 2
1375                                 : ch1 == 'c' ? 3 : -1;
1376                         if (component < 0) {
1377                             throw new IllegalArgumentException("Invalid component '" + ch1
1378                                     + "' in '" + spec + "'");
1379                         }
1380                         components.add(component);
1381                     }
1382                 } else if (action == 0) {
1383                     throw new IllegalArgumentException("Missing selector in '" + spec + "'");
1384                 } else {
1385                     for (final int component : components) {
1386                         ((List<String>) expressions[component]).add(token);
1387                         final Boolean include = action == '+' ? Boolean.TRUE : Boolean.FALSE;
1388                         if (includes[component] != null
1389                                 && !Objects.equals(includes[component], include)) {
1390                             throw new IllegalArgumentException(
1391                                     "Include (+) and exclude (-) rules both "
1392                                             + "specified for same component in '" + spec + "'");
1393                         }
1394                         includes[component] = include;
1395                     }
1396                 }
1397             }
1398 
1399             // Create ValueTransformers
1400             final ValueMatcher[] matchers = new ValueMatcher[4];
1401             for (int i = 0; i < 4; ++i) {
1402                 matchers[i] = expressions[i].isEmpty() ? null : new ValueMatcher(
1403                         (List<String>) expressions[i], Boolean.TRUE.equals(includes[i]));
1404             }
1405             this.subjMatcher = matchers[0];
1406             this.predMatcher = matchers[1];
1407             this.objMatcher = matchers[2];
1408             this.ctxMatcher = matchers[3];
1409         }
1410 
1411         @Override
1412         public boolean test(final Statement stmt) {
1413             return (this.subjMatcher == null || this.subjMatcher.match(stmt.getSubject()))
1414                     && (this.predMatcher == null || this.predMatcher.match(stmt.getPredicate()))
1415                     && (this.objMatcher == null || this.objMatcher.match(stmt.getObject()))
1416                     && (this.ctxMatcher == null || this.ctxMatcher.match(stmt.getContext()));
1417         }
1418 
1419         private static final class ValueMatcher {
1420 
1421             private final boolean include;
1422 
1423             // for URIs
1424 
1425             private final boolean matchAnyURI;
1426 
1427             private final Set<String> matchedURINamespaces;
1428 
1429             private final Set<URI> matchedURIs;
1430 
1431             // for BNodes
1432 
1433             private final boolean matchAnyBNode;
1434 
1435             private final Set<BNode> matchedBNodes;
1436 
1437             // for Literals
1438 
1439             private final boolean matchAnyPlainLiteral;
1440 
1441             private final boolean matchAnyLangLiteral;
1442 
1443             private final boolean matchAnyTypedLiteral;
1444 
1445             private final Set<String> matchedLanguages;
1446 
1447             private final Set<URI> matchedDatatypeURIs;
1448 
1449             private final Set<String> matchedDatatypeNamespaces;
1450 
1451             private final Set<Literal> matchedLiterals;
1452 
1453             ValueMatcher(final Iterable<String> matchExpressions, final boolean include) {
1454 
1455                 this.include = include;
1456 
1457                 this.matchedURINamespaces = new HashSet<>();
1458                 this.matchedURIs = new HashSet<>();
1459                 this.matchedBNodes = new HashSet<>();
1460                 this.matchedLanguages = new HashSet<>();
1461                 this.matchedDatatypeURIs = new HashSet<>();
1462                 this.matchedDatatypeNamespaces = new HashSet<>();
1463                 this.matchedLiterals = new HashSet<>();
1464 
1465                 boolean matchAnyURI = false;
1466                 boolean matchAnyBNode = false;
1467                 boolean matchAnyPlainLiteral = false;
1468                 boolean matchAnyLangLiteral = false;
1469                 boolean matchAnyTypedLiteral = false;
1470 
1471                 for (final String expression : matchExpressions) {
1472                     if ("<*>".equals(expression)) {
1473                         matchAnyURI = true;
1474                     } else if ("_:*".equals(expression)) {
1475                         matchAnyBNode = true;
1476                     } else if ("*".equals(expression)) {
1477                         matchAnyPlainLiteral = true;
1478                     } else if ("*@*".equals(expression)) {
1479                         matchAnyLangLiteral = true;
1480                     } else if ("*^^*".equals(expression)) {
1481                         matchAnyTypedLiteral = true;
1482                     } else if (expression.startsWith("*@")) {
1483                         this.matchedLanguages.add(expression.substring(2));
1484                     } else if (expression.startsWith("*^^")) {
1485                         if (expression.endsWith(":*")) {
1486                             this.matchedDatatypeNamespaces.add(Namespaces.DEFAULT
1487                                     .uriFor(expression.substring(3, expression.length() - 2)));
1488                         } else {
1489                             this.matchedDatatypeURIs.add((URI) Statements.parseValue(
1490                                     expression.substring(3), Namespaces.DEFAULT));
1491                         }
1492                     } else if (expression.endsWith(":*")) {
1493                         this.matchedURINamespaces.add(Namespaces.DEFAULT.uriFor(expression
1494                                 .substring(0, expression.length() - 2)));
1495 
1496                     } else if (expression.endsWith("*>")) {
1497                         this.matchedURINamespaces.add(expression.substring(1,
1498                                 expression.length() - 2));
1499                     } else {
1500                         final Value value = Statements.parseValue(expression, Namespaces.DEFAULT);
1501                         if (value instanceof URI) {
1502                             this.matchedURIs.add((URI) value);
1503                         } else if (value instanceof BNode) {
1504                             this.matchedBNodes.add((BNode) value);
1505                         } else if (value instanceof Literal) {
1506                             this.matchedLiterals.add((Literal) value);
1507                         }
1508 
1509                     }
1510                 }
1511 
1512                 this.matchAnyURI = matchAnyURI;
1513                 this.matchAnyBNode = matchAnyBNode;
1514                 this.matchAnyPlainLiteral = matchAnyPlainLiteral;
1515                 this.matchAnyLangLiteral = matchAnyLangLiteral;
1516                 this.matchAnyTypedLiteral = matchAnyTypedLiteral;
1517             }
1518 
1519             boolean match(final Value value) {
1520                 final boolean matched = matchHelper(value);
1521                 return this.include == matched;
1522             }
1523 
1524             private boolean matchHelper(final Value value) {
1525                 if (value instanceof URI) {
1526                     return this.matchAnyURI //
1527                             || contains(this.matchedURIs, value)
1528                             || containsNs(this.matchedURINamespaces, (URI) value);
1529                 } else if (value instanceof Literal) {
1530                     final Literal lit = (Literal) value;
1531                     final String lang = lit.getLanguage();
1532                     final URI dt = lit.getDatatype();
1533                     return lang == null
1534                             && (dt == null || XMLSchema.STRING.equals(dt))
1535                             && this.matchAnyPlainLiteral //
1536                             || lang != null //
1537                             && (this.matchAnyLangLiteral || contains(this.matchedLanguages, lang)) //
1538                             || dt != null //
1539                             && (this.matchAnyTypedLiteral
1540                                     || contains(this.matchedDatatypeURIs, dt) || containsNs(
1541                                         this.matchedDatatypeNamespaces, dt)) //
1542                             || contains(this.matchedLiterals, lit);
1543                 } else {
1544                     return this.matchAnyBNode //
1545                             || contains(this.matchedBNodes, value);
1546                 }
1547             }
1548 
1549             private static boolean contains(final Set<?> set, final Object value) {
1550                 return !set.isEmpty() && set.contains(value);
1551             }
1552 
1553             private static boolean containsNs(final Set<String> set, final URI uri) {
1554                 if (set.isEmpty()) {
1555                     return false;
1556                 }
1557                 if (set.contains(uri.getNamespace())) {
1558                     return true; // exact lookup
1559                 }
1560                 final String uriString = uri.stringValue();
1561                 for (final String elem : set) {
1562                     if (uriString.startsWith(elem)) {
1563                         return true; // prefix match
1564                     }
1565                 }
1566                 return false;
1567             }
1568 
1569         }
1570 
1571     }
1572 
1573 }