1   package eu.fbk.knowledgestore.data;
2   
3   import com.google.common.base.*;
4   import com.google.common.base.Objects;
5   import com.google.common.collect.*;
6   import com.google.common.hash.Hasher;
7   import com.google.common.hash.Hashing;
8   import com.google.common.io.Resources;
9   import com.google.common.primitives.*;
10  import com.google.common.util.concurrent.ListeningScheduledExecutorService;
11  import eu.fbk.knowledgestore.internal.Util;
12  import eu.fbk.knowledgestore.internal.rdf.CompactValueFactory;
13  import eu.fbk.rdfpro.util.Namespaces;
14  import org.openrdf.model.*;
15  import org.openrdf.model.impl.ValueFactoryImpl;
16  import org.openrdf.model.vocabulary.XMLSchema;
17  
18  import javax.annotation.Nullable;
19  import javax.xml.datatype.DatatypeConstants;
20  import javax.xml.datatype.DatatypeFactory;
21  import javax.xml.datatype.XMLGregorianCalendar;
22  import java.io.File;
23  import java.lang.reflect.Array;
24  import java.math.BigDecimal;
25  import java.math.BigInteger;
26  import java.net.URL;
27  import java.nio.charset.Charset;
28  import java.util.*;
29  import java.util.concurrent.ScheduledExecutorService;
30  import java.util.concurrent.atomic.AtomicBoolean;
31  import java.util.concurrent.atomic.AtomicInteger;
32  import java.util.concurrent.atomic.AtomicLong;
33  
34  // TODO: RDF conversion
35  // TODO: bytes
36  // TODO: getterOf, getterOfUnique, converterTo
37  
38  /**
39   * Helper services for working with KnowledgeStore data.
40   * <p>
41   * This class provides a number of services for working with KnowledgeStore data, i.e., with
42   * {@link Representation}, {@link Value}, {@link Statement}, {@link Record} instances and
43   * instances of scalar types that can be converted to {@code Literal} values. The following
44   * services are offered:
45   * </p>
46   * <ul>
47   * <li><b>MIME type registry</b>. Method {@link #extensionToMimeType(String)} and
48   * {@link #mimeTypeToExtensions(String)} allow to map from a file extension to the corresponding
49   * MIME type and the other way round based on an internal database; in addition, method
50   * {@link #isMimeTypeCompressible(String)} checks whether data of a MIME type can be effectively
51   * compressed. MIME types and associated extensions have been imported from the Apache Web server
52   * media type database (<a
53   * href="http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?view=markup"
54   * >http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?view=markup</a>). A list
55   * of non-compressed media types have been obtained from here: <a href=
56   * "http://pic.dhe.ibm.com/infocenter/storwize/unified_ic/index.jsp?topic=
57   * %2Fcom.ibm.storwize.v7000.unified.140.doc%2Fifs_plancompdata.html"
58   * >http://pic.dhe.ibm.com/infocenter/storwize/unified_ic/index.jsp?topic=%2Fcom.ibm.storwize.
59   * v7000.unified.140.doc%2Fifs_plancompdata.html</a>. Some manual modification to the data has
60   * been done as well, with to goal to enforce that a file association is mapped to at most a media
61   * type (a few entries from the Apache DB had to be changed).</li>
62   * <li><b>Factories for value objects</b>. Method {@link #getValueFactory()} returns a singleton,
63   * memory-optimized {@code ValueFactory} for creating {@code Statement}s and {@code Value}s (
64   * {@code URI}s, {@code BNode}s, {@code Literal}s); method {@link #getDatatypeFactory()} returns a
65   * singleton factory for creating instances of XML Schema structured types, such as
66   * {@link XMLGregorianCalendar} instances.</li>
67   * <li><b>Total and partial ordering</b>. Methods {@link #getTotalComparator()} and
68   * {@link #getPartialComparator()} returns two general-purpose comparators that impose,
69   * respectively, a total and a partial order over objects of the data model. The total order allow
70   * comparing any pair of {@code Value}s, {@code Statement}s, {@code Record}s and scalars
71   * convertible to literal values; resorting to type comparison if the denoted value belong to
72   * incomparable domains. The partial order compares only instances of compatible types (e.g.,
73   * numbers with numbers), and can thus be better suited for the evaluation of conditions (while
74   * the total order can help with the presentation of data).</li>
75   * <li><b>Prefix-to-namespace mappings management support</b>. Method {@link #getNamespaceMap()}
76   * returns a prefix-to-namespace map with common bindings as defined on the {@code prefix.cc} site
77   * (as of end 2013); it can be used to achieve a reasonable presentation of URIs, e.g., for RDF
78   * serialization. Methods {@link #newNamespaceMap()} and {@link #newNamespaceMap(Map, Map)} create
79   * either an empty, optimized prefix-to-namespace map or a combined prefix-to-namespace map that
80   * merges definitions of two input maps. Method {@link #namespaceToPrefix(String, Map)} performs a
81   * reverse lookup in a prefix-to-namespace map, efficiently returning the prefix for a given
82   * namespace.</li>
83   * <li><b>General-purpose conversion</b>. Methods {@link #convert(Object, Class)} and
84   * {@link #convert(Object, Class, Object)} attempts conversion from a data model object to another
85   * data model class or scalar class compatible convertible to a literal object. More details about
86   * the conversion are specified in the associated Javadoc documentation.</li>
87   * <li><b>Value normalization</b>. Methods {@link #normalize(Object)} and
88   * {@link #normalize(Object, Collection)} accepts any object of a data model class or of a scalar
89   * class convertible to a scalar literal (e.g., an integer) and normalize it to an instance of the
90   * three data model classes: {@code Value}, {@code Statement} and {@code Record}.</li>
91   * <li><b>String rendering and parsing</b>. Methods {@link #toString(Object, Map)} and
92   * {@link #toString(Object, Map, boolean)} generate a string representation of any data model
93   * object, using the supplied prefix-to-namespace mappings and possibly including a full listing
94   * of record properties. Method {@link #parseValue(String, Map)} allow parsing a {@code Value} out
95   * of a Turtle / TriG string, such as the ones produced by {@code toString(...)} methods.</li>
96   * </ul>
97   *
98   * <p style="color:red">
99   * TODO: this class needs urgent refactoring
100  * </p>
101  */
102 public final class Data {
103 
104     private static final Ordering<Object> TOTAL_ORDERING = new TotalOrdering();
105 
106     private static final Comparator<Object> PARTIAL_ORDERING = new PartialOrdering(TOTAL_ORDERING);
107 
108     private static final Map<String, String> COMMON_NAMESPACES = Namespaces.DEFAULT.uriMap();
109 
110     private static final Map<String, String> COMMON_PREFIXES = Namespaces.DEFAULT.prefixMap();
111 
112     private static final Set<String> UNCOMPRESSIBLE_MIME_TYPES;
113 
114     private static final Map<String, String> EXTENSIONS_TO_MIME_TYPES;
115 
116     private static final Map<String, List<String>> MIME_TYPES_TO_EXTENSIONS;
117 
118     private static final Map<String, URI> LANGUAGE_CODES_TO_URIS;
119 
120     private static final Map<URI, String> LANGUAGE_URIS_TO_CODES;
121 
122     private static final DatatypeFactory DATATYPE_FACTORY;
123 
124     private static final ValueFactory VALUE_FACTORY;
125 
126     private static ListeningScheduledExecutorService executor;
127 
128     private static AtomicBoolean executorPrivate = new AtomicBoolean();
129 
130     static {
131         VALUE_FACTORY = CompactValueFactory.getInstance();
132         try {
133             DATATYPE_FACTORY = DatatypeFactory.newInstance();
134         } catch (final Throwable ex) {
135             throw new Error("Unexpected exception (!): " + ex.getMessage(), ex);
136         }
137         final Map<String, URI> codesToURIs = Maps.newHashMap();
138         final Map<URI, String> urisToCodes = Maps.newHashMap();
139         for (final String language : Locale.getISOLanguages()) {
140             final Locale locale = new Locale(language);
141             final URI uri = Data.getValueFactory().createURI("http://lexvo.org/id/iso639-3/",
142                     locale.getISO3Language());
143             codesToURIs.put(language, uri);
144             urisToCodes.put(uri, language);
145         }
146         LANGUAGE_CODES_TO_URIS = ImmutableMap.copyOf(codesToURIs);
147         LANGUAGE_URIS_TO_CODES = ImmutableMap.copyOf(urisToCodes);
148     }
149 
150     static {
151         try {
152             final ImmutableSet.Builder<String> uncompressibleMtsBuilder = ImmutableSet.builder();
153             final ImmutableMap.Builder<String, String> extToMtIndexBuilder = ImmutableMap
154                     .builder();
155             final ImmutableMap.Builder<String, List<String>> mtToExtsIndexBuilder = ImmutableMap
156                     .builder();
157 
158             final URL resource = Data.class.getResource("mimetypes");
159             for (final String line : Resources.readLines(resource, Charsets.UTF_8)) {
160                 if (!line.isEmpty() && line.charAt(0) != '#') {
161                     final Iterator<String> iterator = Splitter.on(' ').trimResults()
162                             .omitEmptyStrings().split(line).iterator();
163                     final String mimeType = iterator.next();
164                     final ImmutableList.Builder<String> extBuilder = ImmutableList.builder();
165                     while (iterator.hasNext()) {
166                         final String token = iterator.next();
167                         if ("*".equals(token)) {
168                             uncompressibleMtsBuilder.add(mimeType);
169                         } else {
170                             extBuilder.add(token);
171                             extToMtIndexBuilder.put(token, mimeType);
172                         }
173                     }
174                     mtToExtsIndexBuilder.put(mimeType, extBuilder.build());
175                 }
176             }
177 
178             UNCOMPRESSIBLE_MIME_TYPES = uncompressibleMtsBuilder.build();
179             EXTENSIONS_TO_MIME_TYPES = extToMtIndexBuilder.build();
180             MIME_TYPES_TO_EXTENSIONS = mtToExtsIndexBuilder.build();
181 
182         } catch (final Throwable ex) {
183             throw new Error("Unexpected exception (!): " + ex.getMessage(), ex);
184         }
185     }
186 
187     /**
188      * Returns the executor shared by KnowledgeStore components. If no executor is setup using
189      * {@link #setExecutor(ScheduledExecutorService)}, an executor is automatically created using
190      * the thread number and naming given by system properties
191      * {@code eu.fbk.knowledgestore.threadCount} and {@code eu.fbk.knowledgestore.threadNumber}.
192      *
193      * @return the shared executor
194      */
195     public static ListeningScheduledExecutorService getExecutor() {
196         synchronized (executorPrivate) {
197             if (executor == null) {
198                 final String threadName = MoreObjects.firstNonNull(
199                         System.getProperty("eu.fbk.knowledgestore.threadName"), "worker-%02d");
200                 int threadCount = 32;
201                 try {
202                     threadCount = Integer.parseInt(System
203                             .getProperty("eu.fbk.knowledgestore.threadCount"));
204                 } catch (final Throwable ex) {
205                     // ignore
206                 }
207                 executor = Util.newScheduler(threadCount, threadName, true);
208                 executorPrivate.set(true);
209             }
210             return executor;
211         }
212     }
213 
214     /**
215      * Setup the executor shared by KnowledgeStore components. If another executor was previously
216      * in use, it will not be used anymore; in case it was the executor automatically created by
217      * the system, it will be shutdown.
218      *
219      * @param newExecutor
220      *            the new executor
221      */
222     public static void setExecutor(final ScheduledExecutorService newExecutor) {
223         Preconditions.checkNotNull(newExecutor);
224         ScheduledExecutorService executorToShutdown = null;
225         synchronized (executorPrivate) {
226             if (executor != null && executorPrivate.get()) {
227                 executorToShutdown = executor;
228             }
229             executor = Util.decorate(newExecutor);
230             executorPrivate.set(false);
231         }
232         if (executorToShutdown != null) {
233             executorToShutdown.shutdown();
234         }
235     }
236 
237     /**
238      * Returns an optimized {@code ValueFactory} for creating RDF {@code URI}s, {@code BNode}s,
239      * {@code Literal}s and {@code Statement}s. Note that while any {@code ValueFactory} can be
240      * used for this purpose (including the {@link ValueFactoryImpl} shipped with Sesame), the
241      * factory returned by this method has been optimized to create objects that minimize the use
242      * of memory, thus allowing to keep more objects / records in memory.
243      *
244      * @return a singleton {@code ValueFactory}
245      */
246     public static ValueFactory getValueFactory() {
247         return VALUE_FACTORY;
248     }
249 
250     /**
251      * Returns a {@code DatatypeFactory} for creating {@code XMLGregorianCalendar} instances and
252      * instances of other XML schema structured types.
253      *
254      * @return a singleton {@code DatatypeFactory}
255      */
256     public static DatatypeFactory getDatatypeFactory() {
257         return DATATYPE_FACTORY;
258     }
259 
260     /**
261      * Returns a comparator imposing a total order over objects of the data model ({@code Value}s,
262      * {@code Statement}s, {@code Record}s). Objects are organized in groups: booleans, strings,
263      * numbers (longs, ints, ...), calendar dates, URIs, BNodes and records. Ordering is done
264      * first based on the group. Then, among a group the ordering is the natural one defined for
265      * its elements, where applicable (booleans, strings, numbers, dates), otherwise the following
266      * extensions are adopted: (i) statements are sorted by context first, followed by subject,
267      * predicate and object; (ii) identifiers are sorted based on their string representation.
268      * Note that comparison of dates follows the XML Schema specification with the only exception
269      * that incomparable dates according to this specification (due to unknown timezone) are
270      * considered equal.
271      *
272      * @return a singleton comparator imposing a total order over objects of the data model
273      */
274     public static Comparator<Object> getTotalComparator() {
275         return TOTAL_ORDERING;
276     }
277 
278     /**
279      * Returns a comparator imposing a partial order over objects of the data model ({@code Value}
280      * s, {@code Statement}s, {@code Record}s). This comparator behaves like the one of
281      * {@link #getTotalComparator()} but in case objects belong to different groups (e.g., an
282      * integer and a string) an exception is thrown rather than applying group sorting, thus
283      * resulting in a more strict comparison that may be useful, e.g., for checking conditions.
284      *
285      * @return a singleton comparator imposing a partial order over objects of the data model
286      */
287     public static Comparator<Object> getPartialComparator() {
288         return PARTIAL_ORDERING;
289     }
290 
291     /**
292      * Returns a map of common prefix-to-namespace mappings, taken from a {@code prefix.cc} dump.
293      * The returned map provides multiple prefixes for some namespace; it also support fast
294      * reverse prefix lookup via {@link #namespaceToPrefix(String, Map)}.
295      *
296      * @return a singleton map of common prefix-to-namespace mappings
297      */
298     public static Map<String, String> getNamespaceMap() {
299         return COMMON_NAMESPACES;
300     }
301 
302     /**
303      * Creates a new, empty prefix-to-namespace map that supports fast reverse prefix lookup via
304      * {@link #namespaceToPrefix(String, Map)}.
305      *
306      * @return the created prefix-to-namespace map, empty
307      */
308     public static Map<String, String> newNamespaceMap() {
309         return new NamespaceMap();
310     }
311 
312     /**
313      * Creates a new prefix-to-namespace map combining the mappings in the supplied maps. Mappings
314      * in the {@code primaryNamespaceMap} take precedence, while the {@code secondaryNamespaceMap}
315      * is accessed only if a mapping is not found in the primary map. Modification operations
316      * target exclusively the {@code primaryNamespaceMap}.
317      *
318      * @param primaryNamespaceMap
319      *            the primary prefix-to-namespace map, not null
320      * @param secondaryNamespaceMap
321      *            the secondary prefix-to-namespace map, not null
322      * @return the created, combined prefix-to-namespace map
323      */
324     public static Map<String, String> newNamespaceMap(
325             final Map<String, String> primaryNamespaceMap,
326             final Map<String, String> secondaryNamespaceMap) {
327 
328         Preconditions.checkNotNull(primaryNamespaceMap);
329         Preconditions.checkNotNull(secondaryNamespaceMap);
330 
331         if (primaryNamespaceMap == secondaryNamespaceMap) {
332             return primaryNamespaceMap;
333         } else {
334             return new NamespaceCombinedMap(primaryNamespaceMap, secondaryNamespaceMap);
335         }
336     }
337 
338     /**
339      * Performs a reverse lookup of the prefix corresponding to a namespace in a
340      * prefix-to-namespace map. This method tries a number of strategy for efficiently performing
341      * the reverse lookup, expliting features of the particular prefix-to-namespace map supplied
342      * (e.g., whether it is a {@code BiMap} from Guava, a namespace map from
343      * {@link #newNamespaceMap()} or the map of common prefix-to-namespace declarations); as a
344      * result, calling this method may be significantly faster than manually looping over all the
345      * prefix-to-namespace entries.
346      *
347      * @param namespace
348      *            the namespace the corresponding prefix should be looked up
349      * @param namespaceMap
350      *            the prefix-to-namespace map containing the searched mapping
351      * @return the prefix corresponding to the namespace, or null if no mapping is defined
352      */
353     @Nullable
354     public static String namespaceToPrefix(final String namespace,
355             final Map<String, String> namespaceMap) {
356 
357         Preconditions.checkNotNull(namespace);
358 
359         if (namespaceMap == COMMON_NAMESPACES) {
360             return COMMON_PREFIXES.get(namespace);
361 
362         } else if (namespaceMap instanceof NamespaceCombinedMap) {
363             final NamespaceCombinedMap map = (NamespaceCombinedMap) namespaceMap;
364             String prefix = namespaceToPrefix(namespace, map.primaryNamespaces);
365             if (prefix == null) {
366                 prefix = namespaceToPrefix(namespace, map.secondaryNamespaces);
367             }
368             return prefix;
369 
370         } else if (namespaceMap instanceof NamespaceMap) {
371             return ((NamespaceMap) namespaceMap).getPrefix(namespace);
372 
373         } else if (namespaceMap instanceof BiMap) {
374             return ((BiMap<String, String>) namespaceMap).inverse().get(namespace);
375 
376         } else {
377             Preconditions.checkNotNull(namespaceMap);
378             for (final Map.Entry<String, String> entry : namespaceMap.entrySet()) {
379                 if (entry.getValue().equals(namespace)) {
380                     return entry.getKey();
381                 }
382             }
383             return null;
384         }
385     }
386 
387     /**
388      * Checks whether the specific MIME type can be compressed.
389      *
390      * @param mimeType
391      *            the MIME type
392      * @return true if compression can reduce size of data belonging to the specified MIME type,
393      *         or if there is no knowledge about compressibility of the specified MIME type; false
394      *         if it is known that compression cannot help (e.g., because the media type is
395      *         already compressed).
396      */
397     public static boolean isMimeTypeCompressible(final String mimeType) {
398         Preconditions.checkNotNull(mimeType);
399         final int index = mimeType.indexOf(';');
400         final String key = (index < 0 ? mimeType : mimeType.substring(0, index)).toLowerCase();
401         return !UNCOMPRESSIBLE_MIME_TYPES.contains(key);
402     }
403 
404     /**
405      * Returns the MIME type for the file extension specified, if known. If the parameter contains
406      * a full file name, its extension is extracted and used for the lookup.
407      *
408      * @param fileNameOrExtension
409      *            the file extension or file name (from which the extension is extracted)
410      * @return the corresponding MIME type; null if the file extension specified is not contained
411      *         in the internal database
412      */
413     public static String extensionToMimeType(final String fileNameOrExtension) {
414         Preconditions.checkNotNull(fileNameOrExtension);
415         final int index = fileNameOrExtension.lastIndexOf('.');
416         final String extension = index < 0 ? fileNameOrExtension : fileNameOrExtension
417                 .substring(index + 1);
418         return EXTENSIONS_TO_MIME_TYPES.get(extension);
419     }
420 
421     /**
422      * Returns the file extensions commonly associated to the specified MIME type. Extensions are
423      * reported without a leading {@code '.'} (e.g., {@code txt}). In case multiple extensions are
424      * returned, it is safe to consider the first one as the most common and preferred.
425      *
426      * @param mimeType
427      *            the MIME type
428      * @return a list with the extensions mapped to the MIME type, if known; an empty list
429      *         otherwise
430      */
431     public static List<String> mimeTypeToExtensions(final String mimeType) {
432         Preconditions.checkNotNull(mimeType);
433         final int index = mimeType.indexOf(';');
434         final String key = (index < 0 ? mimeType : mimeType.substring(0, index)).toLowerCase();
435         final List<String> result = MIME_TYPES_TO_EXTENSIONS.get(key);
436         return result != null ? result : ImmutableList.<String>of();
437     }
438 
439     /**
440      * Returns the language URI for the ISO 639 code (2-letters, 3-letters) specified. The
441      * returned URI is in the form {@code http://lexvo.org/id/iso639-3/XYZ}.
442      *
443      * @param code
444      *            the ISO 639 language code, possibly null
445      * @return the corresponding URI, or null if the input is null
446      * @throws IllegalArgumentException
447      *             in case the supplied string is not a valid ISO 639 2-letters or 3-letters code
448      */
449     @Nullable
450     public static URI languageCodeToURI(@Nullable final String code)
451             throws IllegalArgumentException {
452         if (code == null) {
453             return null;
454         }
455         final int length = code.length();
456         if (length == 2) {
457             final URI uri = LANGUAGE_CODES_TO_URIS.get(code);
458             if (uri != null) {
459                 return uri;
460             }
461         } else if (length == 3) {
462             final URI uri = Data.getValueFactory().createURI(
463                     "http://lexvo.org/id/iso639-3/" + code);
464             if (LANGUAGE_URIS_TO_CODES.containsKey(uri)) {
465                 return uri;
466             }
467         }
468         throw new IllegalArgumentException("Invalid language code: " + code);
469     }
470 
471     /**
472      * Returns the 2-letter ISO 639 code for the language URI supplied. The URI must be in the
473      * form {@code http://lexvo.org/id/iso639-3/XYZ}.
474      *
475      * @param uri
476      *            the language URI, possibly null
477      * @return the corresponding ISO 639 2-letter code, or null if the input URI is null
478      * @throws IllegalArgumentException
479      *             if the supplied URI is not valid
480      */
481     @Nullable
482     public static String languageURIToCode(@Nullable final URI uri)
483             throws IllegalArgumentException {
484         if (uri == null) {
485             return null;
486         }
487         final String code = LANGUAGE_URIS_TO_CODES.get(uri);
488         if (code != null) {
489             return code;
490         }
491         throw new IllegalArgumentException("Invalid language URI: " + uri);
492     }
493 
494     /**
495      * Utility method to compute an hash string from a vararg list of objects. The returned string
496      * is 16 characters long, starts with {@code A-Za-z} and contains only characters
497      * {@code A-Za-z0-9}.
498      *
499      * @param objects
500      *            the objects to compute the hash from
501      * @return the computed hash string
502      */
503     public static String hash(final Object... objects) {
504         final Hasher hasher = Hashing.md5().newHasher();
505         for (final Object object : objects) {
506             if (object instanceof CharSequence) {
507                 hasher.putString((CharSequence) object, Charsets.UTF_16LE);
508             } else if (object instanceof byte[]) {
509                 hasher.putBytes((byte[]) object);
510             } else if (object instanceof Character) {
511                 hasher.putChar((Character) object);
512             } else if (object instanceof Boolean) {
513                 hasher.putBoolean((Boolean) object);
514             } else if (object instanceof Integer) {
515                 hasher.putInt(((Integer) object).intValue());
516             } else if (object instanceof Long) {
517                 hasher.putLong(((Long) object).longValue());
518             } else if (object instanceof Double) {
519                 hasher.putDouble(((Double) object).doubleValue());
520             } else if (object instanceof Float) {
521                 hasher.putFloat(((Float) object).floatValue());
522             } else if (object instanceof Byte) {
523                 hasher.putByte(((Byte) object).byteValue());
524             } else {
525                 hasher.putString(object.toString(), Charsets.UTF_16LE);
526             }
527         }
528         final byte[] bytes = hasher.hash().asBytes();
529         final StringBuilder builder = new StringBuilder(16);
530         int max = 52;
531         for (int i = 0; i < bytes.length; ++i) {
532             final int n = (bytes[i] & 0x7F) % max;
533             if (n < 26) {
534                 builder.append((char) (65 + n));
535             } else if (n < 52) {
536                 builder.append((char) (71 + n));
537             } else {
538                 builder.append((char) (n - 4));
539             }
540             max = 62;
541         }
542         return builder.toString();
543     }
544 
545     /**
546      * General conversion facility. This method attempts to convert a supplied {@code object} to
547      * an instance of the class specified. If the input is null, null is returned. If conversion
548      * is unsupported or fails, an exception is thrown. The following table lists the supported
549      * conversions: <blockquote>
550      * <table border="1">
551      * <thead>
552      * <tr>
553      * <th>From classes (and sub-classes)</th>
554      * <th>To classes (and super-classes)</th>
555      * </tr>
556      * </thead><tbody>
557      * <tr>
558      * <td>{@link Boolean}, {@link Literal} ({@code xsd:boolean})</td>
559      * <td>{@link Boolean}, {@link Literal} ({@code xsd:boolean}), {@link String}</td>
560      * </tr>
561      * <tr>
562      * <td>{@link String}, {@link Literal} (plain, {@code xsd:string})</td>
563      * <td>{@link String}, {@link Literal} (plain, {@code xsd:string}), {@code URI} (as uri
564      * string), {@code BNode} (as BNode ID), {@link Integer}, {@link Long}, {@link Double},
565      * {@link Float}, {@link Short}, {@link Byte}, {@link BigDecimal}, {@link BigInteger},
566      * {@link AtomicInteger}, {@link AtomicLong}, {@link Boolean}, {@link XMLGregorianCalendar},
567      * {@link GregorianCalendar}, {@link Date} (via parsing), {@link Character} (length >= 1)</td>
568      * </tr>
569      * <tr>
570      * <td>{@link Number}, {@link Literal} (any numeric {@code xsd:} type)</td>
571      * <td>{@link Literal} (top-level numeric {@code xsd:} type), {@link Integer}, {@link Long},
572      * {@link Double}, {@link Float}, {@link Short}, {@link Byte}, {@link BigDecimal},
573      * {@link BigInteger}, {@link AtomicInteger}, {@link AtomicLong}, {@link String}</td>
574      * </tr>
575      * <tr>
576      * <td>{@link Date}, {@link GregorianCalendar}, {@link XMLGregorianCalendar}, {@link Literal}
577      * ({@code xsd:dateTime}, {@code xsd:date})</td>
578      * <td>{@link Date}, {@link GregorianCalendar}, {@link XMLGregorianCalendar}, {@link Literal}
579      * ({@code xsd:dateTime}), {@link String}</td>
580      * </tr>
581      * <tr>
582      * <td>{@link URI}</td>
583      * <td>{@link URI}, {@link Record} (ID assigned), {@link String}</td>
584      * </tr>
585      * <tr>
586      * <td>{@link BNode}</td>
587      * <td>{@link BNode}, {@link URI} (skolemization), {@link String}</td>
588      * </tr>
589      * <tr>
590      * <td>{@link Statement}</td>
591      * <td>{@link Statement}, {@link String}</td>
592      * </tr>
593      * <tr>
594      * <td>{@link Record}</td>
595      * <td>{@link Record}, {@link URI} (ID extracted), {@link String}</td>
596      * </tr>
597      * </tbody>
598      * </table>
599      * </blockquote>
600      *
601      * @param object
602      *            the object to convert, possibly null
603      * @param clazz
604      *            the class to convert to, not null
605      * @param <T>
606      *            the type of result
607      * @return the result of the conversion, or null if {@code object} was null
608      * @throws IllegalArgumentException
609      *             in case conversion fails or is unsupported for the {@code object} and class
610      *             specified
611      */
612     @SuppressWarnings("unchecked")
613     @Nullable
614     public static <T> T convert(@Nullable final Object object, final Class<T> clazz)
615             throws IllegalArgumentException {
616         if (object == null) {
617             Preconditions.checkNotNull(clazz);
618             return null;
619         }
620         if (clazz.isInstance(object)) {
621             return (T) object;
622         }
623         final T result = (T) convertObject(object, clazz);
624         if (result != null) {
625             return result;
626         }
627         throw new IllegalArgumentException("Unsupported conversion of " + object + " to " + clazz);
628     }
629 
630     /**
631      * General conversion facility, with fall back to default value. This method operates as
632      * {@link #convert(Object, Class)}, but in case the input is null or conversion is not
633      * supported returns the specified default value.
634      *
635      * @param object
636      *            the object to convert, possibly null
637      * @param clazz
638      *            the class to convert to, not null
639      * @param defaultValue
640      *            the default value to fall back to
641      * @param <T>
642      *            the type of result
643      * @return the result of the conversion, or the default value if {@code object} was null,
644      *         conversion failed or is unsupported
645      */
646     @SuppressWarnings("unchecked")
647     @Nullable
648     public static <T> T convert(@Nullable final Object object, final Class<T> clazz,
649             @Nullable final T defaultValue) {
650         if (object == null) {
651             Preconditions.checkNotNull(clazz);
652             return defaultValue;
653         }
654         if (clazz.isInstance(object)) {
655             return (T) object;
656         }
657         try {
658             final T result = (T) convertObject(object, clazz);
659             return result != null ? result : defaultValue;
660         } catch (final RuntimeException ex) {
661             return defaultValue;
662         }
663     }
664 
665     @Nullable
666     private static Object convertObject(final Object object, final Class<?> clazz) {
667         if (object instanceof Literal) {
668             return convertLiteral((Literal) object, clazz);
669         } else if (object instanceof URI) {
670             return convertURI((URI) object, clazz);
671         } else if (object instanceof String) {
672             return convertString((String) object, clazz);
673         } else if (object instanceof Number) {
674             return convertNumber((Number) object, clazz);
675         } else if (object instanceof Boolean) {
676             return convertBoolean((Boolean) object, clazz);
677         } else if (object instanceof XMLGregorianCalendar) {
678             return convertCalendar((XMLGregorianCalendar) object, clazz);
679         } else if (object instanceof BNode) {
680             return convertBNode((BNode) object, clazz);
681         } else if (object instanceof Statement) {
682             return convertStatement((Statement) object, clazz);
683         } else if (object instanceof Record) {
684             return convertRecord((Record) object, clazz);
685         } else if (object instanceof GregorianCalendar) {
686             final XMLGregorianCalendar calendar = getDatatypeFactory().newXMLGregorianCalendar(
687                     (GregorianCalendar) object);
688             return clazz == XMLGregorianCalendar.class ? calendar : convertCalendar(calendar,
689                     clazz);
690         } else if (object instanceof Date) {
691             final GregorianCalendar calendar = new GregorianCalendar();
692             calendar.setTime((Date) object);
693             final XMLGregorianCalendar xmlCalendar = getDatatypeFactory().newXMLGregorianCalendar(
694                     calendar);
695             return clazz == XMLGregorianCalendar.class ? xmlCalendar : convertCalendar(
696                     xmlCalendar, clazz);
697         } else if (object instanceof Enum<?>) {
698             return convertEnum((Enum<?>) object, clazz);
699         } else if (object instanceof File) {
700             return convertFile((File) object, clazz);
701         }
702         return null;
703     }
704 
705     @Nullable
706     private static Object convertStatement(final Statement statement, final Class<?> clazz) {
707         if (clazz.isAssignableFrom(String.class)) {
708             return statement.toString();
709         }
710         return null;
711     }
712 
713     @Nullable
714     private static Object convertLiteral(final Literal literal, final Class<?> clazz) {
715         final URI datatype = literal.getDatatype();
716         if (datatype == null || datatype.equals(XMLSchema.STRING)) {
717             return convertString(literal.getLabel(), clazz);
718         } else if (datatype.equals(XMLSchema.BOOLEAN)) {
719             return convertBoolean(literal.booleanValue(), clazz);
720         } else if (datatype.equals(XMLSchema.DATE) || datatype.equals(XMLSchema.DATETIME)) {
721             return convertCalendar(literal.calendarValue(), clazz);
722         } else if (datatype.equals(XMLSchema.INT)) {
723             return convertNumber(literal.intValue(), clazz);
724         } else if (datatype.equals(XMLSchema.LONG)) {
725             return convertNumber(literal.longValue(), clazz);
726         } else if (datatype.equals(XMLSchema.DOUBLE)) {
727             return convertNumber(literal.doubleValue(), clazz);
728         } else if (datatype.equals(XMLSchema.FLOAT)) {
729             return convertNumber(literal.floatValue(), clazz);
730         } else if (datatype.equals(XMLSchema.SHORT)) {
731             return convertNumber(literal.shortValue(), clazz);
732         } else if (datatype.equals(XMLSchema.BYTE)) {
733             return convertNumber(literal.byteValue(), clazz);
734         } else if (datatype.equals(XMLSchema.DECIMAL)) {
735             return convertNumber(literal.decimalValue(), clazz);
736         } else if (datatype.equals(XMLSchema.INTEGER)) {
737             return convertNumber(literal.integerValue(), clazz);
738         } else if (datatype.equals(XMLSchema.NON_NEGATIVE_INTEGER)
739                 || datatype.equals(XMLSchema.NON_POSITIVE_INTEGER)
740                 || datatype.equals(XMLSchema.NEGATIVE_INTEGER)
741                 || datatype.equals(XMLSchema.POSITIVE_INTEGER)) {
742             return convertNumber(literal.integerValue(), clazz); // infrequent integer cases
743         } else if (datatype.equals(XMLSchema.NORMALIZEDSTRING) || datatype.equals(XMLSchema.TOKEN)
744                 || datatype.equals(XMLSchema.NMTOKEN) || datatype.equals(XMLSchema.LANGUAGE)
745                 || datatype.equals(XMLSchema.NAME) || datatype.equals(XMLSchema.NCNAME)) {
746             return convertString(literal.getLabel(), clazz); // infrequent string cases
747         }
748         return null;
749     }
750 
751     @Nullable
752     private static Object convertBoolean(final Boolean bool, final Class<?> clazz) {
753         if (clazz == Boolean.class || clazz == boolean.class) {
754             return bool;
755         } else if (clazz.isAssignableFrom(Literal.class)) {
756             return getValueFactory().createLiteral(bool);
757         } else if (clazz.isAssignableFrom(String.class)) {
758             return bool.toString();
759         }
760         return null;
761     }
762 
763     @Nullable
764     private static Object convertString(final String string, final Class<?> clazz) {
765         if (clazz.isInstance(string)) {
766             return string;
767         } else if (clazz.isAssignableFrom(Literal.class)) {
768             return getValueFactory().createLiteral(string, XMLSchema.STRING);
769         } else if (clazz.isAssignableFrom(URI.class)) {
770             return getValueFactory().createURI(string);
771         } else if (clazz.isAssignableFrom(BNode.class)) {
772             return getValueFactory().createBNode(
773                     string.startsWith("_:") ? string.substring(2) : string);
774         } else if (clazz == Boolean.class || clazz == boolean.class) {
775             return Boolean.valueOf(string);
776         } else if (clazz == Integer.class || clazz == int.class) {
777             return Integer.valueOf(string);
778         } else if (clazz == Long.class || clazz == long.class) {
779             return Long.valueOf(string);
780         } else if (clazz == Double.class || clazz == double.class) {
781             return Double.valueOf(string);
782         } else if (clazz == Float.class || clazz == float.class) {
783             return Float.valueOf(string);
784         } else if (clazz == Short.class || clazz == short.class) {
785             return Short.valueOf(string);
786         } else if (clazz == Byte.class || clazz == byte.class) {
787             return Byte.valueOf(string);
788         } else if (clazz == BigDecimal.class) {
789             return new BigDecimal(string);
790         } else if (clazz == BigInteger.class) {
791             return new BigInteger(string);
792         } else if (clazz == AtomicInteger.class) {
793             return new AtomicInteger(Integer.parseInt(string));
794         } else if (clazz == AtomicLong.class) {
795             return new AtomicLong(Long.parseLong(string));
796         } else if (clazz == Date.class) {
797             final String fixed = string.contains("T") ? string : string + "T00:00:00";
798             return getDatatypeFactory().newXMLGregorianCalendar(fixed).toGregorianCalendar()
799                     .getTime();
800         } else if (clazz.isAssignableFrom(GregorianCalendar.class)) {
801             final String fixed = string.contains("T") ? string : string + "T00:00:00";
802             return getDatatypeFactory().newXMLGregorianCalendar(fixed).toGregorianCalendar();
803         } else if (clazz.isAssignableFrom(XMLGregorianCalendar.class)) {
804             final String fixed = string.contains("T") ? string : string + "T00:00:00";
805             return getDatatypeFactory().newXMLGregorianCalendar(fixed);
806         } else if (clazz == Character.class || clazz == char.class) {
807             return string.isEmpty() ? null : string.charAt(0);
808         } else if (clazz.isEnum()) {
809             for (final Object constant : clazz.getEnumConstants()) {
810                 if (string.equalsIgnoreCase(((Enum<?>) constant).name())) {
811                     return constant;
812                 }
813             }
814             throw new IllegalArgumentException("Illegal " + clazz.getSimpleName() + " constant: "
815                     + string);
816         } else if (clazz == File.class) {
817             return new File(string);
818         }
819         return null;
820     }
821 
822     @Nullable
823     private static Object convertNumber(final Number number, final Class<?> clazz) {
824         if (clazz.isAssignableFrom(Literal.class)) {
825             // TODO: perhaps datatype should be based on denoted value, rather than class (e.g.
826             // 3.0f -> 3 xsd:byte)
827             if (number instanceof Integer || number instanceof AtomicInteger) {
828                 return getValueFactory().createLiteral(number.intValue());
829             } else if (number instanceof Long || number instanceof AtomicLong) {
830                 return getValueFactory().createLiteral(number.longValue());
831             } else if (number instanceof Double) {
832                 return getValueFactory().createLiteral(number.doubleValue());
833             } else if (number instanceof Float) {
834                 return getValueFactory().createLiteral(number.floatValue());
835             } else if (number instanceof Short) {
836                 return getValueFactory().createLiteral(number.shortValue());
837             } else if (number instanceof Byte) {
838                 return getValueFactory().createLiteral(number.byteValue());
839             } else if (number instanceof BigDecimal) {
840                 return getValueFactory().createLiteral(number.toString(), XMLSchema.DECIMAL);
841             } else if (number instanceof BigInteger) {
842                 return getValueFactory().createLiteral(number.toString(), XMLSchema.INTEGER);
843             }
844         } else if (clazz.isAssignableFrom(String.class)) {
845             return number.toString();
846         } else if (clazz == Integer.class || clazz == int.class) {
847             return Integer.valueOf(number.intValue());
848         } else if (clazz == Long.class || clazz == long.class) {
849             return Long.valueOf(number.longValue());
850         } else if (clazz == Double.class || clazz == double.class) {
851             return Double.valueOf(number.doubleValue());
852         } else if (clazz == Float.class || clazz == float.class) {
853             return Float.valueOf(number.floatValue());
854         } else if (clazz == Short.class || clazz == short.class) {
855             return Short.valueOf(number.shortValue());
856         } else if (clazz == Byte.class || clazz == byte.class) {
857             return Byte.valueOf(number.byteValue());
858         } else if (clazz == BigDecimal.class) {
859             return toBigDecimal(number);
860         } else if (clazz == BigInteger.class) {
861             return toBigInteger(number);
862         } else if (clazz == AtomicInteger.class) {
863             return new AtomicInteger(number.intValue());
864         } else if (clazz == AtomicLong.class) {
865             return new AtomicLong(number.longValue());
866         }
867         return null;
868     }
869 
870     @Nullable
871     private static Object convertCalendar(final XMLGregorianCalendar calendar, //
872             final Class<?> clazz) {
873         if (clazz.isInstance(calendar)) {
874             return calendar;
875         } else if (clazz.isAssignableFrom(Literal.class)) {
876             return getValueFactory().createLiteral(calendar);
877         } else if (clazz.isAssignableFrom(String.class)) {
878             return calendar.toXMLFormat();
879         } else if (clazz == Date.class) {
880             return calendar.toGregorianCalendar().getTime();
881         } else if (clazz.isAssignableFrom(GregorianCalendar.class)) {
882             return calendar.toGregorianCalendar();
883         }
884         return null;
885     }
886 
887     @Nullable
888     private static Object convertURI(final URI uri, final Class<?> clazz) {
889         if (clazz.isInstance(uri)) {
890             return uri;
891         } else if (clazz.isAssignableFrom(String.class)) {
892             return uri.stringValue();
893         } else if (clazz == Record.class) {
894             return Record.create(uri);
895         }
896         return null;
897     }
898 
899     @Nullable
900     private static Object convertBNode(final BNode bnode, final Class<?> clazz) {
901         if (clazz.isInstance(bnode)) {
902             return bnode;
903         } else if (clazz.isAssignableFrom(URI.class)) {
904             return getValueFactory().createURI("bnode:" + bnode.getID());
905         } else if (clazz.isAssignableFrom(String.class)) {
906             return "_:" + bnode.getID();
907         }
908         return null;
909     }
910 
911     @Nullable
912     private static Object convertRecord(final Record record, final Class<?> clazz) {
913         if (clazz.isInstance(record)) {
914             return record;
915         } else if (clazz.isAssignableFrom(URI.class)) {
916             return record.getID();
917         } else if (clazz.isAssignableFrom(String.class)) {
918             return record.toString();
919         }
920         return null;
921     }
922 
923     @Nullable
924     private static Object convertEnum(final Enum<?> constant, final Class<?> clazz) {
925         if (clazz.isInstance(constant)) {
926             return constant;
927         } else if (clazz.isAssignableFrom(String.class)) {
928             return constant.name();
929         } else if (clazz.isAssignableFrom(Literal.class)) {
930             return getValueFactory().createLiteral(constant.name(), XMLSchema.STRING);
931         }
932         return null;
933     }
934 
935     @Nullable
936     private static Object convertFile(final File file, final Class<?> clazz) {
937         if (clazz.isInstance(file)) {
938             return clazz.cast(file);
939         } else if (clazz.isAssignableFrom(URI.class)) {
940             return VALUE_FACTORY.createURI("file://" + file.getAbsolutePath());
941         } else if (clazz.isAssignableFrom(String.class)) {
942             return file.getAbsolutePath();
943         }
944         return null;
945     }
946 
947     private static BigDecimal toBigDecimal(final Number number) {
948         if (number instanceof BigDecimal) {
949             return (BigDecimal) number;
950         } else if (number instanceof BigInteger) {
951             return new BigDecimal((BigInteger) number);
952         } else if (number instanceof Double || number instanceof Float) {
953             final double value = number.doubleValue();
954             return Double.isInfinite(value) || Double.isNaN(value) ? null : new BigDecimal(value);
955         } else {
956             return new BigDecimal(number.longValue());
957         }
958     }
959 
960     private static BigInteger toBigInteger(final Number number) {
961         if (number instanceof BigInteger) {
962             return (BigInteger) number;
963         } else if (number instanceof BigDecimal) {
964             return ((BigDecimal) number).toBigInteger();
965         } else if (number instanceof Double || number instanceof Float) {
966             return new BigDecimal(number.doubleValue()).toBigInteger();
967         } else {
968             return BigInteger.valueOf(number.longValue());
969         }
970     }
971 
972     /**
973      * Normalizes the supplied object to an object of the data model. The method operates as
974      * follows:
975      * <ul>
976      * <li>if the input is null, null is returned;</li>
977      * <li>if the input is already an object of the data model ({@link Record}, {@link Value},
978      * {@link Statement}), it is returned unchanged;</li>
979      * <li>if the input is an iterable or array, its unique element is converted if length is 1;
980      * null is returned if length is 0; {@link IllegalArgumentException} is thrown otherwise;</li>
981      * <li>in all the other cases, conversion to {@code Value} is performed.</li>
982      * </ul>
983      *
984      * @param object
985      *            the object to normalize, possibly an array or iterable
986      * @return the corresponding object of the data model
987      * @throws IllegalArgumentException
988      *             in case the supplied object is an array or iterable with more than one element,
989      *             or if conversion was required but failed or was unsupported
990      */
991     @Nullable
992     public static Object normalize(@Nullable final Object object) throws IllegalArgumentException {
993         if (object == null || object instanceof Record || object instanceof Value
994                 || object instanceof Statement) {
995             return object;
996         }
997         if (object.getClass().isArray()) {
998             final int length = Array.getLength(object);
999             if (length == 0) {
1000                 return null;
1001             }
1002             if (length == 1) {
1003                 return normalize(Array.get(object, 0));
1004             }
1005             throw new IllegalArgumentException(
1006                     "Cannot extract a unique node from array of length " + length);
1007         }
1008         if (object instanceof Iterable<?>) {
1009             Object result = null;
1010             for (final Object element : (Iterable<?>) object) {
1011                 if (result != null) {
1012                     throw new IllegalArgumentException(
1013                             "cannot extract a unique node from iterable " + object);
1014                 }
1015                 result = normalize(element);
1016             }
1017             return result;
1018         }
1019         return convert(object, Value.class);
1020     }
1021 
1022     /**
1023      * Normalizes the supplied object to zero or more objects of the data model, which are added
1024      * to the collection specified. The method operates as follows:
1025      * <ul>
1026      * <li>if the input is null, no nodes are produced and false is returned</li>
1027      * <li>if the input is already an object of the data model ({@link Record}, {@link Value},
1028      * {@link Statement}), it is stored unchanged in the supplied collection;</li>
1029      * <li>if the input is an iterable or array, its elements are converted recursively (i.e.,
1030      * {@code normalize()} is called for each of them, using the same collection supplied);</li>
1031      * <li>in all the other cases, conversion to {@code Value} is performed.</li>
1032      * </ul>
1033      *
1034      * @param object
1035      *            the object to normalize, possibly an array or iterable
1036      * @param collection
1037      *            a collection where to add the resulting nodes
1038      * @return true, if the collection changed as a result of the call
1039      * @throws IllegalArgumentException
1040      *             in case conversion was necessary but failed or was unsupported
1041      */
1042     public static boolean normalize(final Object object, final Collection<Object> collection)
1043             throws IllegalArgumentException {
1044         if (object == null) {
1045             return false;
1046         } else if (object.getClass().isArray()) {
1047             final int length = Array.getLength(object);
1048             if (length == 0) {
1049                 return false;
1050             } else if (length == 1) {
1051                 return normalize(Array.get(object, 0), collection);
1052             } else if (object instanceof Object[]) {
1053                 return normalize(Arrays.asList((Object[]) object), collection);
1054             } else if (object instanceof int[]) {
1055                 return normalize(Ints.asList((int[]) object), collection);
1056             } else if (object instanceof long[]) {
1057                 return normalize(Longs.asList((long[]) object), collection);
1058             } else if (object instanceof double[]) {
1059                 return normalize(Doubles.asList((double[]) object), collection);
1060             } else if (object instanceof float[]) {
1061                 return normalize(Floats.asList((float[]) object), collection);
1062             } else if (object instanceof short[]) {
1063                 return normalize(Shorts.asList((short[]) object), collection);
1064             } else if (object instanceof boolean[]) {
1065                 return normalize(Booleans.asList((boolean[]) object), collection);
1066             } else if (object instanceof char[]) {
1067                 return normalize(Chars.asList((char[]) object), collection);
1068             } else {
1069                 throw new IllegalArgumentException("Unsupported primitive array type: "
1070                         + object.getClass());
1071             }
1072         } else if (object instanceof Iterable<?>) {
1073             boolean changed = false;
1074             for (final Object element : (Iterable<?>) object) {
1075                 if (normalize(element, collection)) {
1076                     changed = true;
1077                 }
1078             }
1079             return changed;
1080         } else {
1081             return collection.add(normalize(object));
1082         }
1083     }
1084 
1085     /**
1086      * Check that the supplied string is a legal IRI (as per RFC 3987).
1087      *
1088      * @param string
1089      *            the IRI string to check
1090      * @throws IllegalArgumentException
1091      *             in case the IRI is illegal
1092      */
1093     public static void validateIRI(@Nullable final String string) throws IllegalArgumentException {
1094 
1095         // TODO: currently we check only the characters forming the IRI, not its structure
1096 
1097         // Ignore null input
1098         if (string == null) {
1099             return;
1100         }
1101 
1102         // Illegal characters should be percent encoded. Illegal IRI characters are all the
1103         // character that are not 'unreserved' (A-Z a-z 0-9 - . _ ~ 0xA0-0xD7FF 0xF900-0xFDCF
1104         // 0xFDF0-0xFFEF) or 'reserved' (! # $ % & ' ( ) * + , / : ; = ? @ [ ])
1105 
1106         for (int i = 0; i < string.length(); ++i) {
1107             final char c = string.charAt(i);
1108             if (c >= 'a' && c <= 'z' || c >= '?' && c <= '[' || c >= '&' && c <= ';' || c == '#'
1109                     || c == '$' || c == '!' || c == '=' || c == ']' || c == '_' || c == '~'
1110                     || c >= 0xA0 && c <= 0xD7FF || c >= 0xF900 && c <= 0xFDCF || c >= 0xFDF0
1111                     && c <= 0xFFEF) {
1112                 // character is OK
1113             } else if (c == '%') {
1114                 if (i >= string.length() - 2 || Character.digit(string.charAt(i + 1), 16) < 0
1115                         || Character.digit(string.charAt(i + 2), 16) < 0) {
1116                     throw new IllegalArgumentException("Illegal IRI '" + string
1117                             + "' (invalid percent encoding at index " + i + ")");
1118                 }
1119             } else {
1120                 throw new IllegalArgumentException("Illegal IRI '" + string
1121                         + "' (illegal character at index " + i + ")");
1122             }
1123         }
1124     }
1125 
1126     /**
1127      * Clean an illegal IRI string, trying to make it legal (as per RFC 3987).
1128      *
1129      * @param string
1130      *            the IRI string to clean
1131      * @return the cleaned IRI string (possibly the input unchanged) upon success
1132      * @throws IllegalArgumentException
1133      *             in case the supplied input cannot be transformed into a legal IRI
1134      */
1135     @Nullable
1136     public static String cleanIRI(@Nullable final String string) throws IllegalArgumentException {
1137 
1138         // TODO: we only replace illegal characters, but we should also check and fix the IRI
1139         // structure
1140 
1141         // We implement the cleaning suggestions provided at the following URL (section 'So what
1142         // exactly should I do?'), extended to deal with IRIs instead of URIs:
1143         // https://unspecified.wordpress.com/2012/02/12/how-do-you-escape-a-complete-uri/
1144 
1145         // Handle null input
1146         if (string == null) {
1147             return null;
1148         }
1149 
1150         // Illegal characters should be percent encoded. Illegal IRI characters are all the
1151         // character that are not 'unreserved' (A-Z a-z 0-9 - . _ ~ 0xA0-0xD7FF 0xF900-0xFDCF
1152         // 0xFDF0-0xFFEF) or 'reserved' (! # $ % & ' ( ) * + , / : ; = ? @ [ ])
1153         final StringBuilder builder = new StringBuilder();
1154         for (int i = 0; i < string.length(); ++i) {
1155             final char c = string.charAt(i);
1156             if (c >= 'a' && c <= 'z' || c >= '?' && c <= '[' || c >= '&' && c <= ';' || c == '#'
1157                     || c == '$' || c == '!' || c == '=' || c == ']' || c == '_' || c == '~'
1158                     || c >= 0xA0 && c <= 0xD7FF || c >= 0xF900 && c <= 0xFDCF || c >= 0xFDF0
1159                     && c <= 0xFFEF) {
1160                 builder.append(c);
1161             } else if (c == '%' && i < string.length() - 2
1162                     && Character.digit(string.charAt(i + 1), 16) >= 0
1163                     && Character.digit(string.charAt(i + 2), 16) >= 0) {
1164                 builder.append('%'); // preserve valid percent encodings
1165             } else {
1166                 builder.append('%').append(Character.forDigit(c / 16, 16))
1167                         .append(Character.forDigit(c % 16, 16));
1168             }
1169         }
1170 
1171         // Return the cleaned IRI (no Java validation as it is an IRI, not a URI)
1172         return builder.toString();
1173     }
1174 
1175     /**
1176      * Clean a possibly illegal URI string (in a way similar to what a browser does), returning
1177      * the corresponding cleaned {@code URI} object if successfull. A null result is returned for
1178      * a null input. Cleaning consists in (i) encode Unicode characters above U+0080 as UTF-8
1179      * octet sequences and (ii) percent-encode all resulting characters that are illegal as per
1180      * RFC 3986 (i.e., characters that are not 'reserved' or 'unreserved' according to the RFC).
1181      * Note that relative URIs are rejected by this method.
1182      *
1183      * @param string
1184      *            the input string
1185      * @return the resulting cleaned URI
1186      * @throws IllegalArgumentException
1187      *             if the supplied string (after being cleaned) is still not valid (e.g., it does
1188      *             not contain a valid URI scheme) or represent a relative URI
1189      */
1190     public static String cleanURI(final String string) throws IllegalArgumentException {
1191 
1192         // We implement the cleaning suggestions provided at the following URL (section 'So what
1193         // exactly should I do?'):
1194         // https://unspecified.wordpress.com/2012/02/12/how-do-you-escape-a-complete-uri/
1195 
1196         // Handle null input
1197         if (string == null) {
1198             return null;
1199         }
1200 
1201         // The input string should be first encoded as a sequence of UTF-8 bytes, so to deal with
1202         // Unicode chars properly (this encoding is a non-standard, common practice)
1203         final byte[] bytes = string.getBytes(Charset.forName("UTF-8"));
1204 
1205         // Then illegal characters should be percent encoded. Illegal characters are all the
1206         // character that are not 'unreserved' (A-Z a-z 0-9 - . _ ~) or 'reserved' (! # $ % & ' (
1207         // ) * + , / : ; = ? @ [ ])
1208         final StringBuilder builder = new StringBuilder();
1209         for (int i = 0; i < bytes.length; ++i) {
1210             final int b = bytes[i] & 0xFF; // transform from signed to unsigned
1211             if (b >= 'a' && b <= 'z' || b >= '?' && b <= '[' || b >= '&' && b <= ';' || b == '#'
1212                     || b == '$' || b == '!' || b == '=' || b == ']' || b == '_' || b == '~') {
1213                 builder.append((char) b);
1214             } else if (b == '%' && i < string.length() - 2
1215                     && Character.digit(string.charAt(i + 1), 16) >= 0
1216                     && Character.digit(string.charAt(i + 2), 16) >= 0) {
1217                 builder.append('%'); // preserve valid percent encodings
1218             } else {
1219                 builder.append('%').append(Character.forDigit(b / 16, 16))
1220                         .append(Character.forDigit(b % 16, 16));
1221             }
1222         }
1223 
1224         // Can now create an URI object, letting Java do further validation on the URI structure
1225         // (e.g., whether valid scheme, host, etc. have been provided)
1226         final java.net.URI uri = java.net.URI.create(builder.toString()).normalize();
1227 
1228         // We reject relative URIs, as they can cause problems downstream
1229         if (!uri.isAbsolute()) {
1230             throw new IllegalArgumentException("Not a valid absolute URI: " + uri);
1231         }
1232 
1233         // Can finally return the URI
1234         return uri.toString();
1235     }
1236 
1237     /**
1238      * Parses an RDF value out of a string. The string can be in the Turtle / N3 / TriG format,
1239      * i.e., {@code "literal", "literal"^^^datatype, "literal"@lang", <uri>, _:bnode} (strings
1240      * produced by {@link #toString(Object, Map, boolean)} obey this format).
1241      *
1242      * @param string
1243      *            the string to parse, possibly null
1244      * @param namespaces
1245      *            the optional prefix-to-namespace mappings to use for parsing the string,
1246      *            possibly null
1247      * @return the parsed value, or null if a null string was passed as input
1248      * @throws ParseException
1249      *             in case parsing fails
1250      */
1251     @Nullable
1252     public static Value parseValue(@Nullable final String string,
1253             @Nullable final Map<String, String> namespaces) throws ParseException {
1254 
1255         if (string == null) {
1256             return null;
1257         }
1258 
1259         try {
1260             final int length = string.length();
1261             if (string.startsWith("\"") || string.startsWith("'")) {
1262                 if (string.charAt(length - 1) == '"' || string.charAt(length - 1) == '\'') {
1263                     return getValueFactory().createLiteral(string.substring(1, length - 1));
1264                 }
1265                 int index = string.lastIndexOf("@");
1266                 if (index == length - 3) {
1267                     final String language = string.substring(index + 1);
1268                     if (Character.isLetter(language.charAt(0))
1269                             && Character.isLetter(language.charAt(1))) {
1270                         return getValueFactory().createLiteral(string.substring(1, index - 1),
1271                                 language);
1272                     }
1273                 }
1274                 index = string.lastIndexOf("^^");
1275                 if (index > 0) {
1276                     final String datatype = string.substring(index + 2);
1277                     try {
1278                         final URI datatypeURI = (URI) parseValue(datatype, namespaces);
1279                         return getValueFactory().createLiteral(string.substring(1, index - 1),
1280                                 datatypeURI);
1281                     } catch (final Throwable ex) {
1282                         // ignore
1283                     }
1284                 }
1285                 throw new ParseException(string, "Invalid literal");
1286 
1287             } else if (string.startsWith("_:")) {
1288                 return getValueFactory().createBNode(string.substring(2));
1289 
1290             } else if (string.startsWith("<")) {
1291                 return getValueFactory().createURI(string.substring(1, length - 1));
1292 
1293             } else if (namespaces != null) {
1294                 final int index = string.indexOf(':');
1295                 if (index >= 0) {
1296                     final String prefix = string.substring(0, index);
1297                     final String localName = string.substring(index + 1);
1298                     final String namespace = namespaces.get(prefix);
1299                     if (namespace != null) {
1300                         return getValueFactory().createURI(namespace, localName);
1301                     }
1302                 }
1303             }
1304             throw new ParseException(string, "Unparseable value");
1305 
1306         } catch (final RuntimeException ex) {
1307             throw ex instanceof ParseException ? (ParseException) ex : new ParseException(string,
1308                     ex.getMessage(), ex);
1309         }
1310     }
1311 
1312     /**
1313      * Returns a string representation of the supplied data model object, optionally using the
1314      * supplied namespaces and including record properties. Supported objects are {@link Value},
1315      * {@link Statement}, {@link Record} instances and instances of scalar types that can be
1316      * converted to {@code Value}s (via {@link #convert(Object, Class)}).
1317      *
1318      * @param object
1319      *            the data model object, possibly null
1320      * @param namespaces
1321      *            the optional prefix-to-namespace mappings to use for generating the string,
1322      *            possibly null
1323      * @param includeProperties
1324      *            true if record properties should be included in the resulting string, in
1325      *            addition to the record ID
1326      * @return the produced string, or null if a null object was passed as input
1327      */
1328     @Nullable
1329     public static String toString(@Nullable final Object object,
1330             @Nullable final Map<String, String> namespaces, final boolean includeProperties) {
1331 
1332         if (object instanceof Record) {
1333             return ((Record) object).toString(namespaces, includeProperties);
1334 
1335         } else if (object instanceof Statement) {
1336             final Statement statement = (Statement) object;
1337             final Resource subj = statement.getSubject();
1338             final URI pred = statement.getPredicate();
1339             final Value obj = statement.getObject();
1340             final Resource ctx = statement.getContext();
1341             final StringBuilder builder = new StringBuilder();
1342             builder.append('(');
1343             toString(subj, namespaces, builder);
1344             builder.append(',').append(' ');
1345             toString(pred, namespaces, builder);
1346             builder.append(',').append(' ');
1347             toString(obj, namespaces, builder);
1348             builder.append(")");
1349             if (statement.getContext() != null) {
1350                 builder.append(' ').append('[');
1351                 toString(ctx, namespaces, builder);
1352                 builder.append(']');
1353             }
1354             return builder.toString();
1355 
1356         } else if (object != null) {
1357             final Value value = convert(object, Value.class);
1358             final StringBuilder builder = new StringBuilder();
1359             toString(value, namespaces, builder);
1360             return builder.toString();
1361         }
1362 
1363         return null;
1364     }
1365 
1366     /**
1367      * Returns a string representation of the supplied data model object, optionally using the
1368      * supplied namespaces. This method is a shortcut for {@link #toString(Object, Map, boolean)}
1369      * when no record properties are desired in output.
1370      *
1371      * @param object
1372      *            the data model object, possibly null
1373      * @param namespaces
1374      *            the optional prefix-to-namespace mappings to use for generating the string,
1375      *            possibly null
1376      * @return the produced string, or null if a null object was passed as input
1377      */
1378     public static String toString(final Object object,
1379             @Nullable final Map<String, String> namespaces) {
1380         return toString(object, namespaces, false);
1381     }
1382 
1383     private static void toString(final Value value,
1384             @Nullable final Map<String, String> namespaces, final StringBuilder builder) {
1385 
1386         if (value instanceof URI) {
1387             final URI uri = (URI) value;
1388             String prefix = null;
1389             if (namespaces != null) {
1390                 prefix = namespaceToPrefix(uri.getNamespace(), namespaces);
1391             }
1392             if (prefix != null) {
1393                 builder.append(prefix).append(':').append(uri.getLocalName());
1394             } else {
1395                 builder.append('<').append(uri.stringValue()).append('>');
1396             }
1397 
1398         } else if (value instanceof BNode) {
1399             builder.append('_').append(':').append(((BNode) value).getID());
1400 
1401         } else {
1402             final Literal literal = (Literal) value;
1403             builder.append('\"').append(literal.getLabel().replace("\"", "\\\"")).append('\"');
1404             final URI datatype = literal.getDatatype();
1405             if (datatype != null) {
1406                 builder.append('^').append('^');
1407                 toString(datatype, namespaces, builder);
1408             } else {
1409                 final String language = literal.getLanguage();
1410                 if (language != null) {
1411                     builder.append('@').append(language);
1412                 }
1413             }
1414         }
1415     }
1416 
1417     private Data() {
1418     }
1419 
1420     private static final class TotalOrdering extends Ordering<Object> {
1421 
1422         private static final int DT_BOOLEAN = 1;
1423 
1424         private static final int DT_STRING = 2;
1425 
1426         private static final int DT_LONG = 3;
1427 
1428         private static final int DT_DOUBLE = 4;
1429 
1430         private static final int DT_DECIMAL = 5;
1431 
1432         private static final int DT_CALENDAR = 6;
1433 
1434         @Override
1435         public int compare(final Object first, final Object second) {
1436             if (first == null) {
1437                 return second == null ? 0 : -1;
1438             } else if (second == null) {
1439                 return 1;
1440             } else if (first instanceof URI) {
1441                 return compareURI((URI) first, second);
1442             } else if (first instanceof BNode) {
1443                 return compareBNode((BNode) first, second);
1444             } else if (first instanceof Record) {
1445                 return compareRecord((Record) first, second);
1446             } else if (first instanceof Statement) {
1447                 return compareStatement((Statement) first, second);
1448             } else if (first instanceof Literal) {
1449                 return compareLiteral((Literal) first, second);
1450             } else {
1451                 return compareLiteral(convert(first, Literal.class), second);
1452             }
1453         }
1454 
1455         private int compareStatement(final Statement first, final Object second) {
1456             if (second instanceof Statement) {
1457                 final Statement secondStmt = (Statement) second;
1458                 int result = compare(first.getSubject(), secondStmt.getSubject());
1459                 if (result != 0) {
1460                     return result;
1461                 }
1462                 result = compare(first.getPredicate(), secondStmt.getPredicate());
1463                 if (result != 0) {
1464                     return result;
1465                 }
1466                 result = compare(first.getObject(), secondStmt.getObject());
1467                 if (result != 0) {
1468                     return result;
1469                 }
1470                 result = compare(first.getContext(), secondStmt.getContext());
1471                 return result;
1472             }
1473             return -1;
1474         }
1475 
1476         private int compareLiteral(final Literal first, final Object second) {
1477             if (second instanceof Resource || second instanceof Record) {
1478                 return -1;
1479             } else if (second instanceof Statement) {
1480                 return 1;
1481             }
1482             final Literal secondLit = second instanceof Literal ? (Literal) second : convert(
1483                     second, Literal.class);
1484             final int firstGroup = classifyDatatype(first.getDatatype());
1485             final int secondGroup = classifyDatatype(secondLit.getDatatype());
1486             switch (firstGroup) {
1487             case DT_BOOLEAN:
1488                 if (secondGroup == DT_BOOLEAN) {
1489                     return Booleans.compare(first.booleanValue(), secondLit.booleanValue());
1490                 }
1491                 break;
1492             case DT_STRING:
1493                 if (secondGroup == DT_STRING) {
1494                     final int result = first.getLabel().compareTo(secondLit.getLabel());
1495                     if (result != 0) {
1496                         return result;
1497                     }
1498                     final String firstLang = first.getLanguage();
1499                     final String secondLang = secondLit.getLanguage();
1500                     if (firstLang == null) {
1501                         return secondLang == null ? 0 : -1;
1502                     } else {
1503                         return secondLang == null ? 1 : firstLang.compareTo(secondLang);
1504                     }
1505                 }
1506                 break;
1507             case DT_LONG:
1508                 if (secondGroup == DT_LONG) {
1509                     return Longs.compare(first.longValue(), secondLit.longValue());
1510                 } else if (secondGroup == DT_DOUBLE) {
1511                     return Doubles.compare(first.doubleValue(), secondLit.doubleValue());
1512                 } else if (secondGroup == DT_DECIMAL) {
1513                     return first.decimalValue().compareTo(secondLit.decimalValue());
1514                 }
1515                 break;
1516             case DT_DOUBLE:
1517                 if (secondGroup == DT_LONG //
1518                         || secondGroup == DT_DOUBLE) {
1519                     return Doubles.compare(first.doubleValue(), secondLit.doubleValue());
1520                 } else if (secondGroup == DT_DECIMAL) {
1521                     return first.decimalValue().compareTo(secondLit.decimalValue());
1522                 }
1523                 break;
1524             case DT_DECIMAL:
1525                 if (secondGroup == DT_LONG || secondGroup == DT_DOUBLE
1526                         || secondGroup == DT_DECIMAL) {
1527                     return first.decimalValue().compareTo(secondLit.decimalValue());
1528                 }
1529                 break;
1530             case DT_CALENDAR:
1531                 if (secondGroup == DT_CALENDAR) {
1532                     final int result = first.calendarValue().compare(secondLit.calendarValue());
1533                     return result == DatatypeConstants.INDETERMINATE ? 0 : result;
1534                 }
1535                 break;
1536             default:
1537             }
1538             return firstGroup < secondGroup ? -1 : 1;
1539         }
1540 
1541         private int compareBNode(final BNode first, final Object second) {
1542             if (second instanceof BNode) {
1543                 return first.getID().compareTo(((BNode) second).getID());
1544             } else if (second instanceof URI || second instanceof Record) {
1545                 return -1;
1546             }
1547             return 1;
1548         }
1549 
1550         private int compareURI(final URI first, final Object second) {
1551             if (second instanceof URI) {
1552                 return first.stringValue().compareTo(((URI) second).stringValue());
1553             } else if (second instanceof Record) {
1554                 return -1;
1555             }
1556             return 1;
1557         }
1558 
1559         private int compareRecord(final Record first, final Object second) {
1560             if (second instanceof Record) {
1561                 return first.compareTo((Record) second);
1562             }
1563             return 1;
1564         }
1565 
1566         private static int classifyDatatype(final URI datatype) {
1567             if (datatype == null || datatype.equals(XMLSchema.STRING)) {
1568                 return DT_STRING;
1569             } else if (datatype.equals(XMLSchema.BOOLEAN)) {
1570                 return DT_BOOLEAN;
1571             } else if (datatype.equals(XMLSchema.INT) || datatype.equals(XMLSchema.LONG)
1572                     || datatype.equals(XMLSchema.SHORT) || datatype.equals(XMLSchema.BYTE)) {
1573                 return DT_LONG;
1574             } else if (datatype.equals(XMLSchema.DOUBLE) || datatype.equals(XMLSchema.FLOAT)) {
1575                 return DT_DOUBLE;
1576             } else if (datatype.equals(XMLSchema.DATE) || datatype.equals(XMLSchema.DATETIME)) {
1577                 return DT_CALENDAR;
1578             } else if (datatype.equals(XMLSchema.DECIMAL) || datatype.equals(XMLSchema.INTEGER)
1579                     || datatype.equals(XMLSchema.NON_NEGATIVE_INTEGER)
1580                     || datatype.equals(XMLSchema.POSITIVE_INTEGER)
1581                     || datatype.equals(XMLSchema.NEGATIVE_INTEGER)) {
1582                 return DT_DECIMAL;
1583             } else if (datatype.equals(XMLSchema.NORMALIZEDSTRING)
1584                     || datatype.equals(XMLSchema.TOKEN) || datatype.equals(XMLSchema.NMTOKEN)
1585                     || datatype.equals(XMLSchema.LANGUAGE) || datatype.equals(XMLSchema.NAME)
1586                     || datatype.equals(XMLSchema.NCNAME)) {
1587                 return DT_STRING;
1588             }
1589             throw new IllegalArgumentException("Comparison unsupported for literal datatype "
1590                     + datatype);
1591         }
1592 
1593     }
1594 
1595     private static final class PartialOrdering extends Ordering<Object> {
1596 
1597         private final Comparator<Object> totalComparator;
1598 
1599         PartialOrdering(final Comparator<Object> totalComparator) {
1600             this.totalComparator = Preconditions.checkNotNull(totalComparator);
1601         }
1602 
1603         @Override
1604         public int compare(final Object first, final Object second) {
1605             final int result = this.totalComparator.compare(first, second);
1606             if (result == Integer.MIN_VALUE || result == Integer.MAX_VALUE) {
1607                 throw new IllegalArgumentException("Incomparable values: " + first + ", " + second);
1608             }
1609             return result;
1610         }
1611 
1612     }
1613 
1614     private static final class NamespaceMap extends AbstractMap<String, String> {
1615 
1616         private final Map<String, String> namespaces;
1617 
1618         private final Map<String, String> prefixes;
1619 
1620         private final EntrySet entries;
1621 
1622         NamespaceMap() {
1623             this.namespaces = Maps.newHashMap();
1624             this.prefixes = Maps.newHashMap();
1625             this.entries = new EntrySet();
1626         }
1627 
1628         @Override
1629         public int size() {
1630             return this.namespaces.size();
1631         }
1632 
1633         @Override
1634         public boolean isEmpty() {
1635             return this.namespaces.isEmpty();
1636         }
1637 
1638         @Override
1639         public boolean containsKey(final Object prefix) {
1640             return this.namespaces.containsKey(prefix);
1641         }
1642 
1643         @Override
1644         public boolean containsValue(final Object namespace) {
1645             return this.prefixes.containsKey(namespace);
1646         }
1647 
1648         @Override
1649         public String get(final Object prefix) {
1650             return this.namespaces.get(prefix);
1651         }
1652 
1653         public String getPrefix(final Object namespace) {
1654             return this.prefixes.get(namespace);
1655         }
1656 
1657         @Override
1658         public String put(final String prefix, final String namespace) {
1659             this.prefixes.put(namespace, prefix);
1660             return this.namespaces.put(prefix, namespace);
1661         }
1662 
1663         @Override
1664         public String remove(final Object prefix) {
1665             final String namespace = super.remove(prefix);
1666             removeInverse(namespace, prefix);
1667             return namespace;
1668         }
1669 
1670         private void removeInverse(@Nullable final String namespace, final Object prefix) {
1671             if (namespace == null) {
1672                 return;
1673             }
1674             final String inversePrefix = this.prefixes.remove(namespace);
1675             if (!prefix.equals(inversePrefix)) {
1676                 this.prefixes.put(namespace, inversePrefix);
1677             } else if (this.prefixes.size() != this.namespaces.size()) {
1678                 for (final Map.Entry<String, String> entry : this.namespaces.entrySet()) {
1679                     if (entry.getValue().equals(namespace)) {
1680                         this.prefixes.put(entry.getValue(), entry.getKey());
1681                         break;
1682                     }
1683                 }
1684             }
1685         }
1686 
1687         @Override
1688         public void clear() {
1689             this.namespaces.clear();
1690             this.prefixes.clear();
1691         }
1692 
1693         @Override
1694         public Set<Entry<String, String>> entrySet() {
1695             return this.entries;
1696         }
1697 
1698         final class EntrySet extends AbstractSet<Map.Entry<String, String>> {
1699 
1700             @Override
1701             public int size() {
1702                 return NamespaceMap.this.namespaces.size();
1703             }
1704 
1705             @Override
1706             public Iterator<Entry<String, String>> iterator() {
1707                 final Iterator<Entry<String, String>> iterator = NamespaceMap.this.namespaces
1708                         .entrySet().iterator();
1709                 return new Iterator<Entry<String, String>>() {
1710 
1711                     private Entry<String, String> last;
1712 
1713                     @Override
1714                     public boolean hasNext() {
1715                         return iterator.hasNext();
1716                     }
1717 
1718                     @Override
1719                     public Entry<String, String> next() {
1720                         this.last = new EntryWrapper(iterator.next());
1721                         return this.last;
1722                     }
1723 
1724                     @Override
1725                     public void remove() {
1726                         iterator.remove();
1727                         removeInverse(this.last.getValue(), this.last.getKey());
1728                     }
1729 
1730                 };
1731             }
1732 
1733             private class EntryWrapper implements Entry<String, String> {
1734 
1735                 private final Entry<String, String> entry;
1736 
1737                 EntryWrapper(final Entry<String, String> entry) {
1738                     this.entry = entry;
1739                 }
1740 
1741                 @Override
1742                 public String getKey() {
1743                     return this.entry.getKey();
1744                 }
1745 
1746                 @Override
1747                 public String getValue() {
1748                     return this.entry.getValue();
1749                 }
1750 
1751                 @Override
1752                 public String setValue(final String namespace) {
1753                     final String oldNamespace = this.entry.getValue();
1754                     if (!Objects.equal(oldNamespace, namespace)) {
1755                         final String prefix = this.entry.getKey();
1756                         removeInverse(oldNamespace, prefix);
1757                         this.entry.setValue(namespace);
1758                         NamespaceMap.this.prefixes.put(namespace, prefix);
1759                     }
1760                     return oldNamespace;
1761                 }
1762 
1763                 @Override
1764                 public boolean equals(final Object object) {
1765                     return this.entry.equals(object);
1766                 }
1767 
1768                 @Override
1769                 public int hashCode() {
1770                     return this.entry.hashCode();
1771                 }
1772 
1773                 @Override
1774                 public String toString() {
1775                     return this.entry.toString();
1776                 }
1777 
1778             }
1779         }
1780 
1781     }
1782 
1783     private static final class NamespaceCombinedMap extends AbstractMap<String, String> {
1784 
1785         final Map<String, String> primaryNamespaces;
1786 
1787         final Map<String, String> secondaryNamespaces;
1788 
1789         NamespaceCombinedMap(final Map<String, String> primaryNamespaces,
1790                 final Map<String, String> secondaryNamespaces) {
1791 
1792             this.primaryNamespaces = primaryNamespaces;
1793             this.secondaryNamespaces = secondaryNamespaces;
1794         }
1795 
1796         @Override
1797         public String get(final Object prefix) {
1798             String uri = this.primaryNamespaces.get(prefix);
1799             if (uri == null) {
1800                 uri = this.secondaryNamespaces.get(prefix);
1801             }
1802             return uri;
1803         }
1804 
1805         @Override
1806         public String put(final String prefix, final String uri) {
1807             return this.primaryNamespaces.put(prefix, uri);
1808         }
1809 
1810         @Override
1811         public Set<Map.Entry<String, String>> entrySet() {
1812             return new EntrySet();
1813         }
1814 
1815         @Override
1816         public void clear() {
1817             this.primaryNamespaces.clear();
1818         }
1819 
1820         final class EntrySet extends AbstractSet<Map.Entry<String, String>> {
1821 
1822             @Override
1823             public int size() {
1824                 return Sets.union(NamespaceCombinedMap.this.primaryNamespaces.keySet(), //
1825                         NamespaceCombinedMap.this.secondaryNamespaces.keySet()).size();
1826             }
1827 
1828             @Override
1829             public Iterator<Entry<String, String>> iterator() {
1830 
1831                 final Set<String> additionalKeys = Sets.difference(
1832                         NamespaceCombinedMap.this.secondaryNamespaces.keySet(),
1833                         NamespaceCombinedMap.this.primaryNamespaces.keySet());
1834 
1835                 Function<String, Entry<String, String>> transformer;
1836                 transformer = new Function<String, Entry<String, String>>() {
1837 
1838                     @Override
1839                     public Entry<String, String> apply(final String prefix) {
1840                         return new AbstractMap.SimpleImmutableEntry<String, String>(prefix,
1841                                 NamespaceCombinedMap.this.secondaryNamespaces.get(prefix));
1842                     }
1843 
1844                 };
1845 
1846                 return Iterators.concat(NamespaceCombinedMap.this.primaryNamespaces.entrySet()
1847                         .iterator(), ignoreRemove(Iterators.transform(additionalKeys.iterator(),
1848                         transformer)));
1849             }
1850 
1851             private <T> Iterator<T> ignoreRemove(final Iterator<T> iterator) {
1852                 return new Iterator<T>() {
1853 
1854                     @Override
1855                     public boolean hasNext() {
1856                         return iterator.hasNext();
1857                     }
1858 
1859                     @Override
1860                     public T next() {
1861                         return iterator.next();
1862                     }
1863 
1864                     @Override
1865                     public void remove() {
1866                     }
1867 
1868                 };
1869             }
1870 
1871         }
1872 
1873     }
1874 
1875 }