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 addElement(new CatalogEntry(element, value, NO_ATTRIBUTES));
157 }
158
159
160
161
162
163
164
165
166
167
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
180
181
182
183
184
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
197
198
199
200
201
202
203
204
205
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
223
224
225
226
227
228
229
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
246
247
248
249
250 private void addElement(CatalogEntry element) {
251
252
253
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
268
269
270
271
272 protected void removeElement(EName element) {
273 removeValues(element, null, true);
274 }
275
276
277
278
279
280
281
282
283
284
285 protected void removeLocalizedValues(EName element, String language) {
286 removeValues(element, language, false);
287 }
288
289
290
291
292
293
294
295
296
297
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
317
318
319
320
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
339
340
341
342
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
352
353
354
355
356
357
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
378
379
380
381
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
393
394
395
396
397
398
399
400
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
416
417
418
419
420
421
422
423 protected CatalogEntry getFirstLocalizedValue(EName element, String language) {
424 return getFirstValue(element, XML_LANG_ATTR, language);
425 }
426
427
428
429
430
431
432
433
434
435
436 protected CatalogEntry getFirstTypedValue(EName element, String type) {
437 return getFirstValue(element, XSI_TYPE_ATTR, type);
438 }
439
440
441
442
443 protected boolean equal(Object a, Object b) {
444 return (a == null && b == null) || (a != null && a.equals(b));
445 }
446
447
448
449
450
451
452
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
463
464
465 @Override
466 public Node toManifest(Document document, MediaPackageSerializer serializer) throws MediaPackageException {
467 return super.toManifest(document, serializer);
468 }
469
470
471
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
484
485 @Override
486 public
487 void includeEmpty(boolean includeEmpty) {
488 this.includeEmpty = includeEmpty;
489 }
490
491
492
493
494
495
496
497
498
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
510
511
512
513
514
515
516
517
518
519
520 protected EName toEName(String prefix, String localName) {
521 return new EName(bindings.getNamespaceURI(prefix), localName);
522 }
523
524
525
526
527
528
529
530
531
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
540
541
542
543
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
559
560
561
562
563
564
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
586
587 public final class CatalogEntry implements XmlElement, Comparable<CatalogEntry>, Serializable {
588
589
590 private static final long serialVersionUID = 7195298081966562710L;
591
592 private final EName name;
593
594 private final String value;
595
596
597 private final Map<EName, String> attributes;
598
599
600
601
602
603
604
605
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
615
616
617 public String getQName() {
618 return toQName(name);
619 }
620
621
622
623
624 public EName getEName() {
625 return name;
626 }
627
628
629
630
631
632
633 public String getValue() {
634 return value;
635 }
636
637
638
639
640
641
642 public boolean hasAttributes() {
643 return attributes.size() > 0;
644 }
645
646
647
648
649
650
651 public Map<EName, String> getAttributes() {
652 return Collections.unmodifiableMap(attributes);
653 }
654
655
656
657
658
659
660 public boolean hasAttribute(EName name) {
661 return attributes.containsKey(name);
662 }
663
664
665
666
667
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
689
690
691
692
693
694 @Override
695 public Node toXml(Document document) {
696 Element node = document.createElement(toQName(name));
697
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
706 bindNamespaceFor(document, attrEName);
707 if (XSI_TYPE_ATTR.equals(attrEName)) {
708
709 try {
710 EName typeName = toEName(value);
711 bindNamespaceFor(document, typeName);
712 } catch (NamespaceBindingException ignore) {
713
714
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
728
729
730
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
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
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;
762 }
763
764
765
766
767
768 private void bindNamespaceFor(Document document, EName name) {
769 Element root = (Element) document.getFirstChild();
770 String namespace = name.getNamespaceURI();
771
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
809
810
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
834
835
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 }