org.opencastproject.mediapackage.MediaPackageImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.opencastproject.mediapackage.MediaPackageImpl.java

Source

/**
 *  Copyright 2009, 2010 The Regents of the University of California
 *  Licensed under the Educational Community License, Version 2.0
 *  (the "License"); you may not use this file except in compliance
 *  with the License. You may obtain a copy of the License at
 *
 *  http://www.osedu.org/licenses/ECL-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an "AS IS"
 *  BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 *  or implied. See the License for the specific language governing
 *  permissions and limitations under the License.
 *
 */

package org.opencastproject.mediapackage;

import static org.opencastproject.mediapackage.MediaPackageSupport.Filters.presentations;
import static org.opencastproject.util.data.Monadics.mlist;

import org.opencastproject.mediapackage.MediaPackageElement.Type;
import org.opencastproject.mediapackage.identifier.Id;
import org.opencastproject.mediapackage.identifier.IdBuilder;
import org.opencastproject.mediapackage.identifier.UUIDIdBuilderImpl;
import org.opencastproject.util.DateTimeSupport;
import org.opencastproject.util.IoSupport;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

/**
 * Default implementation for a media media package.
 */
@XmlType(name = "mediapackage", namespace = "http://mediapackage.opencastproject.org", propOrder = { "title",
        "series", "seriesTitle", "creators", "contributors", "subjects", "license", "language", "tracks",
        "catalogs", "attachments", "publications" })
@XmlRootElement(name = "mediapackage", namespace = "http://mediapackage.opencastproject.org")
@XmlAccessorType(XmlAccessType.NONE)
public final class MediaPackageImpl implements MediaPackage {

    /** the logging facility provided by log4j */
    private static final Logger logger = LoggerFactory.getLogger(MediaPackageImpl.class.getName());

    /**
     * The prefix indicating that a tag should be excluded from a search for elements using
     * {@link #getElementsByTags(Collection)}
     */
    public static final String NEGATE_TAG_PREFIX = "-";

    /** Context for serializing and deserializing */
    static final JAXBContext context;

    /** List of observers */
    private final List<MediaPackageObserver> observers = new ArrayList<MediaPackageObserver>();

    /** The media package element builder, may remain <code>null</code> */
    private MediaPackageElementBuilder mediaPackageElementBuilder = null;

    @XmlElement(name = "title")
    private String title = null;

    @XmlElement(name = "seriestitle")
    private String seriesTitle = null;

    @XmlElement(name = "language")
    private String language = null;

    @XmlElement(name = "series")
    private String series = null;

    @XmlElement(name = "license")
    private String license = null;

    @XmlElementWrapper(name = "creators")
    @XmlElement(name = "creator")
    private Set<String> creators = null;

    @XmlElementWrapper(name = "contributors")
    @XmlElement(name = "contributor")
    private Set<String> contributors = null;

    @XmlElementWrapper(name = "subjects")
    @XmlElement(name = "subject")
    private Set<String> subjects = null;

    /** id builder, for internal use only */
    private static final IdBuilder idBuilder = new UUIDIdBuilderImpl();

    /** The media package's identifier */
    private Id identifier = null;

    /** The start date and time */
    private long startTime = 0L;

    /** The media package duration */
    private Long duration = null;

    /** The media package's other (uncategorized) files */
    private final List<MediaPackageElement> elements = new ArrayList<MediaPackageElement>();

    /** Number of tracks */
    private int tracks = 0;

    /** Number of metadata catalogs */
    private int catalogs = 0;

    /** Number of attachments */
    private int attachments = 0;

    /** Numer of unclassified elements */
    private int others = 0;

