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 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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99 public abstract class XMLCatalogImpl extends CatalogImpl implements XMLCatalog {
100 private static final long serialVersionUID = -7580292199527168951L;
101
102
103 public static final EName XML_LANG_ATTR = new EName(XML_NS_URI, "lang");
104
105
106 public static final String XSI_NS_PREFIX = "xsi";
107
108
109 protected boolean includeEmpty = false;
110
111
112
113
114
115
116
117 public static final EName XSI_TYPE_ATTR = new EName(W3C_XML_SCHEMA_INSTANCE_NS_URI, "type");
118
119
120 protected final Map<EName, List<CatalogEntry>> data = new HashMap<>();
121
122
123 protected XmlNamespaceContext bindings;
124
125
126
127
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
144
145 protected void clear() {
146 data.clear();
147 }
148
149
150
151
152
153
154
155
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
166
167
168
169
170
171
172
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
185
186
187
188
189
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
202
203
204
205
206
207
208
209
210
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
228
229
230
231
232
233
234
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
251
252
253
254
255 private void addElement(CatalogEntry element) {
256
257
258
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
273
274
275
276
277 protected void removeElement(EName element) {
278 removeValues(element, null, true);
279 }
280
281
282
283
284
285
286
287
288
289
290 protected void removeLocalizedValues(EName element, String language) {
291 removeValues(element, language, false);
292 }
293
294
295
296
297
298
299
300
301
302
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
322
323
324
325
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
344
345
346
347
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
357
358
359
360
361
362
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
383
384
385
386
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
398
399
400
401
402
403
404
405
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
421
422
423
424
425
426
427
428 protected CatalogEntry getFirstLocalizedValue(EName element, String language) {
429 return getFirstValue(element, XML_LANG_ATTR, language);
430 }
431
432
433
434
435
436
437
438
439
440
441 protected CatalogEntry getFirstTypedValue(EName element, String type) {
442 return getFirstValue(element, XSI_TYPE_ATTR, type);
443 }
444
445
446
447
448 protected boolean equal(Object a, Object b) {
449 return (a == null && b == null) || (a != null && a.equals(b));
450 }
451
452
453
454
455
456
457
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
468
469
470 @Override
471 public Node toManifest(Document document, MediaPackageSerializer serializer) throws MediaPackageException {
472 return super.toManifest(document, serializer);
473 }
474
475
476
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
489
490 @Override
491 public
492 void includeEmpty(boolean includeEmpty) {
493 this.includeEmpty = includeEmpty;
494 }
495
496
497
498
499
500
501
502
503
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
515
516
517
518
519
520
521
522
523
524
525 protected EName toEName(String prefix, String localName) {
526 return new EName(bindings.getNamespaceURI(prefix), localName);
527 }
528
529
530
531
532
533
534
535
536
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
545
546
547
548
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
564
565
566
567
568
569
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
591
592 public final class CatalogEntry implements XmlElement, Comparable<CatalogEntry>, Serializable {
593
594
595 private static final long serialVersionUID = 7195298081966562710L;
596
597 private final EName name;
598
599 private final String value;
600
601
602 private final Map<EName, String> attributes;
603
604
605
606
607
608
609
610
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
620
621
622 public String getQName() {
623 return toQName(name);
624 }
625
626
627
628
629 public EName getEName() {
630 return name;
631 }
632
633
634
635
636
637
638 public String getValue() {
639 return value;
640 }
641
642
643
644
645
646
647 public boolean hasAttributes() {
648 return attributes.size() > 0;
649 }
650
651
652
653
654
655
656 public Map<EName, String> getAttributes() {
657 return Collections.unmodifiableMap(attributes);
658 }
659
660
661
662
663
664
665 public boolean hasAttribute(EName name) {
666 return attributes.containsKey(name);
667 }
668
669
670
671
672
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
694
695
696
697
698
699 @Override
700 public Node toXml(Document document) {
701 Element node = document.createElement(toQName(name));
702
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
711 bindNamespaceFor(document, attrEName);
712 if (XSI_TYPE_ATTR.equals(attrEName)) {
713
714 try {
715 EName typeName = toEName(value);
716 bindNamespaceFor(document, typeName);
717 } catch (NamespaceBindingException ignore) {
718
719
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
733
734
735
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 {
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
763
764
765 private void bindNamespaceFor(Document document, EName name) {
766 Element root = (Element) document.getFirstChild();
767 String namespace = name.getNamespaceURI();
768
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
806
807
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
831
832
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 }