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  
23  package org.opencastproject.mediapackage;
24  
25  import org.opencastproject.mediapackage.MediaPackageElement.Type;
26  import org.opencastproject.mediapackage.identifier.Id;
27  import org.opencastproject.mediapackage.identifier.IdImpl;
28  import org.opencastproject.util.DateTimeSupport;
29  import org.opencastproject.util.IoSupport;
30  import org.opencastproject.util.XmlSafeParser;
31  
32  import org.apache.commons.io.IOUtils;
33  import org.apache.commons.lang3.StringUtils;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  import org.w3c.dom.Node;
37  import org.w3c.dom.bootstrap.DOMImplementationRegistry;
38  import org.w3c.dom.ls.DOMImplementationLS;
39  import org.w3c.dom.ls.LSOutput;
40  import org.w3c.dom.ls.LSSerializer;
41  import org.xml.sax.SAXException;
42  
43  import java.io.ByteArrayInputStream;
44  import java.io.ByteArrayOutputStream;
45  import java.io.IOException;
46  import java.io.InputStream;
47  import java.net.URI;
48  import java.util.ArrayList;
49  import java.util.Arrays;
50  import java.util.Collection;
51  import java.util.Date;
52  import java.util.HashSet;
53  import java.util.List;
54  import java.util.Map;
55  import java.util.Set;
56  import java.util.TreeSet;
57  
58  import javax.xml.bind.JAXBContext;
59  import javax.xml.bind.JAXBException;
60  import javax.xml.bind.Unmarshaller;
61  import javax.xml.bind.annotation.XmlAccessType;
62  import javax.xml.bind.annotation.XmlAccessorType;
63  import javax.xml.bind.annotation.XmlAttribute;
64  import javax.xml.bind.annotation.XmlElement;
65  import javax.xml.bind.annotation.XmlElementWrapper;
66  import javax.xml.bind.annotation.XmlRootElement;
67  import javax.xml.bind.annotation.XmlType;
68  import javax.xml.bind.annotation.adapters.XmlAdapter;
69  import javax.xml.transform.stream.StreamSource;
70  
71  /**
72   * Default implementation for a media media package.
73   */
74  @XmlType(name = "mediapackage", namespace = "http://mediapackage.opencastproject.org", propOrder = { "title", "series",
75          "seriesTitle", "creators", "contributors", "subjects", "license", "language", "tracks", "catalogs",
76          "attachments", "publications" })
77  @XmlRootElement(name = "mediapackage", namespace = "http://mediapackage.opencastproject.org")
78  @XmlAccessorType(XmlAccessType.NONE)
79  public final class MediaPackageImpl implements MediaPackage {
80  
81    /** the logging facility provided by log4j */
82    private static final Logger logger = LoggerFactory.getLogger(MediaPackageImpl.class.getName());
83  
84    /**
85     * The prefix indicating that a tag should be excluded from a search for elements using
86     * {@link #getElementsByTags(Collection)}
87     */
88    public static final String NEGATE_TAG_PREFIX = "-";
89  
90    /** Context for serializing and deserializing */
91    static final JAXBContext context;
92  
93    /** The media package element builder, may remain <code>null</code> */
94    private MediaPackageElementBuilder mediaPackageElementBuilder = null;
95  
96    @XmlElement(name = "title")
97    private String title = null;
98  
99    @XmlElement(name = "seriestitle")
100   private String seriesTitle = null;
101 
102   @XmlElement(name = "language")
103   private String language = null;
104 
105   @XmlElement(name = "series")
106   private String series = null;
107 
108   @XmlElement(name = "license")
109   private String license = null;
110 
111   @XmlElementWrapper(name = "creators")
112   @XmlElement(name = "creator")
113   private Set<String> creators = null;
114 
115   @XmlElementWrapper(name = "contributors")
116   @XmlElement(name = "contributor")
117   private Set<String> contributors = null;
118 
119   @XmlElementWrapper(name = "subjects")
120   @XmlElement(name = "subject")
121   private Set<String> subjects = null;
122 
123   /** The media package's identifier */
124   private Id identifier = null;
125 
126   /** The start date and time */
127   private long startTime = 0L;
128 
129   /** The media package duration */
130   private Long duration = null;
131 
132   /** The media package's other (uncategorized) files */
133   private final List<MediaPackageElement> elements = new ArrayList<MediaPackageElement>();
134 
135   /** Number of tracks */
136   private int tracks = 0;
137 
138   /** Number of metadata catalogs */
139   private int catalogs = 0;
140 
141   /** Number of attachments */
142   private int attachments = 0;
143 
144   /** Numer of unclassified elements */
145   private int others = 0;
146 
147   static {
148     try {
149       context = JAXBContext.newInstance("org.opencastproject.mediapackage", MediaPackageImpl.class.getClassLoader());
150     } catch (JAXBException e) {
151       throw new RuntimeException(e);
152     }
153   }
154 
155   /**
156    * Creates a media package object.
157    */
158   MediaPackageImpl() {
159     this(IdImpl.fromUUID());
160   }
161 
162   /**
163    * Creates a media package object with the media package identifier.
164    *
165    * @param id
166    *          the media package identifier
167    */
168   MediaPackageImpl(Id id) {
169     this.identifier = id;
170   }
171 
172   /**
173    * {@inheritDoc}
174    *
175    * @see org.opencastproject.mediapackage.MediaPackage#getIdentifier()
176    */
177   @XmlAttribute(name = "id")
178   @Override
179   public Id getIdentifier() {
180     return identifier;
181   }
182 
183   /**
184    * {@inheritDoc}
185    *
186    * @see org.opencastproject.mediapackage.MediaPackage#setIdentifier(org.opencastproject.mediapackage.identifier.Id)
187    */
188   @Override
189   public void setIdentifier(Id identifier) {
190     this.identifier = identifier;
191   }
192 
193   /**
194    * {@inheritDoc}
195    *
196    * @see org.opencastproject.mediapackage.MediaPackage#getDuration()
197    */
198   @XmlAttribute(name = "duration")
199   @Override
200   public Long getDuration() {
201     if (duration == null && hasTracks()) {
202       recalculateDuration();
203     }
204     return duration;
205   }
206 
207   /**
208    * The duration of the media package is the duration of the longest track
209    */
210   private void recalculateDuration() {
211 
212     duration = null;
213     for (Track t : getTracks()) {
214       if (t.getDuration() != null) {
215         if (duration == null || duration < t.getDuration())
216           duration = t.getDuration();
217       }
218     }
219   }
220 
221   /**
222    * {@inheritDoc}
223    *
224    * @see org.opencastproject.mediapackage.MediaPackage#setDuration(Long)
225    */
226   @Override
227   public void setDuration(Long duration) throws IllegalStateException {
228     if (hasTracks())
229       throw new IllegalStateException(
230               "The duration is determined by the length of the tracks and cannot be set manually");
231     this.duration = duration;
232   }
233 
234   /**
235    * {@inheritDoc}
236    *
237    * @see org.opencastproject.mediapackage.MediaPackage#getDate()
238    */
239   @Override
240   public Date getDate() {
241     return new Date(startTime);
242   }
243 
244   /**
245    * Returns the recording time in utc format.
246    *
247    * @return the recording time
248    */
249   @XmlAttribute(name = "start")
250   public String getStartDateAsString() {
251     if (startTime == 0)
252       return null;
253     return DateTimeSupport.toUTC(startTime);
254   }
255 
256   /**
257    * Sets the date and time of recording in utc format.
258    *
259    * @param startTime
260    *          the start time
261    */
262   public void setStartDateAsString(String startTime) {
263     if (startTime != null && !"0".equals(startTime) && !startTime.isEmpty()) {
264       try {
265         this.startTime = DateTimeSupport.fromUTC(startTime);
266       } catch (Exception e) {
267         logger.info("Unable to parse start time {}", startTime);
268       }
269     } else {
270       this.startTime = 0;
271     }
272   }
273 
274   /**
275    * {@inheritDoc}
276    *
277    * @see org.opencastproject.mediapackage.MediaPackage#elements()
278    */
279   @Override
280   public Iterable<MediaPackageElement> elements() {
281     return Arrays.asList(getElements());
282   }
283 
284   /**
285    * {@inheritDoc}
286    *
287    * @see org.opencastproject.mediapackage.MediaPackage#getElements()
288    */
289   @Override
290   public MediaPackageElement[] getElements() {
291     return elements.toArray(new MediaPackageElement[elements.size()]);
292   }
293 
294   /**
295    * {@inheritDoc}
296    *
297    * @see org.opencastproject.mediapackage.MediaPackage#getElementByReference(org.opencastproject.mediapackage.MediaPackageReference)
298    */
299   @Override
300   public MediaPackageElement getElementByReference(MediaPackageReference reference) {
301     for (MediaPackageElement e : this.elements) {
302       if (!reference.getType().equalsIgnoreCase(e.getElementType().toString()))
303         continue;
304       if (reference.getIdentifier().equals(e.getIdentifier()))
305         return e;
306     }
307     return null;
308   }
309 
310   /**
311    * @see org.opencastproject.mediapackage.MediaPackage#getElementById(java.lang.String)
312    */
313   @Override
314   public MediaPackageElement getElementById(String id) {
315     for (MediaPackageElement element : getElements()) {
316       if (id.equals(element.getIdentifier()))
317         return element;
318     }
319     return null;
320   }
321 
322   /**
323    * {@inheritDoc}
324    *
325    * @see org.opencastproject.mediapackage.MediaPackage#getElementsByTags(java.util.Collection)
326    */
327   @Override
328   public MediaPackageElement[] getElementsByTags(Collection<String> tags) {
329     if (tags == null || tags.isEmpty())
330       return getElements();
331     Set<String> keep = new HashSet<String>();
332     Set<String> lose = new HashSet<String>();
333     for (String tag : tags) {
334       if (StringUtils.isBlank(tag))
335         continue;
336       if (tag.startsWith(NEGATE_TAG_PREFIX)) {
337         lose.add(tag.substring(NEGATE_TAG_PREFIX.length()));
338       } else {
339         keep.add(tag);
340       }
341     }
342     List<MediaPackageElement> result = new ArrayList<>();
343     for (MediaPackageElement element : getElements()) {
344       boolean add = false;
345       for (String elementTag : element.getTags()) {
346         if (lose.contains(elementTag)) {
347           add = false;
348           break;
349         } else if (keep.contains(elementTag)) {
350           add = true;
351         }
352       }
353       if (add) {
354         result.add(element);
355       }
356     }
357     return result.toArray(new MediaPackageElement[result.size()]);
358   }
359 
360   /**
361    * {@inheritDoc}
362    *
363    * @see org.opencastproject.mediapackage.MediaPackage#getCatalogsByTags(java.util.Collection)
364    */
365   @Override
366   public Catalog[] getCatalogsByTags(Collection<String> tags) {
367     MediaPackageElement[] matchingElements = getElementsByTags(tags);
368     List<Catalog> catalogs = new ArrayList<>();
369     for (MediaPackageElement element : matchingElements) {
370       if (Catalog.TYPE.equals(element.getElementType())) {
371         catalogs.add((Catalog) element);
372       }
373     }
374     return catalogs.toArray(new Catalog[catalogs.size()]);
375   }
376 
377   /**
378    * {@inheritDoc}
379    *
380    * @see org.opencastproject.mediapackage.MediaPackage#getTracksByTags(java.util.Collection)
381    */
382   @Override
383   public Track[] getTracksByTags(Collection<String> tags) {
384     MediaPackageElement[] matchingElements = getElementsByTags(tags);
385     List<Track> tracks = new ArrayList<>();
386     for (MediaPackageElement element : matchingElements) {
387       if (Track.TYPE.equals(element.getElementType())) {
388         tracks.add((Track) element);
389       }
390     }
391     return tracks.toArray(new Track[tracks.size()]);
392   }
393 
394   /**
395    * {@inheritDoc}
396    *
397    * @see org.opencastproject.mediapackage.MediaPackage#getElementsByFlavor(org.opencastproject.mediapackage.MediaPackageElementFlavor)
398    */
399   @Override
400   public MediaPackageElement[] getElementsByFlavor(MediaPackageElementFlavor flavor) {
401     if (flavor == null)
402       throw new IllegalArgumentException("Flavor cannot be null");
403 
404     List<MediaPackageElement> elements = new ArrayList<>();
405     for (MediaPackageElement element : getElements()) {
406       if (flavor.matches(element.getFlavor()))
407         elements.add(element);
408     }
409     return elements.toArray(new MediaPackageElement[elements.size()]);
410   }
411 
412   /**
413    * @see org.opencastproject.mediapackage.MediaPackage#contains(org.opencastproject.mediapackage.MediaPackageElement)
414    */
415   @Override
416   public boolean contains(MediaPackageElement element) {
417     if (element == null)
418       throw new IllegalArgumentException("Media package element must not be null");
419     return (elements.contains(element));
420   }
421 
422   /**
423    * Returns <code>true</code> if the media package contains an element with the specified identifier.
424    *
425    * @param identifier
426    *          the identifier
427    * @return <code>true</code> if the media package contains an element with this identifier
428    */
429   boolean contains(String identifier) {
430     for (MediaPackageElement element : getElements()) {
431       if (element.getIdentifier().equals(identifier))
432         return true;
433     }
434     return false;
435   }
436 
437   /**
438    * @see org.opencastproject.mediapackage.MediaPackage#add(org.opencastproject.mediapackage.Catalog)
439    */
440   @Override
441   public void add(Catalog catalog) {
442     integrateCatalog(catalog);
443     addInternal(catalog);
444   }
445 
446   /**
447    * @see org.opencastproject.mediapackage.MediaPackage#add(org.opencastproject.mediapackage.Track)
448    */
449   @Override
450   public void add(Track track) {
451     integrateTrack(track);
452     addInternal(track);
453   }
454 
455   /**
456    * @see org.opencastproject.mediapackage.MediaPackage#add(org.opencastproject.mediapackage.Attachment)
457    */
458   @Override
459   public void add(Attachment attachment) {
460     integrateAttachment(attachment);
461     addInternal(attachment);
462   }
463 
464   /**
465    * @see org.opencastproject.mediapackage.MediaPackage#getCatalog(java.lang.String)
466    */
467   @Override
468   public Catalog getCatalog(String catalogId) {
469     synchronized (elements) {
470       for (MediaPackageElement e : elements) {
471         if (e.getIdentifier().equals(catalogId) && e instanceof Catalog)
472           return (Catalog) e;
473       }
474     }
475     return null;
476   }
477 
478   /**
479    * @see org.opencastproject.mediapackage.MediaPackage#getCatalogs()
480    */
481   @XmlElementWrapper(name = "metadata")
482   @XmlElement(name = "catalog")
483   @Override
484   public Catalog[] getCatalogs() {
485     Collection<Catalog> catalogs = loadCatalogs();
486     return catalogs.toArray(new Catalog[catalogs.size()]);
487   }
488 
489   void setCatalogs(Catalog[] catalogs) {
490     List<Catalog> newCatalogs = Arrays.asList(catalogs);
491     List<Catalog> oldCatalogs = Arrays.asList(getCatalogs());
492     // remove any catalogs not in this array
493     for (Catalog existing : oldCatalogs) {
494       if (!newCatalogs.contains(existing)) {
495         remove(existing);
496       }
497     }
498     for (Catalog newCatalog : newCatalogs) {
499       if (!oldCatalogs.contains(newCatalog)) {
500         add(newCatalog);
501       }
502     }
503   }
504 
505   /**
506    * @see org.opencastproject.mediapackage.MediaPackage#getCatalogs(MediaPackageElementFlavor)
507    */
508   @Override
509   public Catalog[] getCatalogs(MediaPackageElementFlavor flavor) {
510     if (flavor == null)
511       throw new IllegalArgumentException("Unable to filter by null criterion");
512 
513     // Go through catalogs and remove those that don't match
514     Collection<Catalog> catalogs = loadCatalogs();
515     List<Catalog> candidates = new ArrayList<>(catalogs);
516     for (Catalog c : catalogs) {
517       if (c.getFlavor() == null || !c.getFlavor().matches(flavor)) {
518         candidates.remove(c);
519       }
520     }
521     return candidates.toArray(new Catalog[0]);
522   }
523 
524   /**
525    * @see org.opencastproject.mediapackage.MediaPackage#getCatalogs(org.opencastproject.mediapackage.MediaPackageReference)
526    */
527   @Override
528   public Catalog[] getCatalogs(MediaPackageReference reference) {
529     return getCatalogs(reference, false);
530   }
531 
532   private Catalog[] getCatalogs(MediaPackageReference reference, boolean includeDerived) {
533     if (reference == null)
534       throw new IllegalArgumentException("Unable to filter by null reference");
535 
536     // Go through catalogs and remove those that don't match
537     Collection<Catalog> catalogs = loadCatalogs();
538     List<Catalog> candidates = new ArrayList<>(catalogs);
539     for (Catalog c : catalogs) {
540       MediaPackageReference r = c.getReference();
541       if (!reference.matches(r)) {
542         boolean indirectHit = false;
543 
544         // Create a reference that will match regardless of properties
545         MediaPackageReference elementRef = new MediaPackageReferenceImpl(reference.getType(), reference.getIdentifier());
546 
547         // Try to find a derived match if possible
548         while (includeDerived && r != null) {
549           if (r.matches(elementRef)) {
550             indirectHit = true;
551             break;
552           }
553           r = getElement(r).getReference();
554         }
555 
556         if (!indirectHit)
557           candidates.remove(c);
558       }
559     }
560 
561     return candidates.toArray(new Catalog[candidates.size()]);
562   }
563 
564   /**
565    * @see org.opencastproject.mediapackage.MediaPackage#getCatalogs(org.opencastproject.mediapackage.MediaPackageElementFlavor,
566    *      org.opencastproject.mediapackage.MediaPackageReference)
567    */
568   @Override
569   public Catalog[] getCatalogs(MediaPackageElementFlavor flavor, MediaPackageReference reference) {
570     if (flavor == null)
571       throw new IllegalArgumentException("Unable to filter by null criterion");
572     if (reference == null)
573       throw new IllegalArgumentException("Unable to filter by null reference");
574 
575     // Go through catalogs and remove those that don't match
576     Collection<Catalog> catalogs = loadCatalogs();
577     List<Catalog> candidates = new ArrayList<>(catalogs);
578     for (Catalog c : catalogs) {
579       if (!flavor.equals(c.getFlavor()) || (c.getReference() != null && !c.getReference().matches(reference))) {
580         candidates.remove(c);
581       }
582     }
583     return candidates.toArray(new Catalog[candidates.size()]);
584   }
585 
586   /**
587    * {@inheritDoc}
588    *
589    * @see org.opencastproject.mediapackage.MediaPackage#getTrack(java.lang.String)
590    */
591   @Override
592   public Track getTrack(String trackId) {
593     synchronized (elements) {
594       for (MediaPackageElement e : elements) {
595         if (e.getIdentifier().equals(trackId) && e instanceof Track)
596           return (Track) e;
597       }
598     }
599     return null;
600   }
601 
602   /**
603    * {@inheritDoc}
604    *
605    * @see org.opencastproject.mediapackage.MediaPackage#getTracks()
606    */
607   @XmlElementWrapper(name = "media")
608   @XmlElement(name = "track")
609   @Override
610   public Track[] getTracks() {
611     Collection<Track> tracks = loadTracks();
612     return tracks.toArray(new Track[tracks.size()]);
613   }
614 
615   void setTracks(Track[] tracks) {
616     List<Track> newTracks = Arrays.asList(tracks);
617     List<Track> oldTracks = Arrays.asList(getTracks());
618     // remove any catalogs not in this array
619     for (Track existing : oldTracks) {
620       if (!newTracks.contains(existing)) {
621         remove(existing);
622       }
623     }
624     for (Track newTrack : newTracks) {
625       if (!oldTracks.contains(newTrack)) {
626         add(newTrack);
627       }
628     }
629   }
630 
631   /**
632    * {@inheritDoc}
633    *
634    * @see org.opencastproject.mediapackage.MediaPackage#getTracksByTag(java.lang.String)
635    */
636   @Override
637   public Track[] getTracksByTag(String tag) {
638     List<Track> result = new ArrayList<>();
639     synchronized (elements) {
640       for (MediaPackageElement e : elements) {
641         if (e instanceof Track && e.containsTag(tag))
642           result.add((Track) e);
643       }
644     }
645     return result.toArray(new Track[result.size()]);
646   }
647 
648   /**
649    * {@inheritDoc}
650    *
651    * @see org.opencastproject.mediapackage.MediaPackage#getTracks(org.opencastproject.mediapackage.MediaPackageElementFlavor)
652    */
653   @Override
654   public Track[] getTracks(MediaPackageElementFlavor flavor) {
655     if (flavor == null)
656       throw new IllegalArgumentException("Unable to filter by null criterion");
657 
658     // Go through tracks and remove those that don't match
659     Collection<Track> tracks = loadTracks();
660     List<Track> candidates = new ArrayList<>(tracks);
661     for (Track a : tracks) {
662       if (a.getFlavor() == null || !a.getFlavor().matches(flavor)) {
663         candidates.remove(a);
664       }
665     }
666     return candidates.toArray(new Track[candidates.size()]);
667   }
668 
669   /**
670    * {@inheritDoc}
671    *
672    * @see org.opencastproject.mediapackage.MediaPackage#hasTracks()
673    */
674   @Override
675   public boolean hasTracks() {
676     synchronized (elements) {
677       for (MediaPackageElement e : elements) {
678         if (e instanceof Track)
679           return true;
680       }
681     }
682     return false;
683   }
684 
685   /**
686    * {@inheritDoc}
687    *
688    * @see org.opencastproject.mediapackage.MediaPackage#getUnclassifiedElements()
689    */
690   @Override
691   public MediaPackageElement[] getUnclassifiedElements() {
692     return getUnclassifiedElements(null);
693   }
694 
695   private MediaPackageElement[] getUnclassifiedElements(MediaPackageElementFlavor flavor) {
696     List<MediaPackageElement> unclassifieds = new ArrayList<>();
697     synchronized (elements) {
698       for (MediaPackageElement e : elements) {
699         if (!(e instanceof Attachment) && !(e instanceof Catalog) && !(e instanceof Track)) {
700           if (flavor == null || flavor.equals(e.getFlavor())) {
701             unclassifieds.add(e);
702           }
703         }
704       }
705     }
706     return unclassifieds.toArray(new MediaPackageElement[unclassifieds.size()]);
707   }
708 
709   /**
710    * {@inheritDoc}
711    *
712    * @see org.opencastproject.mediapackage.MediaPackage#getAttachment(java.lang.String)
713    */
714   @Override
715   public Attachment getAttachment(String attachmentId) {
716     synchronized (elements) {
717       for (MediaPackageElement e : elements) {
718         if (e.getIdentifier().equals(attachmentId) && e instanceof Attachment)
719           return (Attachment) e;
720       }
721     }
722     return null;
723   }
724 
725   /**
726    * {@inheritDoc}
727    *
728    * @see org.opencastproject.mediapackage.MediaPackage#getAttachments()
729    */
730   @XmlElementWrapper(name = "attachments")
731   @XmlElement(name = "attachment")
732   @Override
733   public Attachment[] getAttachments() {
734     Collection<Attachment> attachments = loadAttachments();
735     return attachments.toArray(new Attachment[attachments.size()]);
736   }
737 
738   void setAttachments(Attachment[] catalogs) {
739     List<Attachment> newAttachments = Arrays.asList(catalogs);
740     List<Attachment> oldAttachments = Arrays.asList(getAttachments());
741     // remove any catalogs not in this array
742     for (Attachment existing : oldAttachments) {
743       if (!newAttachments.contains(existing)) {
744         remove(existing);
745       }
746     }
747     for (Attachment newAttachment : newAttachments) {
748       if (!oldAttachments.contains(newAttachment)) {
749         add(newAttachment);
750       }
751     }
752   }
753 
754   /**
755    * {@inheritDoc}
756    *
757    * @see org.opencastproject.mediapackage.MediaPackage#getAttachments(org.opencastproject.mediapackage.MediaPackageElementFlavor)
758    */
759   @Override
760   public Attachment[] getAttachments(MediaPackageElementFlavor flavor) {
761     if (flavor == null)
762       throw new IllegalArgumentException("Unable to filter by null criterion");
763 
764     // Go through attachments and remove those that don't match
765     Collection<Attachment> attachments = loadAttachments();
766     List<Attachment> candidates = new ArrayList<>(attachments);
767     for (Attachment a : attachments) {
768       if (a.getFlavor() == null || !a.getFlavor().matches(flavor)) {
769         candidates.remove(a);
770       }
771     }
772     return candidates.toArray(new Attachment[candidates.size()]);
773   }
774 
775   /**
776    * {@inheritDoc}
777    *
778    * @see org.opencastproject.mediapackage.MediaPackage#getAttachments()
779    */
780   @XmlElementWrapper(name = "publications")
781   @XmlElement(name = "publication")
782   @Override
783   public Publication[] getPublications() {
784 //    return elements.stream()
785 //        .map(presentations::apply)
786 //        .flatMap(List::stream)
787 //        .toArray(Publication[]::new);
788     return elements.stream()
789         .filter(Publication.class::isInstance)
790         .map(Publication.class::cast)
791         .toArray(Publication[]::new);
792   }
793 
794   void setPublications(Publication[] publications) {
795     List<Publication> newPublications = Arrays.asList(publications);
796     List<Publication> oldPublications = Arrays.asList(getPublications());
797     for (Publication oldp : oldPublications) {
798       if (!newPublications.contains(oldp)) {
799         remove(oldp);
800       }
801     }
802     for (Publication newp : newPublications) {
803       if (!oldPublications.contains(newp)) {
804         add(newp);
805       }
806     }
807   }
808 
809   /**
810    * {@inheritDoc}
811    *
812    * @see org.opencastproject.mediapackage.MediaPackage#removeElementById(java.lang.String)
813    */
814   @Override
815   public MediaPackageElement removeElementById(String id) {
816     MediaPackageElement element = getElementById(id);
817     if (element == null)
818       return null;
819     remove(element);
820     return element;
821   }
822 
823   /**
824    * {@inheritDoc}
825    *
826    * @see org.opencastproject.mediapackage.MediaPackage#remove(org.opencastproject.mediapackage.MediaPackageElement)
827    */
828   @Override
829   public void remove(MediaPackageElement element) {
830     removeElement(element);
831   }
832 
833   /**
834    * {@inheritDoc}
835    *
836    * @see org.opencastproject.mediapackage.MediaPackage#remove(org.opencastproject.mediapackage.Attachment)
837    */
838   @Override
839   public void remove(Attachment attachment) {
840     removeElement(attachment);
841   }
842 
843   /**
844    * {@inheritDoc}
845    *
846    * @see org.opencastproject.mediapackage.MediaPackage#remove(org.opencastproject.mediapackage.Catalog)
847    */
848   @Override
849   public void remove(Catalog catalog) {
850     removeElement(catalog);
851   }
852 
853   /**
854    * {@inheritDoc}
855    *
856    * @see org.opencastproject.mediapackage.MediaPackage#remove(org.opencastproject.mediapackage.Track)
857    */
858   @Override
859   public void remove(Track track) {
860     removeElement(track);
861     recalculateDuration();
862   }
863 
864   /**
865    * Removes an element from the media package
866    *
867    * @param element
868    *          the media package element
869    */
870   void removeElement(MediaPackageElement element) {
871     removeInternal(element);
872     if (element instanceof AbstractMediaPackageElement) {
873       ((AbstractMediaPackageElement) element).setMediaPackage(null);
874     }
875   }
876 
877   /**
878    * {@inheritDoc}
879    *
880    * @see org.opencastproject.mediapackage.MediaPackage#add(java.net.URI)
881    */
882   @Override
883   public MediaPackageElement add(URI url) {
884     if (url == null)
885       throw new IllegalArgumentException("Argument 'url' may not be null");
886 
887     if (mediaPackageElementBuilder == null) {
888       mediaPackageElementBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
889     }
890     MediaPackageElement element = mediaPackageElementBuilder.elementFromURI(url);
891     integrate(element);
892     addInternal(element);
893     return element;
894   }
895 
896   /**
897    * @see org.opencastproject.mediapackage.MediaPackage#add(URI,
898    *      org.opencastproject.mediapackage.MediaPackageElement.Type,
899    *      org.opencastproject.mediapackage.MediaPackageElementFlavor)
900    */
901   @Override
902   public MediaPackageElement add(URI uri, Type type, MediaPackageElementFlavor flavor) {
903     if (uri == null)
904       throw new IllegalArgumentException("Argument 'url' may not be null");
905     if (type == null)
906       throw new IllegalArgumentException("Argument 'type' may not be null");
907 
908     if (mediaPackageElementBuilder == null) {
909       mediaPackageElementBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
910     }
911     MediaPackageElement element = mediaPackageElementBuilder.elementFromURI(uri, type, flavor);
912     integrate(element);
913     addInternal(element);
914     return element;
915   }
916 
917   /**
918    * {@inheritDoc}
919    *
920    * @see org.opencastproject.mediapackage.MediaPackage#add(org.opencastproject.mediapackage.MediaPackageElement)
921    */
922   @Override
923   public void add(MediaPackageElement element) {
924     if (element.getElementType().equals(MediaPackageElement.Type.Track) && element instanceof Track) {
925       integrateTrack((Track) element);
926     } else if (element.getElementType().equals(MediaPackageElement.Type.Catalog) && element instanceof Catalog) {
927       integrateCatalog((Catalog) element);
928     } else if (element.getElementType().equals(MediaPackageElement.Type.Attachment) && element instanceof Attachment) {
929       integrateAttachment((Attachment) element);
930     } else {
931       integrate(element);
932     }
933     addInternal(element);
934   }
935 
936   /**
937    * {@inheritDoc}
938    *
939    * @see org.opencastproject.mediapackage.MediaPackage#addDerived(org.opencastproject.mediapackage.MediaPackageElement,
940    *      org.opencastproject.mediapackage.MediaPackageElement)
941    */
942   @Override
943   public void addDerived(MediaPackageElement derivedElement, MediaPackageElement sourceElement) {
944     addDerived(derivedElement, sourceElement, null);
945   }
946 
947   private void addDerived(
948           MediaPackageElement derivedElement, MediaPackageElement sourceElement, Map<String, String> properties) {
949     if (derivedElement == null)
950       throw new IllegalArgumentException("The derived element is null");
951     if (sourceElement == null)
952       throw new IllegalArgumentException("The source element is null");
953     if (!contains(sourceElement))
954       throw new IllegalStateException("The sourceElement needs to be part of the media package");
955 
956     derivedElement.referTo(sourceElement);
957     addInternal(derivedElement);
958 
959     if (properties != null) {
960       MediaPackageReference ref = derivedElement.getReference();
961       for (Map.Entry<String, String> entry : properties.entrySet()) {
962         ref.setProperty(entry.getKey(), entry.getValue());
963       }
964     }
965   }
966 
967   /**
968    * {@inheritDoc}
969    *
970    * @see org.opencastproject.mediapackage.MediaPackage#getDerived(org.opencastproject.mediapackage.MediaPackageElement,
971    *      org.opencastproject.mediapackage.MediaPackageElementFlavor)
972    */
973   @Override
974   public MediaPackageElement[] getDerived(MediaPackageElement sourceElement, MediaPackageElementFlavor derivateFlavor) {
975     if (sourceElement == null)
976       throw new IllegalArgumentException("Source element cannot be null");
977     if (derivateFlavor == null)
978       throw new IllegalArgumentException("Derivate flavor cannot be null");
979 
980     MediaPackageReference reference = new MediaPackageReferenceImpl(sourceElement);
981     List<MediaPackageElement> elements = new ArrayList<>();
982     for (MediaPackageElement element : getElements()) {
983       if (derivateFlavor.equals(element.getFlavor()) && reference.equals(element.getReference()))
984         elements.add(element);
985     }
986     return elements.toArray(new MediaPackageElement[elements.size()]);
987   }
988 
989   /**
990    * Integrates the element into the media package. This mainly involves moving the element into the media package file
991    * structure.
992    *
993    * @param element
994    *          the element to integrate
995    */
996   private void integrate(MediaPackageElement element) {
997     if (element instanceof AbstractMediaPackageElement)
998       ((AbstractMediaPackageElement) element).setMediaPackage(this);
999   }
1000 
1001   /**
1002    * Integrates the catalog into the media package. This mainly involves moving the catalog into the media package file
1003    * structure.
1004    *
1005    * @param catalog
1006    *          the catalog to integrate
1007    */
1008   private void integrateCatalog(Catalog catalog) {
1009     // Check (uniqueness of) catalog identifier
1010     String id = catalog.getIdentifier();
1011     if (id == null || contains(id)) {
1012       catalog.generateIdentifier();
1013     }
1014     integrate(catalog);
1015   }
1016 
1017   /**
1018    * Integrates the track into the media package. This mainly involves moving the track into the media package file
1019    * structure.
1020    *
1021    * @param track
1022    *          the track to integrate
1023    */
1024   private void integrateTrack(Track track) {
1025     // Check (uniqueness of) track identifier
1026     String id = track.getIdentifier();
1027     if (id == null || contains(id)) {
1028       track.generateIdentifier();
1029     }
1030     integrate(track);
1031   }
1032 
1033   /**
1034    * Integrates the attachment into the media package. This mainly involves moving the attachment into the media package
1035    * file structure.
1036    *
1037    * @param attachment
1038    *          the attachment to integrate
1039    */
1040   private void integrateAttachment(Attachment attachment) {
1041     // Check (uniqueness of) attachment identifier
1042     String id = attachment.getIdentifier();
1043     if (id == null || contains(id)) {
1044       attachment.generateIdentifier();
1045     }
1046     integrate(attachment);
1047   }
1048 
1049   /**
1050    * @see org.opencastproject.mediapackage.MediaPackage#verify()
1051    */
1052   @Override
1053   public void verify() throws MediaPackageException {
1054     for (MediaPackageElement e : getElements()) {
1055       e.verify();
1056     }
1057   }
1058 
1059   /* NOTE: DO NOT REMOVE THIS METHOD IT WILL BREAK THINGS,
1060     * SEE https://github.com/opencast/opencast/issues/1860 for an example
1061     */
1062   /**
1063    * Unmarshals XML representation of a MediaPackage via JAXB.
1064    *
1065    * @param xml
1066    *          the serialized xml string
1067    * @return the deserialized media package
1068    * @throws MediaPackageException
1069    */
1070   public static MediaPackageImpl valueOf(String xml) throws MediaPackageException {
1071     return MediaPackageImpl.valueOf(IOUtils.toInputStream(xml, "UTF-8"));
1072   }
1073 
1074 
1075   /**
1076    * @see java.lang.Object#hashCode()
1077    */
1078   @Override
1079   public int hashCode() {
1080     return getIdentifier().hashCode();
1081   }
1082 
1083   /**
1084    * @see java.lang.Object#equals(java.lang.Object)
1085    */
1086   @Override
1087   public boolean equals(Object obj) {
1088     if (obj instanceof MediaPackage) {
1089       return getIdentifier().equals(((MediaPackage) obj).getIdentifier());
1090     }
1091     return false;
1092   }
1093 
1094   /**
1095    * {@inheritDoc}
1096    *
1097    * @see java.lang.Object#clone()
1098    */
1099   @Override
1100   public Object clone() {
1101     try {
1102       String xml = MediaPackageParser.getAsXml(this);
1103       return MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().loadFromXml(xml);
1104     } catch (Exception e) {
1105       throw new RuntimeException(e);
1106     }
1107   }
1108 
1109   /**
1110    * @see java.lang.Object#toString()
1111    */
1112   @Override
1113   public String toString() {
1114     if (identifier != null)
1115       return identifier.toString();
1116     else
1117       return "Unknown media package";
1118   }
1119 
1120   /**
1121    * A JAXB adapter that allows the {@link MediaPackage} interface to be un/marshalled
1122    */
1123   public static class Adapter extends XmlAdapter<MediaPackageImpl, MediaPackage> {
1124     @Override
1125     public MediaPackageImpl marshal(MediaPackage mp) throws Exception {
1126       return (MediaPackageImpl) mp;
1127     }
1128 
1129     @Override
1130     public MediaPackage unmarshal(MediaPackageImpl mp) throws Exception {
1131       return mp;
1132     }
1133   }
1134 
1135   /**
1136    * Reads the media package from the input stream.
1137    *
1138    * @param xml
1139    *          the input stream
1140    * @return the deserialized media package
1141    */
1142   public static MediaPackageImpl valueOf(InputStream xml) throws MediaPackageException {
1143     try {
1144       Unmarshaller unmarshaller = context.createUnmarshaller();
1145       return unmarshaller.unmarshal(XmlSafeParser.parse(xml), MediaPackageImpl.class).getValue();
1146     } catch (JAXBException e) {
1147       throw new MediaPackageException(e.getLinkedException() != null ? e.getLinkedException() : e);
1148     } catch (IOException | SAXException e) {
1149       throw new MediaPackageException(e);
1150     } finally {
1151       IoSupport.closeQuietly(xml);
1152     }
1153   }
1154 
1155   /**
1156    * Reads the media package from an xml node.
1157    *
1158    * @param xml
1159    *          the node
1160    * @return the deserialized media package
1161    */
1162   public static MediaPackageImpl valueOf(Node xml) throws MediaPackageException {
1163     try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
1164       Unmarshaller unmarshaller = context.createUnmarshaller();
1165       // Serialize the media package
1166       DOMImplementationRegistry reg = DOMImplementationRegistry.newInstance();
1167       DOMImplementationLS impl = (DOMImplementationLS) reg.getDOMImplementation("LS");
1168       LSSerializer serializer = impl.createLSSerializer();
1169       serializer.getDomConfig().setParameter("comments", false);
1170       serializer.getDomConfig().setParameter("format-pretty-print", false);
1171       LSOutput output = impl.createLSOutput();
1172       output.setEncoding("UTF-8");
1173       output.setByteStream(out);
1174       // This is safe because the Node was already parsed
1175       serializer.write(xml, output);
1176 
1177       try (InputStream in = new ByteArrayInputStream(out.toByteArray())) {
1178         // CHECKSTYLE:OFF
1179         // in was already parsed, therefore this is save
1180         return unmarshaller.unmarshal(new StreamSource(in), MediaPackageImpl.class).getValue();
1181         // CHECKSTYLE:ON
1182       }
1183     } catch (Exception e) {
1184       throw new MediaPackageException("Error deserializing media package node", e);
1185     }
1186   }
1187 
1188   /**
1189    * {@inheritDoc}
1190    *
1191    * @see org.opencastproject.mediapackage.MediaPackage#getContributors()
1192    */
1193   @Override
1194   public String[] getContributors() {
1195     if (contributors == null)
1196       return new String[] {};
1197     return contributors.toArray(new String[contributors.size()]);
1198   }
1199 
1200   /**
1201    * {@inheritDoc}
1202    *
1203    * @see org.opencastproject.mediapackage.MediaPackage#getCreators()
1204    */
1205   @Override
1206   public String[] getCreators() {
1207     if (creators == null)
1208       return new String[] {};
1209     return creators.toArray(new String[creators.size()]);
1210   }
1211 
1212   /**
1213    * {@inheritDoc}
1214    *
1215    * @see org.opencastproject.mediapackage.MediaPackage#getLanguage()
1216    */
1217   @Override
1218   public String getLanguage() {
1219     return language;
1220   }
1221 
1222   /**
1223    * {@inheritDoc}
1224    *
1225    * @see org.opencastproject.mediapackage.MediaPackage#getLicense()
1226    */
1227   @Override
1228   public String getLicense() {
1229     return license;
1230   }
1231 
1232   /**
1233    * {@inheritDoc}
1234    *
1235    * @see org.opencastproject.mediapackage.MediaPackage#getSeries()
1236    */
1237   @Override
1238   public String getSeries() {
1239     return series;
1240   }
1241 
1242   /**
1243    * {@inheritDoc}
1244    *
1245    * @see org.opencastproject.mediapackage.MediaPackage#getSubjects()
1246    */
1247   @Override
1248   public String[] getSubjects() {
1249     if (subjects == null)
1250       return new String[] {};
1251     return subjects.toArray(new String[subjects.size()]);
1252   }
1253 
1254   /**
1255    * {@inheritDoc}
1256    *
1257    * @see org.opencastproject.mediapackage.MediaPackage#getTitle()
1258    */
1259   @Override
1260   public String getTitle() {
1261     return title;
1262   }
1263 
1264   /**
1265    * {@inheritDoc}
1266    *
1267    * @see org.opencastproject.mediapackage.MediaPackage#getSeriesTitle()
1268    */
1269   @Override
1270   public String getSeriesTitle() {
1271     return seriesTitle;
1272   }
1273 
1274   /**
1275    * {@inheritDoc}
1276    *
1277    * @see org.opencastproject.mediapackage.MediaPackage#setSeriesTitle(java.lang.String)
1278    */
1279   @Override
1280   public void setSeriesTitle(String seriesTitle) {
1281     this.seriesTitle = seriesTitle;
1282   }
1283 
1284   /**
1285    * {@inheritDoc}
1286    *
1287    * @see org.opencastproject.mediapackage.MediaPackage#addContributor(java.lang.String)
1288    */
1289   @Override
1290   public void addContributor(String contributor) {
1291     if (contributors == null)
1292       contributors = new TreeSet<String>();
1293     contributors.add(contributor);
1294   }
1295 
1296   /**
1297    * {@inheritDoc}
1298    *
1299    * @see org.opencastproject.mediapackage.MediaPackage#addCreator(java.lang.String)
1300    */
1301   @Override
1302   public void addCreator(String creator) {
1303     if (creators == null)
1304       creators = new TreeSet<>();
1305     creators.add(creator);
1306   }
1307 
1308   /**
1309    * {@inheritDoc}
1310    *
1311    * @see org.opencastproject.mediapackage.MediaPackage#addSubject(java.lang.String)
1312    */
1313   @Override
1314   public void addSubject(String subject) {
1315     if (subjects == null)
1316       subjects = new TreeSet<>();
1317     subjects.add(subject);
1318   }
1319 
1320   /**
1321    * {@inheritDoc}
1322    *
1323    * @see org.opencastproject.mediapackage.MediaPackage#removeContributor(java.lang.String)
1324    */
1325   @Override
1326   public void removeContributor(String contributor) {
1327     if (contributors != null)
1328       contributors.remove(contributor);
1329   }
1330 
1331   /**
1332    * {@inheritDoc}
1333    *
1334    * @see org.opencastproject.mediapackage.MediaPackage#removeCreator(java.lang.String)
1335    */
1336   @Override
1337   public void removeCreator(String creator) {
1338     if (creators != null)
1339       creators.remove(creator);
1340   }
1341 
1342   /**
1343    * {@inheritDoc}
1344    *
1345    * @see org.opencastproject.mediapackage.MediaPackage#removeSubject(java.lang.String)
1346    */
1347   @Override
1348   public void removeSubject(String subject) {
1349     if (subjects != null)
1350       subjects.remove(subject);
1351   }
1352 
1353   /**
1354    * {@inheritDoc}
1355    *
1356    * @see org.opencastproject.mediapackage.MediaPackage#setDate(java.util.Date)
1357    */
1358   @Override
1359   public void setDate(Date date) {
1360     if (date != null)
1361       this.startTime = date.getTime();
1362     else
1363       this.startTime = 0;
1364   }
1365 
1366   /**
1367    * {@inheritDoc}
1368    *
1369    * @see org.opencastproject.mediapackage.MediaPackage#setLanguage(java.lang.String)
1370    */
1371   @Override
1372   public void setLanguage(String language) {
1373     this.language = language;
1374   }
1375 
1376   /**
1377    * {@inheritDoc}
1378    *
1379    * @see org.opencastproject.mediapackage.MediaPackage#setLicense(java.lang.String)
1380    */
1381   @Override
1382   public void setLicense(String license) {
1383     this.license = license;
1384   }
1385 
1386   /**
1387    * {@inheritDoc}
1388    *
1389    * @see org.opencastproject.mediapackage.MediaPackage#setSeries(java.lang.String)
1390    */
1391   @Override
1392   public void setSeries(String identifier) {
1393     this.series = identifier;
1394   }
1395 
1396   /**
1397    * {@inheritDoc}
1398    *
1399    * @see org.opencastproject.mediapackage.MediaPackage#setTitle(java.lang.String)
1400    */
1401   @Override
1402   public void setTitle(String title) {
1403     this.title = title;
1404   }
1405 
1406   @Override
1407   public boolean isLive() {
1408     return Arrays.stream(getTracks()).anyMatch(Track::isLive);
1409   }
1410 
1411   /**
1412    * Returns the media package element that matches the given reference.
1413    *
1414    * @param reference
1415    *          the reference
1416    * @return the element
1417    */
1418   MediaPackageElement getElement(MediaPackageReference reference) {
1419     if (reference == null)
1420       return null;
1421     for (MediaPackageElement e : elements) {
1422       if (e.getIdentifier().equals(reference.getIdentifier()))
1423         return e;
1424     }
1425     return null;
1426   }
1427 
1428   /**
1429    * Registers a new media package element with this manifest.
1430    *
1431    * @param element
1432    *          the new element
1433    */
1434   private void addInternal(MediaPackageElement element) {
1435     if (element == null)
1436       throw new IllegalArgumentException("Media package element must not be null");
1437 
1438     elements.add(element);
1439     if (element instanceof Track) {
1440       recalculateDuration();
1441     }
1442 
1443     // Check if element has an id
1444     if (element.getIdentifier() == null) {
1445       element.generateIdentifier();
1446     }
1447   }
1448 
1449   /**
1450    * Removes the media package element from the manifest.
1451    *
1452    * @param element
1453    *          the element to remove
1454    */
1455   void removeInternal(MediaPackageElement element) {
1456     if (element == null)
1457       throw new IllegalArgumentException("Media package element must not be null");
1458 
1459     if (elements.remove(element) && element instanceof Track) {
1460       recalculateDuration();
1461     }
1462   }
1463 
1464   /**
1465    * Extracts the list of tracks from the media package.
1466    *
1467    * @return the tracks
1468    */
1469   private Collection<Track> loadTracks() {
1470     List<Track> tracks = new ArrayList<>();
1471     synchronized (elements) {
1472       for (MediaPackageElement e : elements) {
1473         if (e instanceof Track) {
1474           tracks.add((Track) e);
1475         }
1476       }
1477     }
1478     return tracks;
1479   }
1480 
1481   /**
1482    * Extracts the list of catalogs from the media package.
1483    *
1484    * @return the catalogs
1485    */
1486   private Collection<Catalog> loadCatalogs() {
1487     List<Catalog> catalogs = new ArrayList<>();
1488     synchronized (elements) {
1489       for (MediaPackageElement e : elements) {
1490         if (e instanceof Catalog) {
1491           catalogs.add((Catalog) e);
1492         }
1493       }
1494     }
1495     return catalogs;
1496   }
1497 
1498   /**
1499    * Extracts the list of attachments from the media package.
1500    *
1501    * @return the attachments
1502    */
1503   private Collection<Attachment> loadAttachments() {
1504     List<Attachment> attachments = new ArrayList<>();
1505     synchronized (elements) {
1506       for (MediaPackageElement e : elements) {
1507         if (e instanceof Attachment) {
1508           attachments.add((Attachment) e);
1509         }
1510       }
1511     }
1512     return attachments;
1513   }
1514 }