    static {
        try {
            context = JAXBContext.newInstance("org.opencastproject.mediapackage",
                    MediaPackageImpl.class.getClassLoader());
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Creates a media package object.
     */
    MediaPackageImpl() {
        this(idBuilder.createNew());
    }

    /**
     * Creates a media package object with the media package identifier.
     * 
     * @param id
     *          the media package identifier
     */
    MediaPackageImpl(Id id) {
        this.identifier = id;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getIdentifier()
     */
    @XmlAttribute(name = "id")
    @Override
    public Id getIdentifier() {
        return identifier;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#setIdentifier(org.opencastproject.mediapackage.identifier.Id)
     */
    @Override
    public void setIdentifier(Id identifier) {
        this.identifier = identifier;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getDuration()
     */
    @XmlAttribute(name = "duration")
    @Override
    public Long getDuration() {
        if (duration == null && hasTracks()) {
            for (Track t : getTracks()) {
                if (t.getDuration() != null) {
                    if (duration == null || duration < t.getDuration())
                        duration = t.getDuration();
                }
            }
        }
        return duration;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#setDuration(Long)
     */
    @Override
    public void setDuration(Long duration) throws IllegalStateException {
        if (hasTracks())
            throw new IllegalStateException(
                    "The duration is determined by the length of the tracks and cannot be set manually");
        this.duration = duration;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getDate()
     */
    @Override
    public Date getDate() {
        return new Date(startTime);
    }

    /**
     * Returns the recording time in utc format.
     * 
     * @return the recording time
     */
    @XmlAttribute(name = "start")
    public String getStartDateAsString() {
        if (startTime == 0)
            return null;
        return DateTimeSupport.toUTC(startTime);
    }

    /**
     * Sets the date and time of recording in utc format.
     * 
     * @param startTime
     *          the start time
     */
    public void setStartDateAsString(String startTime) {
        if (startTime != null && !"0".equals(startTime) && !startTime.isEmpty()) {
            try {
                this.startTime = DateTimeSupport.fromUTC(startTime);
            } catch (Exception e) {
                logger.info("Unable to parse start time {}", startTime);
            }
        } else {
            this.startTime = 0;
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#elements()
     */
    @Override
    public Iterable<MediaPackageElement> elements() {
        return Arrays.asList(getElements());
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getElements()
     */
    @Override
    public MediaPackageElement[] getElements() {
        return elements.toArray(new MediaPackageElement[elements.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getElementByReference(org.opencastproject.mediapackage.MediaPackageReference)
     */
    @Override
    public MediaPackageElement getElementByReference(MediaPackageReference reference) {
        for (MediaPackageElement e : this.elements) {
            if (!reference.getType().equalsIgnoreCase(e.getElementType().toString()))
                continue;
            if (reference.getIdentifier().equals(e.getIdentifier()))
                return e;
        }
        return null;
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#getElementById(java.lang.String)
     */
    @Override
    public MediaPackageElement getElementById(String id) {
        for (MediaPackageElement element : getElements()) {
            if (id.equals(element.getIdentifier()))
                return element;
        }
        return null;
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#getElementById(java.lang.String)
     */
    @Override
    public MediaPackageElement[] getElementsByTag(String tag) {
        List<MediaPackageElement> result = new ArrayList<MediaPackageElement>();
        for (MediaPackageElement element : getElements()) {
            if (element.containsTag(tag)) {
                result.add(element);
            }
        }
        return result.toArray(new MediaPackageElement[result.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getElementsByTags(java.util.Collection)
     */
    @Override
    public MediaPackageElement[] getElementsByTags(Collection<String> tags) {
        if (tags == null || tags.isEmpty())
            return getElements();
        Set<String> keep = new HashSet<String>();
        Set<String> lose = new HashSet<String>();
        for (String tag : tags) {
            if (StringUtils.isBlank(tag))
                continue;
            if (tag.startsWith(NEGATE_TAG_PREFIX)) {
                lose.add(tag.substring(NEGATE_TAG_PREFIX.length()));
            } else {
                keep.add(tag);
            }
        }
        List<MediaPackageElement> result = new ArrayList<MediaPackageElement>();
        for (MediaPackageElement element : getElements()) {
            boolean add = false;
            for (String elementTag : element.getTags()) {
                if (lose.contains(elementTag)) {
                    add = false;
                    break;
                } else if (keep.contains(elementTag)) {
                    add = true;
                }
            }
            if (add) {
                result.add(element);
            }
        }
        return result.toArray(new MediaPackageElement[result.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getAttachmentsByTags(java.util.Collection)
     */
    @Override
    public Attachment[] getAttachmentsByTags(Collection<String> tags) {
        MediaPackageElement[] matchingElements = getElementsByTags(tags);
        List<Attachment> attachments = new ArrayList<Attachment>();
        for (MediaPackageElement element : matchingElements) {
            if (Attachment.TYPE.equals(element.getElementType())) {
                attachments.add((Attachment) element);
            }
        }
        return attachments.toArray(new Attachment[attachments.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getCatalogsByTags(java.util.Collection)
     */
    @Override
    public Catalog[] getCatalogsByTags(Collection<String> tags) {
        MediaPackageElement[] matchingElements = getElementsByTags(tags);
        List<Catalog> catalogs = new ArrayList<Catalog>();
        for (MediaPackageElement element : matchingElements) {
            if (Catalog.TYPE.equals(element.getElementType())) {
                catalogs.add((Catalog) element);
            }
        }
        return catalogs.toArray(new Catalog[catalogs.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getTracksByTags(java.util.Collection)
     */
    @Override
    public Track[] getTracksByTags(Collection<String> tags) {
        MediaPackageElement[] matchingElements = getElementsByTags(tags);
        List<Track> tracks = new ArrayList<Track>();
        for (MediaPackageElement element : matchingElements) {
            if (Track.TYPE.equals(element.getElementType())) {
                tracks.add((Track) element);
            }
        }
        return tracks.toArray(new Track[tracks.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getElementsByFlavor(org.opencastproject.mediapackage.MediaPackageElementFlavor)
     */
    @Override
    public MediaPackageElement[] getElementsByFlavor(MediaPackageElementFlavor flavor) {
        if (flavor == null)
            throw new IllegalArgumentException("Flavor cannot be null");

        List<MediaPackageElement> elements = new ArrayList<MediaPackageElement>();
        for (MediaPackageElement element : getElements()) {
            if (flavor.equals(element.getFlavor()))
                elements.add(element);
        }
        return elements.toArray(new MediaPackageElement[elements.size()]);
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#contains(org.opencastproject.mediapackage.MediaPackageElement)
     */
    @Override
    public boolean contains(MediaPackageElement element) {
        if (element == null)
            throw new IllegalArgumentException("Media package element must not be null");
        return (elements.contains(element));
    }

    /**
     * Returns <code>true</code> if the media package contains an element with the specified identifier.
     * 
     * @param identifier
     *          the identifier
     * @return <code>true</code> if the media package contains an element with this identifier
     */
    boolean contains(String identifier) {
        for (MediaPackageElement element : getElements()) {
            if (element.getIdentifier().equals(identifier))
                return true;
        }
        return false;
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#add(org.opencastproject.mediapackage.Catalog)
     */
    @Override
    public void add(Catalog catalog) {
        integrateCatalog(catalog);
        addInternal(catalog);
        fireElementAdded(catalog);
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#add(org.opencastproject.mediapackage.Track)
     */
    @Override
    public void add(Track track) {
        integrateTrack(track);
        addInternal(track);
        fireElementAdded(track);
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#add(org.opencastproject.mediapackage.Attachment)
     */
    @Override
    public void add(Attachment attachment) {
        integrateAttachment(attachment);
        addInternal(attachment);
        fireElementAdded(attachment);
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#getCatalog(java.lang.String)
     */
    @Override
    public Catalog getCatalog(String catalogId) {
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e.getIdentifier().equals(catalogId) && e instanceof Catalog)
                    return (Catalog) e;
            }
        }
        return null;
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#getCatalogs()
     */
    @XmlElementWrapper(name = "metadata")
    @XmlElement(name = "catalog")
    @Override
    public Catalog[] getCatalogs() {
        Collection<Catalog> catalogs = loadCatalogs();
        return catalogs.toArray(new Catalog[catalogs.size()]);
    }

    void setCatalogs(Catalog[] catalogs) {
        List<Catalog> newCatalogs = Arrays.asList(catalogs);
        List<Catalog> oldCatalogs = Arrays.asList(getCatalogs());
        // remove any catalogs not in this array
        for (Catalog existing : oldCatalogs) {
            if (!newCatalogs.contains(existing)) {
                remove(existing);
            }
        }
        for (Catalog newCatalog : newCatalogs) {
            if (!oldCatalogs.contains(newCatalog)) {
                add(newCatalog);
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getCatalogsByTag(java.lang.String)
     */
    @Override
    public Catalog[] getCatalogsByTag(String tag) {
        List<Catalog> result = new ArrayList<Catalog>();
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e instanceof Catalog && e.containsTag(tag))
                    result.add((Catalog) e);
            }
        }
        return result.toArray(new Catalog[result.size()]);
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#getCatalogs(MediaPackageElementFlavor)
     */
    @Override
    public Catalog[] getCatalogs(MediaPackageElementFlavor flavor) {
        if (flavor == null)
            throw new IllegalArgumentException("Unable to filter by null criterion");

        // Go through catalogs and remove those that don't match
        Collection<Catalog> catalogs = loadCatalogs();
        List<Catalog> candidates = new ArrayList<Catalog>(catalogs);
        for (Catalog c : catalogs) {
            if (c.getFlavor() == null || !c.getFlavor().matches(flavor)) {
                candidates.remove(c);
            }
        }
        return candidates.toArray(new Catalog[candidates.size()]);
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#getCatalogs(org.opencastproject.mediapackage.MediaPackageReference)
     */
    @Override
    public Catalog[] getCatalogs(MediaPackageReference reference) {
        return getCatalogs(reference, false);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getCatalogs(org.opencastproject.mediapackage.MediaPackageReference,
     *      boolean)
     */
    @Override
    public Catalog[] getCatalogs(MediaPackageReference reference, boolean includeDerived) {
        if (reference == null)
            throw new IllegalArgumentException("Unable to filter by null reference");

        // Go through catalogs and remove those that don't match
        Collection<Catalog> catalogs = loadCatalogs();
        List<Catalog> candidates = new ArrayList<Catalog>(catalogs);
        for (Catalog c : catalogs) {
            MediaPackageReference r = c.getReference();
            if (!reference.matches(r)) {
                boolean indirectHit = false;

                // Create a reference that will match regardless of properties
                MediaPackageReference elementRef = new MediaPackageReferenceImpl(reference.getType(),
                        reference.getIdentifier());

                // Try to find a derived match if possible
                while (includeDerived && r != null) {
                    if (r.matches(elementRef)) {
                        indirectHit = true;
                        break;
                    }
                    r = getElement(r).getReference();
                }

                if (!indirectHit)
                    candidates.remove(c);
            }
        }

        return candidates.toArray(new Catalog[candidates.size()]);
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#getCatalogs(org.opencastproject.mediapackage.MediaPackageElementFlavor,
     *      org.opencastproject.mediapackage.MediaPackageReference)
     */
    @Override
    public Catalog[] getCatalogs(MediaPackageElementFlavor flavor, MediaPackageReference reference) {
        if (flavor == null)
            throw new IllegalArgumentException("Unable to filter by null criterion");
        if (reference == null)
            throw new IllegalArgumentException("Unable to filter by null reference");

        // Go through catalogs and remove those that don't match
        Collection<Catalog> catalogs = loadCatalogs();
        List<Catalog> candidates = new ArrayList<Catalog>(catalogs);
        for (Catalog c : catalogs) {
            if (!flavor.equals(c.getFlavor())
                    || (c.getReference() != null && !c.getReference().matches(reference))) {
                candidates.remove(c);
            }
        }
        return candidates.toArray(new Catalog[candidates.size()]);
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#hasCatalogs()
     */
    @Override
    public boolean hasCatalogs() {
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e instanceof Catalog)
                    return true;
            }
        }
        return false;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getTrack(java.lang.String)
     */
    @Override
    public Track getTrack(String trackId) {
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e.getIdentifier().equals(trackId) && e instanceof Track)
                    return (Track) e;
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getTracks()
     */
    @XmlElementWrapper(name = "media")
    @XmlElement(name = "track")
    @Override
    public Track[] getTracks() {
        Collection<Track> tracks = loadTracks();
        return tracks.toArray(new Track[tracks.size()]);
    }

    void setTracks(Track[] tracks) {
        List<Track> newTracks = Arrays.asList(tracks);
        List<Track> oldTracks = Arrays.asList(getTracks());
        // remove any catalogs not in this array
        for (Track existing : oldTracks) {
            if (!newTracks.contains(existing)) {
                remove(existing);
            }
        }
        for (Track newTrack : newTracks) {
            if (!oldTracks.contains(newTrack)) {
                add(newTrack);
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getTracksByTag(java.lang.String)
     */
    @Override
    public Track[] getTracksByTag(String tag) {
        List<Track> result = new ArrayList<Track>();
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e instanceof Track && e.containsTag(tag))
                    result.add((Track) e);
            }
        }
        return result.toArray(new Track[result.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getTracks(org.opencastproject.mediapackage.MediaPackageElementFlavor)
     */
    @Override
    public Track[] getTracks(MediaPackageElementFlavor flavor) {
        if (flavor == null)
            throw new IllegalArgumentException("Unable to filter by null criterion");

        // Go through tracks and remove those that don't match
        Collection<Track> tracks = loadTracks();
        List<Track> candidates = new ArrayList<Track>(tracks);
        for (Track a : tracks) {
            if (!flavor.equals(a.getFlavor())) {
                candidates.remove(a);
            }
        }
        return candidates.toArray(new Track[candidates.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getTracks(org.opencastproject.mediapackage.MediaPackageReference)
     */
    @Override
    public Track[] getTracks(MediaPackageReference reference) {
        return getTracks(reference, false);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getTracks(org.opencastproject.mediapackage.MediaPackageReference,
     *      boolean)
     */
    @Override
    public Track[] getTracks(MediaPackageReference reference, boolean includeDerived) {
        if (reference == null)
            throw new IllegalArgumentException("Unable to filter by null reference");

        // Go through tracks and remove those that don't match
        Collection<Track> tracks = loadTracks();
        List<Track> candidates = new ArrayList<Track>(tracks);
        for (Track t : tracks) {
            MediaPackageReference r = t.getReference();
            if (!reference.matches(r)) {
                boolean indirectHit = false;

                // Create a reference that will match regardless of properties
                MediaPackageReference elementRef = new MediaPackageReferenceImpl(reference.getType(),
                        reference.getIdentifier());

                // Try to find a derived match if possible
                while (includeDerived && r != null) {
                    if (r.matches(elementRef)) {
                        indirectHit = true;
                        break;
                    }
                    r = getElement(r).getReference();
                }

                if (!indirectHit)
                    candidates.remove(t);
            }
        }

        return candidates.toArray(new Track[candidates.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getTracks(org.opencastproject.mediapackage.MediaPackageElementFlavor,
     *      org.opencastproject.mediapackage.MediaPackageReference)
     */
    @Override
    public Track[] getTracks(MediaPackageElementFlavor flavor, MediaPackageReference reference) {
        if (flavor == null)
            throw new IllegalArgumentException("Unable to filter by null criterion");
        if (reference == null)
            throw new IllegalArgumentException("Unable to filter by null reference");

        // Go through tracks and remove those that don't match
        Collection<Track> tracks = loadTracks();
        List<Track> candidates = new ArrayList<Track>(tracks);
        for (Track a : tracks) {
            if (!flavor.equals(a.getFlavor()) || !reference.matches(a.getReference())) {
                candidates.remove(a);
            }
        }
        return candidates.toArray(new Track[candidates.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#hasTracks()
     */
    @Override
    public boolean hasTracks() {
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e instanceof Track)
                    return true;
            }
        }
        return false;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getUnclassifiedElements()
     */
    @Override
    public MediaPackageElement[] getUnclassifiedElements() {
        return getUnclassifiedElements(null);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getUnclassifiedElements(org.opencastproject.mediapackage.MediaPackageElementFlavor)
     */
    @Override
    public MediaPackageElement[] getUnclassifiedElements(MediaPackageElementFlavor flavor) {
        List<MediaPackageElement> unclassifieds = new ArrayList<MediaPackageElement>();
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (!(e instanceof Attachment) && !(e instanceof Catalog) && !(e instanceof Track)) {
                    if (flavor == null || flavor.equals(e.getFlavor())) {
                        unclassifieds.add(e);
                    }
                }
            }
        }
        return unclassifieds.toArray(new MediaPackageElement[unclassifieds.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#hasUnclassifiedElements(org.opencastproject.mediapackage.MediaPackageElementFlavor)
     */
    @Override
    public boolean hasUnclassifiedElements(MediaPackageElementFlavor type) {
        if (type == null)
            return others > 0;
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (!(e instanceof Attachment) && !(e instanceof Catalog) && !(e instanceof Track)) {
                    if (type.equals(e.getFlavor())) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#hasUnclassifiedElements()
     */
    @Override
    public boolean hasUnclassifiedElements() {
        return hasUnclassifiedElements(null);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#addObserver(org.opencastproject.mediapackage.MediaPackageObserver)
     */
    @Override
    public void addObserver(MediaPackageObserver observer) {
        synchronized (observers) {
            observers.add(observer);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getAttachment(java.lang.String)
     */
    @Override
    public Attachment getAttachment(String attachmentId) {
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e.getIdentifier().equals(attachmentId) && e instanceof Attachment)
                    return (Attachment) e;
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getAttachments()
     */
    @XmlElementWrapper(name = "attachments")
    @XmlElement(name = "attachment")
    @Override
    public Attachment[] getAttachments() {
        Collection<Attachment> attachments = loadAttachments();
        return attachments.toArray(new Attachment[attachments.size()]);
    }

    void setAttachments(Attachment[] catalogs) {
        List<Attachment> newAttachments = Arrays.asList(catalogs);
        List<Attachment> oldAttachments = Arrays.asList(getAttachments());
        // remove any catalogs not in this array
        for (Attachment existing : oldAttachments) {
            if (!newAttachments.contains(existing)) {
                remove(existing);
            }
        }
        for (Attachment newAttachment : newAttachments) {
            if (!oldAttachments.contains(newAttachment)) {
                add(newAttachment);
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getAttachmentsByTag(java.lang.String)
     */
    @Override
    public Attachment[] getAttachmentsByTag(String tag) {
        List<Attachment> result = new ArrayList<Attachment>();
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e instanceof Attachment && e.containsTag(tag))
                    result.add((Attachment) e);
            }
        }
        return result.toArray(new Attachment[result.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getAttachments(org.opencastproject.mediapackage.MediaPackageElementFlavor)
     */
    @Override
    public Attachment[] getAttachments(MediaPackageElementFlavor flavor) {
        if (flavor == null)
            throw new IllegalArgumentException("Unable to filter by null criterion");

        // Go through attachments and remove those that don't match
        Collection<Attachment> attachments = loadAttachments();
        List<Attachment> candidates = new ArrayList<Attachment>(attachments);
        for (Attachment a : attachments) {
            if (!flavor.equals(a.getFlavor())) {
                candidates.remove(a);
            }
        }
        return candidates.toArray(new Attachment[candidates.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getAttachments(org.opencastproject.mediapackage.MediaPackageReference)
     */
    @Override
    public Attachment[] getAttachments(MediaPackageReference reference) {
        return getAttachments(reference, false);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getAttachments(org.opencastproject.mediapackage.MediaPackageReference,
     *      boolean)
     */
    @Override
    public Attachment[] getAttachments(MediaPackageReference reference, boolean includeDerived) {
        if (reference == null)
            throw new IllegalArgumentException("Unable to filter by null reference");

        // Go through attachments and remove those that don't match
        Collection<Attachment> attachments = loadAttachments();
        List<Attachment> candidates = new ArrayList<Attachment>(attachments);
        for (Attachment a : attachments) {
            MediaPackageReference r = a.getReference();
            if (!reference.matches(r)) {
                boolean indirectHit = false;

                // Create a reference that will match regardless of properties
                MediaPackageReference elementRef = new MediaPackageReferenceImpl(reference.getType(),
                        reference.getIdentifier());

                // Try to find a derived match if possible
                while (includeDerived && getElement(r) != null && r != null) {
                    if (r.matches(elementRef)) {
                        indirectHit = true;
                        break;
                    }
                    r = getElement(r).getReference();
                }

                if (!indirectHit)
                    candidates.remove(a);
            }
        }
        return candidates.toArray(new Attachment[candidates.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getAttachments(org.opencastproject.mediapackage.MediaPackageElementFlavor,
     *      org.opencastproject.mediapackage.MediaPackageReference)
     */
    @Override
    public Attachment[] getAttachments(MediaPackageElementFlavor flavor, MediaPackageReference reference) {
        if (flavor == null)
            throw new IllegalArgumentException("Unable to filter by null criterion");
        if (reference == null)
            throw new IllegalArgumentException("Unable to filter by null reference");

        // Go through attachments and remove those that don't match
        Collection<Attachment> attachments = loadAttachments();
        List<Attachment> candidates = new ArrayList<Attachment>(attachments);
        for (Attachment a : attachments) {
            if (!flavor.equals(a.getFlavor()) || !reference.matches(a.getReference())) {
                candidates.remove(a);
            }
        }
        return candidates.toArray(new Attachment[candidates.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#hasAttachments()
     */
    @Override
    public boolean hasAttachments() {
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e instanceof Attachment)
                    return true;
            }
        }
        return false;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getAttachments()
     */
    @XmlElementWrapper(name = "publications")
    @XmlElement(name = "publication")
    @Override
    public Publication[] getPublications() {
        return mlist(elements).bind(presentations).value().toArray(new Publication[0]);
    }

    void setPublications(Publication[] publications) {
        List<Publication> newPublications = Arrays.asList(publications);
        List<Publication> oldPublications = Arrays.asList(getPublications());
        for (Publication oldp : oldPublications) {
            if (!newPublications.contains(oldp)) {
                remove(oldp);
            }
        }
        for (Publication newp : newPublications) {
            if (!oldPublications.contains(newp)) {
                add(newp);
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#removeElementById(java.lang.String)
     */
    @Override
    public MediaPackageElement removeElementById(String id) {
        MediaPackageElement element = getElementById(id);
        if (element == null)
            return null;
        remove(element);
        return element;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#remove(org.opencastproject.mediapackage.MediaPackageElement)
     */
    @Override
    public void remove(MediaPackageElement element) {
        removeElement(element);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#remove(org.opencastproject.mediapackage.Attachment)
     */
    @Override
    public void remove(Attachment attachment) {
        removeElement(attachment);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#remove(org.opencastproject.mediapackage.Catalog)
     */
    @Override
    public void remove(Catalog catalog) {
        removeElement(catalog);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#remove(org.opencastproject.mediapackage.Track)
     */
    @Override
    public void remove(Track track) {
        duration = null;
        removeElement(track);
    }

    /**
     * Removes an element from the media package
     * 
     * @param element
     *          the media package element
     */
    void removeElement(MediaPackageElement element) {
        removeInternal(element);
        fireElementRemoved(element);
        if (element instanceof AbstractMediaPackageElement) {
            ((AbstractMediaPackageElement) element).setMediaPackage(null);
        }
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#removeObserver(MediaPackageObserver)
     */
    @Override
    public void removeObserver(MediaPackageObserver observer) {
        synchronized (observers) {
            observers.remove(observer);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#add(java.net.URI)
     */
    @Override
    public MediaPackageElement add(URI url) {
        if (url == null)
            throw new IllegalArgumentException("Argument 'url' may not be null");

        if (mediaPackageElementBuilder == null) {
            mediaPackageElementBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
        }
        MediaPackageElement element = mediaPackageElementBuilder.elementFromURI(url);
        addInternal(element);
        return element;
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#add(URI,
     *      org.opencastproject.mediapackage.MediaPackageElement.Type,
     *      org.opencastproject.mediapackage.MediaPackageElementFlavor)
     */
    @Override
    public MediaPackageElement add(URI uri, Type type, MediaPackageElementFlavor flavor) {
        if (uri == null)
            throw new IllegalArgumentException("Argument 'url' may not be null");
        if (type == null)
            throw new IllegalArgumentException("Argument 'type' may not be null");

        if (mediaPackageElementBuilder == null) {
            mediaPackageElementBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
        }
        MediaPackageElement element = mediaPackageElementBuilder.elementFromURI(uri, type, flavor);
        addInternal(element);
        return element;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#add(org.opencastproject.mediapackage.MediaPackageElement)
     */
    @Override
    public void add(MediaPackageElement element) {
        if (element.getElementType().equals(MediaPackageElement.Type.Track) && element instanceof Track) {
            integrateTrack((Track) element);
        } else if (element.getElementType().equals(MediaPackageElement.Type.Catalog)
                && element instanceof Catalog) {
            integrateCatalog((Catalog) element);
        } else if (element.getElementType().equals(MediaPackageElement.Type.Attachment)
                && element instanceof Attachment) {
            integrateAttachment((Attachment) element);
        } else {
            integrate(element);
        }
        addInternal(element);
        fireElementAdded(element);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#addDerived(org.opencastproject.mediapackage.MediaPackageElement,
     *      org.opencastproject.mediapackage.MediaPackageElement)
     */
    @Override
    public void addDerived(MediaPackageElement derivedElement, MediaPackageElement sourceElement) {
        addDerived(derivedElement, sourceElement, null);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#addDerived(org.opencastproject.mediapackage.MediaPackageElement,
     *      org.opencastproject.mediapackage.MediaPackageElement, java.util.Map)
     */
    @Override
    public void addDerived(MediaPackageElement derivedElement, MediaPackageElement sourceElement,
            Map<String, String> properties) {
        if (derivedElement == null)
            throw new IllegalArgumentException("The derived element is null");
        if (sourceElement == null)
            throw new IllegalArgumentException("The source element is null");
        if (!contains(sourceElement))
            throw new IllegalStateException("The sourceElement needs to be part of the media package");

        derivedElement.referTo(sourceElement);
        addInternal(derivedElement);

        if (properties != null) {
            MediaPackageReference ref = derivedElement.getReference();
            for (Map.Entry<String, String> entry : properties.entrySet()) {
                ref.setProperty(entry.getKey(), entry.getValue());
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getDerived(org.opencastproject.mediapackage.MediaPackageElement,
     *      org.opencastproject.mediapackage.MediaPackageElementFlavor)
     */
    @Override
    public MediaPackageElement[] getDerived(MediaPackageElement sourceElement,
            MediaPackageElementFlavor derivateFlavor) {
        if (sourceElement == null)
            throw new IllegalArgumentException("Source element cannot be null");
        if (derivateFlavor == null)
            throw new IllegalArgumentException("Derivate flavor cannot be null");

        MediaPackageReference reference = new MediaPackageReferenceImpl(sourceElement);
        List<MediaPackageElement> elements = new ArrayList<MediaPackageElement>();
        for (MediaPackageElement element : getElements()) {
            if (derivateFlavor.equals(element.getFlavor()) && reference.equals(element.getReference()))
                elements.add(element);
        }
        return elements.toArray(new MediaPackageElement[elements.size()]);
    }

    /**
     * Notify observers of a removed media package element.
     * 
     * @param element
     *          the removed element
     */
    private void fireElementAdded(MediaPackageElement element) {
        synchronized (observers) {
            for (MediaPackageObserver o : observers) {
                try {
                    o.elementAdded(element);
                } catch (Throwable th) {
                    logger.error("MediaPackageOberserver " + o + " throw exception while processing callback", th);
                }
            }
        }
    }

    /**
     * Notify observers of a removed media package element.
     * 
     * @param element
     *          the removed element
     */
    private void fireElementRemoved(MediaPackageElement element) {
        synchronized (observers) {
            for (MediaPackageObserver o : observers) {
                try {
                    o.elementRemoved(element);
                } catch (Throwable th) {
                    logger.error("MediaPackageObserver " + o + " threw exception while processing callback", th);
                }
            }
        }
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#renameTo(org.opencastproject.mediapackage.identifier.Id)
     */
    @Override
    public void renameTo(Id identifier) {
        this.identifier = identifier;
    }

    /**
     * Integrates the element into the media package. This mainly involves moving the element into the media package file
     * structure.
     * 
     * @param element
     *          the element to integrate
     */
    private void integrate(MediaPackageElement element) {
        if (element instanceof AbstractMediaPackageElement)
            ((AbstractMediaPackageElement) element).setMediaPackage(this);
    }

    /**
     * Integrates the catalog into the media package. This mainly involves moving the catalog into the media package file
     * structure.
     * 
     * @param catalog
     *          the catalog to integrate
     */
    private void integrateCatalog(Catalog catalog) {
        // Check (uniqueness of) catalog identifier
        String id = catalog.getIdentifier();
        if (id == null || contains(id)) {
            catalog.setIdentifier(createElementIdentifier());
        }
        integrate(catalog);
    }

    /**
     * Integrates the track into the media package. This mainly involves moving the track into the media package file
     * structure.
     * 
     * @param track
     *          the track to integrate
     */
    private void integrateTrack(Track track) {
        // Check (uniqueness of) track identifier
        String id = track.getIdentifier();
        if (id == null || contains(id)) {
            track.setIdentifier(createElementIdentifier());
        }
        duration = null;
        integrate(track);
    }

    /**
     * Integrates the attachment into the media package. This mainly involves moving the attachment into the media package
     * file structure.
     * 
     * @param attachment
     *          the attachment to integrate
     */
    private void integrateAttachment(Attachment attachment) {
        // Check (uniqueness of) attachment identifier
        String id = attachment.getIdentifier();
        if (id == null || contains(id)) {
            attachment.setIdentifier(createElementIdentifier());
        }
        integrate(attachment);
    }

    /**
     * Returns a media package element identifier with the given prefix and the given number or a higher one as the
     * suffix. The identifier will be unique within the media package.
     * 
     * @param prefix
     *          the identifier prefix
     * @param count
     *          the number
     * @return the element identifier
     */
    private String createElementIdentifier() {
        return UUID.randomUUID().toString();
    }

    /**
     * @see org.opencastproject.mediapackage.MediaPackage#verify()
     */
    @Override
    public void verify() throws MediaPackageException {
        for (MediaPackageElement e : getElements()) {
            e.verify();
        }
    }

    /**
     * Unmarshals XML representation of a MediaPackage via JAXB.
     * 
     * @param xml
     *          the serialized xml string
     * @return the deserialized media package
     * @throws MediaPackageException
     */
    public static MediaPackageImpl valueOf(String xml) throws MediaPackageException {
        try {
            return MediaPackageImpl.valueOf(IOUtils.toInputStream(xml, "UTF-8"));
        } catch (IOException e) {
            throw new MediaPackageException(e);
        }
    }

    /**
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return getIdentifier().hashCode();
    }

    /**
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof MediaPackage) {
            return getIdentifier().equals(((MediaPackage) obj).getIdentifier());
        }
        return false;
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.lang.Object#clone()
     */
    @Override
    public Object clone() {
        try {
            String xml = MediaPackageParser.getAsXml(this);
            return MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().loadFromXml(xml);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        if (identifier != null)
            return identifier.toString();
        else
            return "Unknown media package";
    }

    /**
     * A JAXB adapter that allows the {@link MediaPackage} interface to be un/marshalled
     */
    static class Adapter extends XmlAdapter<MediaPackageImpl, MediaPackage> {
        @Override
        public MediaPackageImpl marshal(MediaPackage mp) throws Exception {
            return (MediaPackageImpl) mp;
        }

        @Override
        public MediaPackage unmarshal(MediaPackageImpl mp) throws Exception {
            return mp;
        }
    }

    /**
     * Reads the media package from the input stream.
     * 
     * @param xml
     *          the input stream
     * @return the deserialized media package
     */
    public static MediaPackageImpl valueOf(InputStream xml) throws MediaPackageException {
        try {
            Unmarshaller unmarshaller = context.createUnmarshaller();
            return unmarshaller.unmarshal(new StreamSource(xml), MediaPackageImpl.class).getValue();
        } catch (JAXBException e) {
            throw new MediaPackageException(e.getLinkedException() != null ? e.getLinkedException() : e);
        } finally {
            IoSupport.closeQuietly(xml);
        }
    }

    /**
     * Reads the media package from an xml node.
     * 
     * @param xml
     *          the node
     * @return the deserialized media package
     */
    public static MediaPackageImpl valueOf(Node xml) throws MediaPackageException {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        try {
            Unmarshaller unmarshaller = context.createUnmarshaller();

            // Serialize the media package
            DOMSource domSource = new DOMSource(xml);
            out = new ByteArrayOutputStream();
            StreamResult result = new StreamResult(out);
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.transform(domSource, result);
            in = new ByteArrayInputStream(out.toByteArray());

            return unmarshaller.unmarshal(new StreamSource(in), MediaPackageImpl.class).getValue();
        } catch (Exception e) {
            throw new MediaPackageException("Error deserializing media package node", e);
        } finally {
            IoSupport.closeQuietly(in);
            IoSupport.closeQuietly(out);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getContributors()
     */
    @Override
    public String[] getContributors() {
        if (contributors == null)
            return new String[] {};
        return contributors.toArray(new String[contributors.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getCreators()
     */
    @Override
    public String[] getCreators() {
        if (creators == null)
            return new String[] {};
        return creators.toArray(new String[creators.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getLanguage()
     */
    @Override
    public String getLanguage() {
        return language;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getLicense()
     */
    @Override
    public String getLicense() {
        return license;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getSeries()
     */
    @Override
    public String getSeries() {
        return series;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getSubjects()
     */
    @Override
    public String[] getSubjects() {
        if (subjects == null)
            return new String[] {};
        return subjects.toArray(new String[subjects.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getTitle()
     */
    @Override
    public String getTitle() {
        return title;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#getSeriesTitle()
     */
    @Override
    public String getSeriesTitle() {
        return seriesTitle;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#setSeriesTitle(java.lang.String)
     */
    @Override
    public void setSeriesTitle(String seriesTitle) {
        this.seriesTitle = seriesTitle;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#addContributor(java.lang.String)
     */
    @Override
    public void addContributor(String contributor) {
        if (contributors == null)
            contributors = new TreeSet<String>();
        contributors.add(contributor);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#addCreator(java.lang.String)
     */
    @Override
    public void addCreator(String creator) {
        if (creators == null)
            creators = new TreeSet<String>();
        creators.add(creator);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#addSubject(java.lang.String)
     */
    @Override
    public void addSubject(String subject) {
        if (subjects == null)
            subjects = new TreeSet<String>();
        subjects.add(subject);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#removeContributor(java.lang.String)
     */
    @Override
    public void removeContributor(String contributor) {
        if (contributors != null)
            contributors.remove(contributor);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#removeCreator(java.lang.String)
     */
    @Override
    public void removeCreator(String creator) {
        if (creators != null)
            creators.remove(creator);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#removeSubject(java.lang.String)
     */
    @Override
    public void removeSubject(String subject) {
        if (subjects != null)
            subjects.remove(subject);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#setDate(java.util.Date)
     */
    @Override
    public void setDate(Date date) {
        if (date != null)
            this.startTime = date.getTime();
        else
            this.startTime = 0;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#setLanguage(java.lang.String)
     */
    @Override
    public void setLanguage(String language) {
        this.language = language;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#setLicense(java.lang.String)
     */
    @Override
    public void setLicense(String license) {
        this.license = license;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#setSeries(java.lang.String)
     */
    @Override
    public void setSeries(String identifier) {
        this.series = identifier;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.opencastproject.mediapackage.MediaPackage#setTitle(java.lang.String)
     */
    @Override
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * Returns the media package element that matches the given reference.
     * 
     * @param reference
     *          the reference
     * @return the element
     */
    MediaPackageElement getElement(MediaPackageReference reference) {
        if (reference == null)
            return null;
        for (MediaPackageElement e : elements) {
            if (e.getIdentifier().equals(reference.getIdentifier()))
                return e;
        }
        return null;
    }

    /**
     * Registers a new media package element with this manifest.
     * 
     * @param element
     *          the new element
     */
    void addInternal(MediaPackageElement element) {
        if (element == null)
            throw new IllegalArgumentException("Media package element must not be null");
        String id = null;
        if (elements.add(element)) {
            if (element instanceof Track) {
                tracks++;
                id = "track-" + tracks;
                Long duration = ((Track) element).getDuration();
                // Todo Do not demand equal durations for now... This is an issue that has to be discussed further
                // if (this.duration > 0 && this.duration != duration)
                // throw new MediaPackageException("Track " + element + " cannot be added due to varying duration (" + duration
                // +
                // " instead of " + this.duration +")");
                // else
                if (this.duration == null)
                    this.duration = duration;
            } else if (element instanceof Attachment) {
                attachments++;
                id = "attachment-" + attachments;
            } else if (element instanceof Catalog) {
                catalogs++;
                id = "catalog-" + catalogs;
            } else {
                others++;
                id = "unknown-" + others;
            }
        }

        // Check if element has an id
        if (element.getIdentifier() == null) {
            if (element instanceof AbstractMediaPackageElement) {
                element.setIdentifier(id);
            } else
                throw new UnsupportedElementException(element, "Found unkown element without id");
        }
    }

    /**
     * Removes the media package element from the manifest.
     * 
     * @param element
     *          the element to remove
     */
    void removeInternal(MediaPackageElement element) {
        if (element == null)
            throw new IllegalArgumentException("Media package element must not be null");
        if (elements.remove(element)) {
            if (element instanceof Track) {
                tracks--;
                if (tracks == 0)
                    duration = null;
            } else if (element instanceof Attachment)
                attachments--;
            else if (element instanceof Catalog)
                catalogs--;
            else
                others--;
        }
    }

    /**
     * Extracts the list of tracks from the media package.
     * 
     * @return the tracks
     */
    private Collection<Track> loadTracks() {
        List<Track> tracks = new ArrayList<Track>();
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e instanceof Track) {
                    tracks.add((Track) e);
                }
            }
        }
        return tracks;
    }

    /**
     * Extracts the list of catalogs from the media package.
     * 
     * @return the catalogs
     */
    private Collection<Catalog> loadCatalogs() {
        List<Catalog> catalogs = new ArrayList<Catalog>();
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e instanceof Catalog) {
                    catalogs.add((Catalog) e);
                }
            }
        }
        return catalogs;
    }

    /**
     * Extracts the list of attachments from the media package.
     * 
     * @return the attachments
     */
    private Collection<Attachment> loadAttachments() {
        List<Attachment> attachments = new ArrayList<Attachment>();
        synchronized (elements) {
            for (MediaPackageElement e : elements) {
                if (e instanceof Attachment) {
                    attachments.add((Attachment) e);
                }
            }
        }
        return attachments;
    }

}