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