1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94 public abstract class XMLCatalogImpl extends CatalogImpl implements XMLCatalog {
95 private static final long serialVersionUID = -7580292199527168951L;
96
97
98 public static final EName XML_LANG_ATTR = new EName(XML_NS_URI, "lang");
99
100
101 public static final String XSI_NS_PREFIX = "xsi";
102
103
104 protected boolean includeEmpty = false;
105
106
107
108
109
110
111
112 public static final EName XSI_TYPE_ATTR = new EName(W3C_XML_SCHEMA_INSTANCE_NS_URI, "type");
113
114
115 protected final Map<EName, List<CatalogEntry>> data = new HashMap<>();
116
117
118 protected XmlNamespaceContext bindings;
119
120
121
122
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
139
140 protected void clear() {
141 data.clear();
142 }
143
144
145
146
147
148
149
150
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
162
163
164
165
166
167
168
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
181
182
183
184
185
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
198
199
200
201
202
203
204
205
206
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
227
228
229
230
231
232
233
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
251
252
253
254
255 private void addElement(CatalogEntry element) {
256
257
258
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
275
276
277
278
279 protected void removeElement(EName element) {
280 removeValues(element, null, true);
281 }
282
283
284
285
286
287
288
289
290
291
292 protected void removeLocalizedValues(EName element, String language) {
293 removeValues(element, language, false);
294 }
295
296
297
298
299
300
301
302
303
304
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
324
325
326
327
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
346
347
348
349
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
359
360
361
362
363
364
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
385
386
387
388
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
400
401
402
403
404
405
406
407
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
424
425
426
427
428
429
430
431 protected CatalogEntry getFirstLocalizedValue(EName element, String language) {
432 return getFirstValue(element, XML_LANG_ATTR, language);
433 }
434
435
436
437
438
439
440
441
442
443
444 protected CatalogEntry getFirstTypedValue(EName element, String type) {
445 return getFirstValue(element, XSI_TYPE_ATTR, type);
446 }
447
448
449
450
451 protected boolean equal(Object a, Object b) {
452 return (a == null && b == null) || (a != null && a.equals(b));
453 }
454
455
456
457
458
459
460
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
471
472
473 @Override
474 public Node toManifest(Document document, MediaPackageSerializer serializer) throws MediaPackageException {
475 return super.toManifest(document, serializer);
476 }
477
478
479
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
492
493 @Override
494 public void includeEmpty(boolean includeEmpty) {
495 this.includeEmpty = includeEmpty;
496 }
497
498
499
500
501
502
503
504
505
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
517
518
519
520
521
522
523
524
525
526
527 protected EName toEName(String prefix, String localName) {
528 return new EName(bindings.getNamespaceURI(prefix), localName);
529 }
530
531
532
533
534
535
536
537
538
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
547
548
549
550
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
566
567
568
569
570
571
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
593
594 public final class CatalogEntry implements XmlElement, Comparable<CatalogEntry>, Serializable {
595
596
597 private static final long serialVersionUID = 7195298081966562710L;
598
599 private final EName name;
600
601 private final String value;
602
603
604 private final Map<EName, String> attributes;
605
606
607
608
609
610
611
612
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
622
623
624 public String getQName() {
625 return toQName(name);
626 }
627
628
629
630
631 public EName getEName() {
632 return name;
633 }
634
635
636
637
638
639
640 public String getValue() {
641 return value;
642 }
643
644
645
646
647
648
649 public boolean hasAttributes() {
650 return attributes.size() > 0;
651 }
652
653
654
655
656
657
658 public Map<EName, String> getAttributes() {
659 return Collections.unmodifiableMap(attributes);
660 }
661
662
663
664
665
666
667 public boolean hasAttribute(EName name) {
668 return attributes.containsKey(name);
669 }
670
671
672
673
674
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
696
697
698
699
700
701 @Override
702 public Node toXml(Document document) {
703 Element node = document.createElement(toQName(name));
704
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
713 bindNamespaceFor(document, attrEName);
714 if (XSI_TYPE_ATTR.equals(attrEName)) {
715
716 try {
717 EName typeName = toEName(value);
718 bindNamespaceFor(document, typeName);
719 } catch (NamespaceBindingException ignore) {
720
721
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
735
736
737
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
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
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;
769 }
770
771
772
773
774
775 private void bindNamespaceFor(Document document, EName name) {
776 Element root = (Element) document.getFirstChild();
777 String namespace = name.getNamespaceURI();
778
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
816
817
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
841
842
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 }