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