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