View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.mediapackage;
23  
24  import static java.lang.String.format;
25  import static javax.xml.XMLConstants.DEFAULT_NS_PREFIX;
26  import static javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI;
27  import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE;
28  import static javax.xml.XMLConstants.XML_NS_URI;
29  import static org.opencastproject.util.EqualsUtil.hash;
30  
31  import org.opencastproject.util.RequireUtil;
32  import org.opencastproject.util.XmlNamespaceBinding;
33  import org.opencastproject.util.XmlNamespaceContext;
34  import org.opencastproject.util.XmlSafeParser;
35  
36  import org.apache.commons.lang3.StringUtils;
37  import org.w3c.dom.Document;
38  import org.w3c.dom.Element;
39  import org.w3c.dom.Node;
40  import org.w3c.dom.bootstrap.DOMImplementationRegistry;
41  import org.w3c.dom.ls.DOMImplementationLS;
42  import org.w3c.dom.ls.LSOutput;
43  import org.w3c.dom.ls.LSSerializer;
44  import org.xml.sax.Attributes;
45  
46  import java.io.ByteArrayOutputStream;
47  import java.io.IOException;
48  import java.io.OutputStream;
49  import java.io.Serializable;
50  import java.nio.charset.StandardCharsets;
51  import java.util.ArrayList;
52  import java.util.Collections;
53  import java.util.Comparator;
54  import java.util.HashMap;
55  import java.util.Iterator;
56  import java.util.List;
57  import java.util.Map;
58  import java.util.Map.Entry;
59  import java.util.stream.Collectors;
60  
61  import javax.xml.parsers.DocumentBuilder;
62  import javax.xml.parsers.DocumentBuilderFactory;
63  import javax.xml.parsers.ParserConfigurationException;
64  import javax.xml.transform.TransformerException;
65  
66  /**
67   * This is a basic implementation for handling simple catalogs of metadata. It provides utility methods to store
68   * key-value data.
69   * <p>
70   * For a definition of the terms <dfn>expanded name</dfn>, <dfn>qualified name</dfn> or <dfn>QName</dfn>, <dfn>namespace
71   * prefix</dfn>, <dfn>local part</dfn> and <dfn>local name</dfn>, please see <a
72   * href="http://www.w3.org/TR/REC-xml-names">http://www.w3.org/TR/REC-xml-names</a>
73   * <p>
74   * By default the following namespace prefixes are bound:
75   * <ul>
76   * <li>xml - http://www.w3.org/XML/1998/namespace
77   * <li>xmlns - http://www.w3.org/2000/xmlns/
78   * <li>xsi - http://www.w3.org/2001/XMLSchema-instance
79   * </ul>
80   * <p>
81   * <h2>Limitations</h2>
82   * XMLCatalog supports only <em>one</em> prefix binding per namespace name, so you cannot create documents like the
83   * following using XMLCatalog:
84   *
85   * <pre>
86   * &lt;root xmlns:x=&quot;http://x.demo.org&quot; xmlns:y=&quot;http://x.demo.org&quot;&gt;
87   *   &lt;x:elem&gt;value&lt;/x:elem&gt;
88   *   &lt;y:elem&gt;value&lt;/y:elem&gt;
89   * &lt;/root&gt;
90   * </pre>
91   *
92   * However, reading of those documents is supported.
93   */
94  public abstract class XMLCatalogImpl extends CatalogImpl implements XMLCatalog {
95    private static final long serialVersionUID = -7580292199527168951L;
96  
97    /** Expanded name of the XML language attribute <code>xml:lang</code>. */
98    public static final EName XML_LANG_ATTR = new EName(XML_NS_URI, "lang");
99  
100   /** Namespace prefix for XML schema instance. */
101   public static final String XSI_NS_PREFIX = "xsi";
102 
103   /** To marshaling empty fields to remove existing values during merge, default is not to marshal empty elements */
104   protected boolean includeEmpty = false;
105 
106   /**
107    * Expanded name of the XSI type attribute.
108    * <p>
109    * See <a href="http://www.w3.org/TR/xmlschema-1/#xsi_type">http://www.w3.org/TR/xmlschema-1/#xsi_type</a> for the
110    * definition.
111    */
112   public static final EName XSI_TYPE_ATTR = new EName(W3C_XML_SCHEMA_INSTANCE_NS_URI, "type");
113 
114   /** Key (QName) value meta data */
115   protected final Map<EName, List<CatalogEntry>> data = new HashMap<>();
116 
117   /** Namespace - prefix bindings */
118   protected XmlNamespaceContext bindings;
119 
120   /**
121    * Create an empty catalog and register the {@link javax.xml.XMLConstants#W3C_XML_SCHEMA_INSTANCE_NS_URI}
122    * namespace.
123    */
124   protected XMLCatalogImpl() {
125     super();
126     bindings = XmlNamespaceContext.mk(XSI_NS_PREFIX, W3C_XML_SCHEMA_INSTANCE_NS_URI);
127   }
128 
129   protected void addBinding(XmlNamespaceBinding binding) {
130     bindings = bindings.add(binding);
131   }
132 
133   protected XmlNamespaceContext getBindings() {
134     return bindings;
135   }
136 
137   /**
138    * Clears the catalog.
139    */
140   protected void clear() {
141     data.clear();
142   }
143 
144   /**
145    * Adds the element to the metadata collection.
146    *
147    * @param element
148    *          the expanded name of the element
149    * @param value
150    *          the value
151    */
152   protected void addElement(EName element, String value) {
153     if (element == null) {
154       throw new IllegalArgumentException("Expanded name must not be null");
155     }
156 
157     addElement(new CatalogEntry(element, value, NO_ATTRIBUTES));
158   }
159 
160   /**
161    * Adds the element with the <code>xml:lang</code> attribute to the metadata collection.
162    *
163    * @param element
164    *          the expanded name of the element
165    * @param value
166    *          the value
167    * @param language
168    *          the language identifier (two letter ISO 639)
169    */
170   protected void addLocalizedElement(EName element, String value, String language) {
171     RequireUtil.notNull(element, "expanded name");
172     RequireUtil.notNull(language, "language");
173 
174     Map<EName, String> attributes = new HashMap<>(1);
175     attributes.put(XML_LANG_ATTR, language);
176     addElement(new CatalogEntry(element, value, attributes));
177   }
178 
179   /**
180    * Adds the element with the <code>xsi:type</code> attribute to the metadata collection.
181    *
182    * @param value
183    *          the value
184    * @param type
185    *          the element type
186    */
187   protected void addTypedElement(EName element, String value, EName type) {
188     RequireUtil.notNull(element, "expanded name");
189     RequireUtil.notNull(type, "type");
190 
191     Map<EName, String> attributes = new HashMap<>(1);
192     attributes.put(XSI_TYPE_ATTR, toQName(type));
193     addElement(new CatalogEntry(element, value, attributes));
194   }
195 
196   /**
197    * Adds an element with the <code>xml:lang</code> and <code>xsi:type</code> attributes to the metadata collection.
198    *
199    * @param element
200    *          the expanded name of the element
201    * @param value
202    *          the value
203    * @param language
204    *          the language identifier (two letter ISO 639)
205    * @param type
206    *          the element type
207    */
208   protected void addTypedLocalizedElement(EName element, String value, String language, EName type) {
209     if (element == null) {
210       throw new IllegalArgumentException("EName name must not be null");
211     }
212     if (type == null) {
213       throw new IllegalArgumentException("Type must not be null");
214     }
215     if (language == null) {
216       throw new IllegalArgumentException("Language must not be null");
217     }
218 
219     Map<EName, String> attributes = new HashMap<>(2);
220     attributes.put(XML_LANG_ATTR, language);
221     attributes.put(XSI_TYPE_ATTR, toQName(type));
222     addElement(new CatalogEntry(element, value, attributes));
223   }
224 
225   /**
226    * Adds an element with attributes to the catalog.
227    *
228    * @param element
229    *          the expanded name of the element
230    * @param value
231    *          the element's value
232    * @param attributes
233    *          the attributes. May be null
234    */
235   protected void addElement(EName element, String value, Attributes attributes) {
236     if (element == null) {
237       throw new IllegalArgumentException("Expanded name must not be null");
238     }
239 
240     Map<EName, String> attributeMap = new HashMap<>();
241     if (attributes != null) {
242       for (int i = 0; i < attributes.getLength(); i++) {
243         attributeMap.put(new EName(attributes.getURI(i), attributes.getLocalName(i)), attributes.getValue(i));
244       }
245     }
246     addElement(new CatalogEntry(element, value, attributeMap));
247   }
248 
249   /**
250    * Adds the catalog element to the list of elements.
251    *
252    * @param element
253    *          the element
254    */
255   private void addElement(CatalogEntry element) {
256 
257     // Option includeEmpty allows marshaling empty elements
258     // for deleting existing values during a catalog merge
259     if (element == null) {
260       return;
261     }
262     if (StringUtils.trimToNull(element.getValue()) == null && !includeEmpty) {
263       return;
264     }
265     List<CatalogEntry> values = data.get(element.getEName());
266     if (values == null) {
267       values = new ArrayList<>();
268       data.put(element.getEName(), values);
269     }
270     values.add(element);
271   }
272 
273   /**
274    * Completely removes an element.
275    *
276    * @param element
277    *          the expanded name of the element
278    */
279   protected void removeElement(EName element) {
280     removeValues(element, null, true);
281   }
282 
283   /**
284    * Removes all entries in a certain language from an element.
285    *
286    * @param element
287    *          the expanded name of the element
288    * @param language
289    *          the language code (two letter ISO 639) or null to <em>only</em> remove entries without an
290    *          <code>xml:lang</code> attribute
291    */
292   protected void removeLocalizedValues(EName element, String language) {
293     removeValues(element, language, false);
294   }
295 
296   /**
297    * Removes values from an element or the complete element from the catalog.
298    *
299    * @param element
300    *          the expanded name of the element
301    * @param language
302    *          the language code (two letter ISO 639) to remove or null to remove entries without language code
303    * @param all
304    *          true - remove all entries for that element. This parameter overrides the language parameter.
305    */
306   private void removeValues(EName element, String language, boolean all) {
307     if (all) {
308       data.remove(element);
309     } else {
310       List<CatalogEntry> entries = data.get(element);
311       if (entries != null) {
312         for (Iterator<CatalogEntry> i = entries.iterator(); i.hasNext();) {
313           CatalogEntry entry = i.next();
314           if (equal(language, entry.getAttribute(XML_LANG_ATTR))) {
315             i.remove();
316           }
317         }
318       }
319     }
320   }
321 
322   /**
323    * Returns the values that are associated with the specified key.
324    *
325    * @param element
326    *          the expanded name of the element
327    * @return the elements
328    */
329   protected CatalogEntry[] getValues(EName element) {
330     List<CatalogEntry> values = data.get(element);
331     if (values != null && values.size() > 0) {
332       return values.toArray(new CatalogEntry[values.size()]);
333     }
334     return new CatalogEntry[] {};
335   }
336 
337   protected List<CatalogEntry> getEntriesSorted() {
338     return data.values().stream()
339         .flatMap(List::stream)
340         .sorted(catalogEntryComparator)
341         .collect(Collectors.toList());
342   }
343 
344   /**
345    * Returns the values that are associated with the specified key.
346    *
347    * @param element
348    *          the expanded name of the element
349    * @return all values of the element or an empty list if this element does not exist or does not have any values
350    */
351   @SuppressWarnings("unchecked")
352   protected List<CatalogEntry> getValuesAsList(EName element) {
353     List<CatalogEntry> values = data.get(element);
354     return values != null ? values : Collections.EMPTY_LIST;
355   }
356 
357   /**
358    * Returns the values that are associated with the specified key.
359    *
360    * @param element
361    *          the expandend name of the element
362    * @param language
363    *          a language code or null to get values without <code>xml:lang</code> attribute
364    * @return all values of the element
365    */
366   @SuppressWarnings("unchecked")
367   protected List<CatalogEntry> getLocalizedValuesAsList(EName element, String language) {
368     List<CatalogEntry> values = data.get(element);
369 
370     if (values != null) {
371       List<CatalogEntry> filtered = new ArrayList<>();
372       for (CatalogEntry value : values) {
373         if (equal(language, value.getAttribute(XML_LANG_ATTR))) {
374           filtered.add(value);
375         }
376       }
377       return filtered;
378     } else {
379       return Collections.EMPTY_LIST;
380     }
381   }
382 
383   /**
384    * Returns the first value that is associated with the specified name.
385    *
386    * @param element
387    *          the expanded name of the element
388    * @return the first value
389    */
390   protected CatalogEntry getFirstValue(EName element) {
391     List<CatalogEntry> elements = data.get(element);
392     if (elements != null && elements.size() > 0) {
393       return elements.get(0);
394     }
395     return null;
396   }
397 
398   /**
399    * Returns the first element that is associated with the specified name and attribute.
400    *
401    * @param element
402    *          the expanded name of the element
403    * @param attributeEName
404    *          the expanded attribute name
405    * @param attributeValue
406    *          the attribute value
407    * @return the first value
408    */
409   protected CatalogEntry getFirstValue(EName element, EName attributeEName, String attributeValue) {
410     List<CatalogEntry> elements = data.get(element);
411     if (elements != null) {
412       for (CatalogEntry entry : elements) {
413         String v = entry.getAttribute(attributeEName);
414         if (equal(attributeValue, v)) {
415           return entry;
416         }
417       }
418     }
419     return null;
420   }
421 
422   /**
423    * Returns the first value that is associated with the specified name and language.
424    *
425    * @param element
426    *          the expanded name of the element
427    * @param language
428    *          the language identifier or null to get only elements without <code>xml:lang</code> attribute
429    * @return the first value
430    */
431   protected CatalogEntry getFirstLocalizedValue(EName element, String language) {
432     return getFirstValue(element, XML_LANG_ATTR, language);
433   }
434 
435   /**
436    * Returns the first value that is associated with the specified name and language.
437    *
438    * @param element
439    *          the expanded name of the element
440    * @param type
441    *          the <code>xsi:type</code> value
442    * @return the element
443    */
444   protected CatalogEntry getFirstTypedValue(EName element, String type) {
445     return getFirstValue(element, XSI_TYPE_ATTR, type);
446   }
447 
448   /**
449    * Tests two objects for equality.
450    */
451   protected boolean equal(Object a, Object b) {
452     return (a == null && b == null) || (a != null && a.equals(b));
453   }
454 
455   /**
456    * Creates an xml document root and returns it.
457    *
458    * @return the document
459    * @throws ParserConfigurationException
460    *           If the xml parser environment is not correctly configured
461    */
462   protected Document newDocument() throws ParserConfigurationException {
463     DocumentBuilderFactory docBuilderFactory = XmlSafeParser.newDocumentBuilderFactory();
464     docBuilderFactory.setNamespaceAware(true);
465     DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
466     return docBuilder.newDocument();
467   }
468 
469   /**
470    * @see org.opencastproject.mediapackage.AbstractMediaPackageElement#toManifest(org.w3c.dom.Document,
471    *      org.opencastproject.mediapackage.MediaPackageSerializer)
472    */
473   @Override
474   public Node toManifest(Document document, MediaPackageSerializer serializer) throws MediaPackageException {
475     return super.toManifest(document, serializer);
476   }
477 
478   /**
479    * Get a prefix from {@link #bindings} but throw a {@link NamespaceBindingException} if none found.
480    */
481   protected String getPrefix(String namespaceURI) {
482     final String prefix = bindings.getPrefix(namespaceURI);
483     if (prefix != null) {
484       return prefix;
485     } else {
486       throw new NamespaceBindingException(format("Namespace URI %s is not bound to a prefix", namespaceURI));
487     }
488   }
489 
490   /**
491    * @see org.opencastproject.mediapackage.XMLCatalog#includeEmpty(boolean)
492    */
493   @Override
494   public void includeEmpty(boolean includeEmpty) {
495     this.includeEmpty = includeEmpty;
496   }
497 
498   /**
499    * Transform an expanded name to a qualified name based on the registered binding.
500    *
501    * @param eName
502    *          the expanded name to transform
503    * @return the qualified name, e.g. <code>dcterms:title</code>
504    * @throws NamespaceBindingException
505    *           if the namespace name is not bound to a prefix
506    */
507   protected String toQName(EName eName) {
508     if (eName.hasNamespace()) {
509       return toQName(getPrefix(eName.getNamespaceURI()), eName.getLocalName());
510     } else {
511       return eName.getLocalName();
512     }
513   }
514 
515   /**
516    * Transform an qualified name consisting of prefix and local part to an expanded name, based on the registered
517    * binding.
518    *
519    * @param prefix
520    *          the prefix
521    * @param localName
522    *          the local part
523    * @return the expanded name
524    * @throws NamespaceBindingException
525    *           if the namespace name is not bound to a prefix
526    */
527   protected EName toEName(String prefix, String localName) {
528     return new EName(bindings.getNamespaceURI(prefix), localName);
529   }
530 
531   /**
532    * Transform a qualified name to an expanded name, based on the registered binding.
533    *
534    * @param qName
535    *          the qualified name, e.g. <code>dcterms:title</code> or <code>title</code>
536    * @return the expanded name
537    * @throws NamespaceBindingException
538    *           if the namespace name is not bound to a prefix
539    */
540   protected EName toEName(String qName) {
541     String[] parts = splitQName(qName);
542     return new EName(bindings.getNamespaceURI(parts[0]), parts[1]);
543   }
544 
545   /**
546    * Splits a QName into its parts.
547    *
548    * @param qName
549    *          the qname to split
550    * @return an array of prefix (0) and local part (1). The prefix is "" if the qname belongs to the default namespace.
551    */
552   private static String[] splitQName(String qName) {
553     final String[] parts = qName.split(":", 3);
554     switch (parts.length) {
555       case 1:
556         return new String[] { DEFAULT_NS_PREFIX, parts[0] };
557       case 2:
558         return parts;
559       default:
560         throw new IllegalArgumentException("Local name must not contain ':'");
561     }
562   }
563 
564   /**
565    * Returns a "prefixed name" consisting of namespace prefix and local name.
566    *
567    * @param prefix
568    *          the namespace prefix, may be <code>null</code>
569    * @param localName
570    *          the local name
571    * @return the "prefixed name" <code>prefix:localName</code>
572    */
573   private static String toQName(String prefix, String localName) {
574     final StringBuilder b = new StringBuilder();
575     if (prefix != null && !DEFAULT_NS_PREFIX.equals(prefix)) {
576       b.append(prefix);
577       b.append(":");
578     }
579     b.append(localName);
580     return b.toString();
581   }
582 
583   // --------------------------------------------------------------------------------------------
584 
585   private static final Map<EName, String> NO_ATTRIBUTES = new HashMap<>();
586 
587   CatalogEntry mkCatalogEntry(EName name, String value, Map<EName, String> attributes) {
588     return new CatalogEntry(name, value, attributes);
589   }
590 
591   /**
592    * Element representation.
593    */
594   public final class CatalogEntry implements XmlElement, Comparable<CatalogEntry>, Serializable {
595 
596     /** The serial version UID */
597     private static final long serialVersionUID = 7195298081966562710L;
598 
599     private final EName name;
600 
601     private final String value;
602 
603     /** The attributes of this element */
604     private final Map<EName, String> attributes;
605 
606     /**
607      * Creates a new catalog element representation with name, value and attributes.
608      *
609      * @param value
610      *          the element value
611      * @param attributes
612      *          the element attributes
613      */
614     public CatalogEntry(EName name, String value, Map<EName, String> attributes) {
615       this.name = name;
616       this.value = value;
617       this.attributes = new HashMap<>(attributes);
618     }
619 
620     /**
621      * Returns the qualified name of the entry as a string. The namespace of the entry has to be bound to a prefix for
622      * this method to succeed.
623      */
624     public String getQName() {
625       return toQName(name);
626     }
627 
628     /**
629      * Returns the expanded name of the entry.
630      */
631     public EName getEName() {
632       return name;
633     }
634 
635     /**
636      * Returns the element value.
637      *
638      * @return the value
639      */
640     public String getValue() {
641       return value;
642     }
643 
644     /**
645      * Returns <code>true</code> if the element contains attributes.
646      *
647      * @return <code>true</code> if the element contains attributes
648      */
649     public boolean hasAttributes() {
650       return attributes.size() > 0;
651     }
652 
653     /**
654      * Returns the element's attributes.
655      *
656      * @return the attributes
657      */
658     public Map<EName, String> getAttributes() {
659       return Collections.unmodifiableMap(attributes);
660     }
661 
662     /**
663      * Returns <code>true</code> if the element contains an attribute with the given name.
664      *
665      * @return <code>true</code> if the element contains the attribute
666      */
667     public boolean hasAttribute(EName name) {
668       return attributes.containsKey(name);
669     }
670 
671     /**
672      * Returns the attribute value for the given attribute.
673      *
674      * @return the attribute or null
675      */
676     public String getAttribute(EName name) {
677       return attributes.get(name);
678     }
679 
680     @Override
681     public int hashCode() {
682       return hash(name, value);
683     }
684 
685     @Override
686     public boolean equals(Object that) {
687       return (this == that) || (that instanceof CatalogEntry && eqFields((CatalogEntry) that));
688     }
689 
690     private boolean eqFields(CatalogEntry that) {
691       return this.compareTo(that) == 0;
692     }
693 
694     /**
695      * Returns the XML representation of this entry.
696      *
697      * @param document
698      *          the document
699      * @return the xml node
700      */
701     @Override
702     public Node toXml(Document document) {
703       Element node = document.createElement(toQName(name));
704       // Write prefix binding to document root element
705       bindNamespaceFor(document, name);
706 
707       List<EName> keySet = new ArrayList<>(attributes.keySet());
708       Collections.sort(keySet);
709       for (EName attrEName : keySet) {
710         String value = attributes.get(attrEName);
711         if (attrEName.hasNamespace()) {
712           // Write prefix binding to document root element
713           bindNamespaceFor(document, attrEName);
714           if (XSI_TYPE_ATTR.equals(attrEName)) {
715             // Special treatment for xsi:type attributes
716             try {
717               EName typeName = toEName(value);
718               bindNamespaceFor(document, typeName);
719             } catch (NamespaceBindingException ignore) {
720               // Type is either not a QName or its namespace is not bound.
721               // We decide to gently ignore those cases.
722             }
723           }
724         }
725         node.setAttribute(toQName(attrEName), value);
726       }
727       if (value != null) {
728         node.appendChild(document.createTextNode(value));
729       }
730       return node;
731     }
732 
733     /**
734      * Compare two catalog entries. Comparison order:
735      * - e_name
736      * - number of attributes (less come first)
737      * - attribute comparison (e_name -&gt; value)
738      */
739     @Override
740     public int compareTo(CatalogEntry o) {
741       int c = getEName().compareTo(o.getEName());
742       if (c != 0) {
743         return c;
744       }
745 
746       c = Integer.compare(attributes.size(), o.attributes.size());
747       if (c != 0) {
748         return c;
749       }
750 
751       // Sort attribute entries by attributeComparator
752       List<Entry<EName, String>> thisAttrs = attributes.entrySet().stream()
753           .sorted(attributeComparator)
754           .toList();
755 
756       List<Entry<EName, String>> otherAttrs = o.attributes.entrySet().stream()
757           .sorted(attributeComparator)
758           .toList();
759 
760       // Compare entries pairwise
761       for (int i = 0; i < thisAttrs.size(); i++) {
762         c = attributeComparator.compare(thisAttrs.get(i), otherAttrs.get(i));
763         if (c != 0) {
764           return c;
765         }
766       }
767 
768       return 0; // all equal
769     }
770 
771     /**
772      * Writes a namespace binding for catalog entry <code>name</code> to the documents root element.
773      * <code>xmlns:prefix="namespace"</code>
774      */
775     private void bindNamespaceFor(Document document, EName name) {
776       Element root = (Element) document.getFirstChild();
777       String namespace = name.getNamespaceURI();
778       // Do not bind the "xml" namespace. It is bound by default
779       if (!XML_NS_URI.equals(namespace)) {
780         root.setAttribute(XMLNS_ATTRIBUTE + ":" + XMLCatalogImpl.this.getPrefix(name.getNamespaceURI()),
781                 name.getNamespaceURI());
782       }
783     }
784 
785     @Override
786     public String toString() {
787       return value;
788     }
789   }
790 
791   static int doCompareTo(EName k1, String v1, EName k2, String v2) {
792     final int c = k1.compareTo(k2);
793     return c != 0 ? c : v1.compareTo(v2);
794   }
795 
796   private static final Comparator<Map.Entry<EName, String>> attributeComparator =
797       new Comparator<Map.Entry<EName, String>>() {
798         @Override public int compare(Entry<EName, String> o1, Entry<EName, String> o2) {
799           return doCompareTo(o1.getKey(), o1.getValue(), o2.getKey(), o2.getValue());
800         }
801       };
802 
803   private static final Comparator<CatalogEntry> catalogEntryComparator =
804       new Comparator<CatalogEntry>() {
805         @Override public int compare(CatalogEntry o1, CatalogEntry o2) {
806           return o1.compareTo(o2);
807         }
808       };
809 
810   // --------------------------------------------------------------------------------------------
811 
812   // --
813 
814   /**
815    * {@inheritDoc}
816    *
817    * @see org.opencastproject.mediapackage.XMLCatalog#toXml(java.io.OutputStream, boolean)
818    */
819   @Override
820   public void toXml(OutputStream out, boolean format) throws IOException {
821     try {
822       Document doc = this.toXml();
823       DOMImplementationRegistry reg = DOMImplementationRegistry.newInstance();
824       DOMImplementationLS impl = (DOMImplementationLS) reg.getDOMImplementation("LS");
825       LSSerializer serializer = impl.createLSSerializer();
826       serializer.getDomConfig().setParameter("format-pretty-print", format);
827       LSOutput output = impl.createLSOutput();
828       output.setByteStream(out);
829       serializer.write(doc, output);
830     } catch (ParserConfigurationException e) {
831       throw new IOException("unable to parse document");
832     } catch (TransformerException e) {
833       throw new IOException("unable to transform dom to a stream");
834     } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
835       throw new IOException("unable to serialize DOM");
836     }
837   }
838 
839   /**
840    * {@inheritDoc}
841    *
842    * @see org.opencastproject.mediapackage.XMLCatalog#toXmlString()
843    */
844   @Override
845   public String toXmlString() throws IOException {
846     ByteArrayOutputStream out = new ByteArrayOutputStream();
847     toXml(out, true);
848     return new String(out.toByteArray(), StandardCharsets.UTF_8);
849   }
850 
851 }