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     addElement(new CatalogEntry(element, value, NO_ATTRIBUTES));
157   }
158 
159   /**
160    * Adds the element with the <code>xml:lang</code> attribute to the metadata collection.
161    *
162    * @param element
163    *          the expanded name of the element
164    * @param value
165    *          the value
166    * @param language
167    *          the language identifier (two letter ISO 639)
168    */
169   protected void addLocalizedElement(EName element, String value, String language) {
170     RequireUtil.notNull(element, "expanded name");
171     RequireUtil.notNull(language, "language");
172 
173     Map<EName, String> attributes = new HashMap<>(1);
174     attributes.put(XML_LANG_ATTR, language);
175     addElement(new CatalogEntry(element, value, attributes));
176   }
177 
178   /**
179    * Adds the element with the <code>xsi:type</code> attribute to the metadata collection.
180    *
181    * @param value
182    *          the value
183    * @param type
184    *          the element type
185    */
186   protected void addTypedElement(EName element, String value, EName type) {
187     RequireUtil.notNull(element, "expanded name");
188     RequireUtil.notNull(type, "type");
189 
190     Map<EName, String> attributes = new HashMap<>(1);
191     attributes.put(XSI_TYPE_ATTR, toQName(type));
192     addElement(new CatalogEntry(element, value, attributes));
193   }
194 
195   /**
196    * Adds an element with the <code>xml:lang</code> and <code>xsi:type</code> attributes to the metadata collection.
197    *
198    * @param element
199    *          the expanded name of the element
200    * @param value
201    *          the value
202    * @param language
203    *          the language identifier (two letter ISO 639)
204    * @param type
205    *          the element type
206    */
207   protected void addTypedLocalizedElement(EName element, String value, String language, EName type) {
208     if (element == null)
209       throw new IllegalArgumentException("EName name must not be null");
210     if (type == null)
211       throw new IllegalArgumentException("Type must not be null");
212     if (language == null)
213       throw new IllegalArgumentException("Language must not be null");
214 
215     Map<EName, String> attributes = new HashMap<>(2);
216     attributes.put(XML_LANG_ATTR, language);
217     attributes.put(XSI_TYPE_ATTR, toQName(type));
218     addElement(new CatalogEntry(element, value, attributes));
219   }
220 
221   /**
222    * Adds an element with attributes to the catalog.
223    *
224    * @param element
225    *          the expanded name of the element
226    * @param value
227    *          the element's value
228    * @param attributes
229    *          the attributes. May be null
230    */
231   protected void addElement(EName element, String value, Attributes attributes) {
232     if (element == null)
233       throw new IllegalArgumentException("Expanded name must not be null");
234 
235     Map<EName, String> attributeMap = new HashMap<>();
236     if (attributes != null) {
237       for (int i = 0; i < attributes.getLength(); i++) {
238         attributeMap.put(new EName(attributes.getURI(i), attributes.getLocalName(i)), attributes.getValue(i));
239       }
240     }
241     addElement(new CatalogEntry(element, value, attributeMap));
242   }
243 
244   /**
245    * Adds the catalog element to the list of elements.
246    *
247    * @param element
248    *          the element
249    */
250   private void addElement(CatalogEntry element) {
251 
252     // Option includeEmpty allows marshaling empty elements
253     // for deleting existing values during a catalog merge
254     if (element == null)
255       return;
256     if (StringUtils.trimToNull(element.getValue()) == null && !includeEmpty)
257       return;
258     List<CatalogEntry> values = data.get(element.getEName());
259     if (values == null) {
260       values = new ArrayList<>();
261       data.put(element.getEName(), values);
262     }
263     values.add(element);
264   }
265 
266   /**
267    * Completely removes an element.
268    *
269    * @param element
270    *          the expanded name of the element
271    */
272   protected void removeElement(EName element) {
273     removeValues(element, null, true);
274   }
275 
276   /**
277    * Removes all entries in a certain language from an element.
278    *
279    * @param element
280    *          the expanded name of the element
281    * @param language
282    *          the language code (two letter ISO 639) or null to <em>only</em> remove entries without an
283    *          <code>xml:lang</code> attribute
284    */
285   protected void removeLocalizedValues(EName element, String language) {
286     removeValues(element, language, false);
287   }
288 
289   /**
290    * Removes values from an element or the complete element from the catalog.
291    *
292    * @param element
293    *          the expanded name of the element
294    * @param language
295    *          the language code (two letter ISO 639) to remove or null to remove entries without language code
296    * @param all
297    *          true - remove all entries for that element. This parameter overrides the language parameter.
298    */
299   private void removeValues(EName element, String language, boolean all) {
300     if (all) {
301       data.remove(element);
302     } else {
303       List<CatalogEntry> entries = data.get(element);
304       if (entries != null) {
305         for (Iterator<CatalogEntry> i = entries.iterator(); i.hasNext();) {
306           CatalogEntry entry = i.next();
307           if (equal(language, entry.getAttribute(XML_LANG_ATTR))) {
308             i.remove();
309           }
310         }
311       }
312     }
313   }
314 
315   /**
316    * Returns the values that are associated with the specified key.
317    *
318    * @param element
319    *          the expanded name of the element
320    * @return the elements
321    */
322   protected CatalogEntry[] getValues(EName element) {
323     List<CatalogEntry> values = data.get(element);
324     if (values != null && values.size() > 0) {
325       return values.toArray(new CatalogEntry[values.size()]);
326     }
327     return new CatalogEntry[] {};
328   }
329 
330   protected List<CatalogEntry> getEntriesSorted() {
331     return data.values().stream()
332         .flatMap(List::stream)
333         .sorted(catalogEntryComparator)
334         .collect(Collectors.toList());
335   }
336 
337   /**
338    * Returns the values that are associated with the specified key.
339    *
340    * @param element
341    *          the expanded name of the element
342    * @return all values of the element or an empty list if this element does not exist or does not have any values
343    */
344   @SuppressWarnings("unchecked")
345   protected List<CatalogEntry> getValuesAsList(EName element) {
346     List<CatalogEntry> values = data.get(element);
347     return values != null ? values : Collections.EMPTY_LIST;
348   }
349 
350   /**
351    * Returns the values that are associated with the specified key.
352    *
353    * @param element
354    *          the expandend name of the element
355    * @param language
356    *          a language code or null to get values without <code>xml:lang</code> attribute
357    * @return all values of the element
358    */
359   @SuppressWarnings("unchecked")
360   protected List<CatalogEntry> getLocalizedValuesAsList(EName element, String language) {
361     List<CatalogEntry> values = data.get(element);
362 
363     if (values != null) {
364       List<CatalogEntry> filtered = new ArrayList<>();
365       for (CatalogEntry value : values) {
366         if (equal(language, value.getAttribute(XML_LANG_ATTR))) {
367           filtered.add(value);
368         }
369       }
370       return filtered;
371     } else {
372       return Collections.EMPTY_LIST;
373     }
374   }
375 
376   /**
377    * Returns the first value that is associated with the specified name.
378    *
379    * @param element
380    *          the expanded name of the element
381    * @return the first value
382    */
383   protected CatalogEntry getFirstValue(EName element) {
384     List<CatalogEntry> elements = data.get(element);
385     if (elements != null && elements.size() > 0) {
386       return elements.get(0);
387     }
388     return null;
389   }
390 
391   /**
392    * Returns the first element that is associated with the specified name and attribute.
393    *
394    * @param element
395    *          the expanded name of the element
396    * @param attributeEName
397    *          the expanded attribute name
398    * @param attributeValue
399    *          the attribute value
400    * @return the first value
401    */
402   protected CatalogEntry getFirstValue(EName element, EName attributeEName, String attributeValue) {
403     List<CatalogEntry> elements = data.get(element);
404     if (elements != null) {
405       for (CatalogEntry entry : elements) {
406         String v = entry.getAttribute(attributeEName);
407         if (equal(attributeValue, v))
408           return entry;
409       }
410     }
411     return null;
412   }
413 
414   /**
415    * Returns the first value that is associated with the specified name and language.
416    *
417    * @param element
418    *          the expanded name of the element
419    * @param language
420    *          the language identifier or null to get only elements without <code>xml:lang</code> attribute
421    * @return the first value
422    */
423   protected CatalogEntry getFirstLocalizedValue(EName element, String language) {
424     return getFirstValue(element, XML_LANG_ATTR, language);
425   }
426 
427   /**
428    * Returns the first value that is associated with the specified name and language.
429    *
430    * @param element
431    *          the expanded name of the element
432    * @param type
433    *          the <code>xsi:type</code> value
434    * @return the element
435    */
436   protected CatalogEntry getFirstTypedValue(EName element, String type) {
437     return getFirstValue(element, XSI_TYPE_ATTR, type);
438   }
439 
440   /**
441    * Tests two objects for equality.
442    */
443   protected boolean equal(Object a, Object b) {
444     return (a == null && b == null) || (a != null && a.equals(b));
445   }
446 
447   /**
448    * Creates an xml document root and returns it.
449    *
450    * @return the document
451    * @throws ParserConfigurationException
452    *           If the xml parser environment is not correctly configured
453    */
454   protected Document newDocument() throws ParserConfigurationException {
455     DocumentBuilderFactory docBuilderFactory = XmlSafeParser.newDocumentBuilderFactory();
456     docBuilderFactory.setNamespaceAware(true);
457     DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
458     return docBuilder.newDocument();
459   }
460 
461   /**
462    * @see org.opencastproject.mediapackage.AbstractMediaPackageElement#toManifest(org.w3c.dom.Document,
463    *      org.opencastproject.mediapackage.MediaPackageSerializer)
464    */
465   @Override
466   public Node toManifest(Document document, MediaPackageSerializer serializer) throws MediaPackageException {
467     return super.toManifest(document, serializer);
468   }
469 
470   /**
471    * Get a prefix from {@link #bindings} but throw a {@link NamespaceBindingException} if none found.
472    */
473   protected String getPrefix(String namespaceURI) {
474     final String prefix = bindings.getPrefix(namespaceURI);
475     if (prefix != null) {
476       return prefix;
477     } else {
478       throw new NamespaceBindingException(format("Namespace URI %s is not bound to a prefix", namespaceURI));
479     }
480   }
481 
482   /**
483    * @see org.opencastproject.mediapackage.XMLCatalog#includeEmpty(boolean)
484    */
485   @Override
486   public
487   void includeEmpty(boolean includeEmpty) {
488     this.includeEmpty = includeEmpty;
489   }
490 
491   /**
492    * Transform an expanded name to a qualified name based on the registered binding.
493    *
494    * @param eName
495    *          the expanded name to transform
496    * @return the qualified name, e.g. <code>dcterms:title</code>
497    * @throws NamespaceBindingException
498    *           if the namespace name is not bound to a prefix
499    */
500   protected String toQName(EName eName) {
501     if (eName.hasNamespace()) {
502       return toQName(getPrefix(eName.getNamespaceURI()), eName.getLocalName());
503     } else {
504       return eName.getLocalName();
505     }
506   }
507 
508   /**
509    * Transform an qualified name consisting of prefix and local part to an expanded name, based on the registered
510    * binding.
511    *
512    * @param prefix
513    *          the prefix
514    * @param localName
515    *          the local part
516    * @return the expanded name
517    * @throws NamespaceBindingException
518    *           if the namespace name is not bound to a prefix
519    */
520   protected EName toEName(String prefix, String localName) {
521     return new EName(bindings.getNamespaceURI(prefix), localName);
522   }
523 
524   /**
525    * Transform a qualified name to an expanded name, based on the registered binding.
526    *
527    * @param qName
528    *          the qualified name, e.g. <code>dcterms:title</code> or <code>title</code>
529    * @return the expanded name
530    * @throws NamespaceBindingException
531    *           if the namespace name is not bound to a prefix
532    */
533   protected EName toEName(String qName) {
534     String[] parts = splitQName(qName);
535     return new EName(bindings.getNamespaceURI(parts[0]), parts[1]);
536   }
537 
538   /**
539    * Splits a QName into its parts.
540    *
541    * @param qName
542    *          the qname to split
543    * @return an array of prefix (0) and local part (1). The prefix is "" if the qname belongs to the default namespace.
544    */
545   private static String[] splitQName(String qName) {
546     final String[] parts = qName.split(":", 3);
547     switch (parts.length) {
548       case 1:
549         return new String[] { DEFAULT_NS_PREFIX, parts[0] };
550       case 2:
551         return parts;
552       default:
553         throw new IllegalArgumentException("Local name must not contain ':'");
554     }
555   }
556 
557   /**
558    * Returns a "prefixed name" consisting of namespace prefix and local name.
559    *
560    * @param prefix
561    *          the namespace prefix, may be <code>null</code>
562    * @param localName
563    *          the local name
564    * @return the "prefixed name" <code>prefix:localName</code>
565    */
566   private static String toQName(String prefix, String localName) {
567     final StringBuilder b = new StringBuilder();
568     if (prefix != null && !DEFAULT_NS_PREFIX.equals(prefix)) {
569       b.append(prefix);
570       b.append(":");
571     }
572     b.append(localName);
573     return b.toString();
574   }
575 
576   // --------------------------------------------------------------------------------------------
577 
578   private static final Map<EName, String> NO_ATTRIBUTES = new HashMap<>();
579 
580   CatalogEntry mkCatalogEntry(EName name, String value, Map<EName, String> attributes) {
581     return new CatalogEntry(name, value, attributes);
582   }
583 
584   /**
585    * Element representation.
586    */
587   public final class CatalogEntry implements XmlElement, Comparable<CatalogEntry>, Serializable {
588 
589     /** The serial version UID */
590     private static final long serialVersionUID = 7195298081966562710L;
591 
592     private final EName name;
593 
594     private final String value;
595 
596     /** The attributes of this element */
597     private final Map<EName, String> attributes;
598 
599     /**
600      * Creates a new catalog element representation with name, value and attributes.
601      *
602      * @param value
603      *          the element value
604      * @param attributes
605      *          the element attributes
606      */
607     public CatalogEntry(EName name, String value, Map<EName, String> attributes) {
608       this.name = name;
609       this.value = value;
610       this.attributes = new HashMap<>(attributes);
611     }
612 
613     /**
614      * Returns the qualified name of the entry as a string. The namespace of the entry has to be bound to a prefix for
615      * this method to succeed.
616      */
617     public String getQName() {
618       return toQName(name);
619     }
620 
621     /**
622      * Returns the expanded name of the entry.
623      */
624     public EName getEName() {
625       return name;
626     }
627 
628     /**
629      * Returns the element value.
630      *
631      * @return the value
632      */
633     public String getValue() {
634       return value;
635     }
636 
637     /**
638      * Returns <code>true</code> if the element contains attributes.
639      *
640      * @return <code>true</code> if the element contains attributes
641      */
642     public boolean hasAttributes() {
643       return attributes.size() > 0;
644     }
645 
646     /**
647      * Returns the element's attributes.
648      *
649      * @return the attributes
650      */
651     public Map<EName, String> getAttributes() {
652       return Collections.unmodifiableMap(attributes);
653     }
654 
655     /**
656      * Returns <code>true</code> if the element contains an attribute with the given name.
657      *
658      * @return <code>true</code> if the element contains the attribute
659      */
660     public boolean hasAttribute(EName name) {
661       return attributes.containsKey(name);
662     }
663 
664     /**
665      * Returns the attribute value for the given attribute.
666      *
667      * @return the attribute or null
668      */
669     public String getAttribute(EName name) {
670       return attributes.get(name);
671     }
672 
673     @Override
674     public int hashCode() {
675       return hash(name, value);
676     }
677 
678     @Override
679     public boolean equals(Object that) {
680       return (this == that) || (that instanceof CatalogEntry && eqFields((CatalogEntry) that));
681     }
682 
683     private boolean eqFields(CatalogEntry that) {
684       return this.compareTo(that) == 0;
685     }
686 
687     /**
688      * Returns the XML representation of this entry.
689      *
690      * @param document
691      *          the document
692      * @return the xml node
693      */
694     @Override
695     public Node toXml(Document document) {
696       Element node = document.createElement(toQName(name));
697       // Write prefix binding to document root element
698       bindNamespaceFor(document, name);
699 
700       List<EName> keySet = new ArrayList<>(attributes.keySet());
701       Collections.sort(keySet);
702       for (EName attrEName : keySet) {
703         String value = attributes.get(attrEName);
704         if (attrEName.hasNamespace()) {
705           // Write prefix binding to document root element
706           bindNamespaceFor(document, attrEName);
707           if (XSI_TYPE_ATTR.equals(attrEName)) {
708             // Special treatment for xsi:type attributes
709             try {
710               EName typeName = toEName(value);
711               bindNamespaceFor(document, typeName);
712             } catch (NamespaceBindingException ignore) {
713               // Type is either not a QName or its namespace is not bound.
714               // We decide to gently ignore those cases.
715             }
716           }
717         }
718         node.setAttribute(toQName(attrEName), value);
719       }
720       if (value != null) {
721         node.appendChild(document.createTextNode(value));
722       }
723       return node;
724     }
725 
726     /**
727      * Compare two catalog entries. Comparison order:
728      * - e_name
729      * - number of attributes (less come first)
730      * - attribute comparison (e_name -&gt; value)
731      */
732     @Override
733     public int compareTo(CatalogEntry o) {
734       int c = getEName().compareTo(o.getEName());
735       if (c != 0) {
736         return c;
737       }
738 
739       c = Integer.compare(attributes.size(), o.attributes.size());
740       if (c != 0) {
741         return c;
742       }
743 
744       // Sort attribute entries by attributeComparator
745       List<Entry<EName, String>> thisAttrs = attributes.entrySet().stream()
746           .sorted(attributeComparator)
747           .toList();
748 
749       List<Entry<EName, String>> otherAttrs = o.attributes.entrySet().stream()
750           .sorted(attributeComparator)
751           .toList();
752 
753       // Compare entries pairwise
754       for (int i = 0; i < thisAttrs.size(); i++) {
755         c = attributeComparator.compare(thisAttrs.get(i), otherAttrs.get(i));
756         if (c != 0) {
757           return c;
758         }
759       }
760 
761       return 0; // all equal
762     }
763 
764     /**
765      * Writes a namespace binding for catalog entry <code>name</code> to the documents root element.
766      * <code>xmlns:prefix="namespace"</code>
767      */
768     private void bindNamespaceFor(Document document, EName name) {
769       Element root = (Element) document.getFirstChild();
770       String namespace = name.getNamespaceURI();
771       // Do not bind the "xml" namespace. It is bound by default
772       if (!XML_NS_URI.equals(namespace)) {
773         root.setAttribute(XMLNS_ATTRIBUTE + ":" + XMLCatalogImpl.this.getPrefix(name.getNamespaceURI()),
774                 name.getNamespaceURI());
775       }
776     }
777 
778     @Override
779     public String toString() {
780       return value;
781     }
782   }
783 
784   static int doCompareTo(EName k1, String v1, EName k2, String v2) {
785     final int c = k1.compareTo(k2);
786     return c != 0 ? c : v1.compareTo(v2);
787   }
788 
789   private static final Comparator<Map.Entry<EName, String>> attributeComparator =
790       new Comparator<Map.Entry<EName, String>>() {
791         @Override public int compare(Entry<EName, String> o1, Entry<EName, String> o2) {
792           return doCompareTo(o1.getKey(), o1.getValue(), o2.getKey(), o2.getValue());
793         }
794       };
795 
796   private static final Comparator<CatalogEntry> catalogEntryComparator =
797       new Comparator<CatalogEntry>() {
798         @Override public int compare(CatalogEntry o1, CatalogEntry o2) {
799           return o1.compareTo(o2);
800         }
801       };
802 
803   // --------------------------------------------------------------------------------------------
804 
805   // --
806 
807   /**
808    * {@inheritDoc}
809    *
810    * @see org.opencastproject.mediapackage.XMLCatalog#toXml(java.io.OutputStream, boolean)
811    */
812   @Override
813   public void toXml(OutputStream out, boolean format) throws IOException {
814     try {
815       Document doc = this.toXml();
816       DOMImplementationRegistry reg = DOMImplementationRegistry.newInstance();
817       DOMImplementationLS impl = (DOMImplementationLS) reg.getDOMImplementation("LS");
818       LSSerializer serializer = impl.createLSSerializer();
819       serializer.getDomConfig().setParameter("format-pretty-print", format);
820       LSOutput output = impl.createLSOutput();
821       output.setByteStream(out);
822       serializer.write(doc, output);
823     } catch (ParserConfigurationException e) {
824       throw new IOException("unable to parse document");
825     } catch (TransformerException e) {
826       throw new IOException("unable to transform dom to a stream");
827     } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
828       throw new IOException("unable to serialize DOM");
829     }
830   }
831 
832   /**
833    * {@inheritDoc}
834    *
835    * @see org.opencastproject.mediapackage.XMLCatalog#toXmlString()
836    */
837   @Override
838   public String toXmlString() throws IOException {
839     ByteArrayOutputStream out = new ByteArrayOutputStream();
840     toXml(out, true);
841     return new String(out.toByteArray(), StandardCharsets.UTF_8);
842   }
843 
844 }