AbstractMediaPackageElement.java

/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you 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://opensource.org/licenses/ecl2.txt
 *
 * 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 org.opencastproject.util.Checksum;
import org.opencastproject.util.IoSupport;
import org.opencastproject.util.MimeType;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collection;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;

import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
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.XmlID;
import javax.xml.bind.annotation.XmlTransient;

/**
 * This class provides base functionality for media package elements.
 */
@XmlTransient
@XmlAccessorType(XmlAccessType.NONE)
public abstract class AbstractMediaPackageElement implements MediaPackageElement, Serializable {

  /** Serial version uid */
  private static final long serialVersionUID = 1L;

  /** The element identifier */
  @XmlID
  @XmlAttribute(name = "id")
  protected String id = null;

  /** The element's type whithin the manifest: Track, Catalog etc. */
  protected Type elementType = null;

  /** The element's description */
  protected String description = null;

  /** The element's mime type, e. g. 'audio/mp3' */
  @XmlElement(name = "mimetype")
  protected MimeType mimeType = null;

  /** The element's type, e. g. 'track/slide' */
  @XmlAttribute(name = "type")
  protected MediaPackageElementFlavor flavor = null;

  /** The tags */
  @XmlElementWrapper(name = "tags")
  @XmlElement(name = "tag")
  protected SortedSet<String> tags = new TreeSet<String>();

  /** The element's location */
  @XmlElement(name = "url")
  protected URI uri = null;

  /** Size in bytes */
  @XmlElement(name = "size")
  protected Long size = null;

  /** The element's checksum */
  @XmlElement(name = "checksum")
  protected Checksum checksum = null;

  /** The parent media package */
  protected MediaPackage mediaPackage = null;

  /** The optional reference to other elements or series */
  @XmlAttribute(name = "ref")
  protected MediaPackageReference reference = null;

  /** Needed by JAXB */
  protected AbstractMediaPackageElement() {
  }

  /**
   * Creates a new media package element.
   *
   * @param elementType
   *          the type, e. g. Track, Catalog etc.
   * @param flavor
   *          the flavor
   * @param uri
   *          the elements location
   */
  protected AbstractMediaPackageElement(Type elementType, MediaPackageElementFlavor flavor, URI uri) {
    this(null, elementType, flavor, uri, null, null, null);
  }

  /**
   * Creates a new media package element.
   *
   * @param elementType
   *          the type, e. g. Track, Catalog etc.
   * @param flavor
   *          the flavor
   * @param uri
   *          the elements location
   * @param size
   *          the element size in bytes
   * @param checksum
   *          the element checksum
   * @param mimeType
   *          the element mime type
   */
  protected AbstractMediaPackageElement(Type elementType, MediaPackageElementFlavor flavor, URI uri, Long size,
          Checksum checksum, MimeType mimeType) {
    this(null, elementType, flavor, uri, size, checksum, mimeType);
  }

  /**
   * Creates a new media package element.
   *
   * @param id
   *          the element identifier withing the package
   * @param elementType
   *          the type, e. g. Track, Catalog etc.
   * @param flavor
   *          the flavor
   * @param uri
   *          the elements location
   * @param size
   *          the element size in bytes
   * @param checksum
   *          the element checksum
   * @param mimeType
   *          the element mime type
   */
  protected AbstractMediaPackageElement(String id, Type elementType, MediaPackageElementFlavor flavor, URI uri,
          Long size, Checksum checksum, MimeType mimeType) {
    if (elementType == null)
      throw new IllegalArgumentException("Argument 'elementType' is null");
    this.id = id;
    this.elementType = elementType;
    this.flavor = flavor;
    this.mimeType = mimeType;
    this.uri = uri;
    this.size = size;
    this.checksum = checksum;
    this.tags = new TreeSet<String>();
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#setIdentifier(String)
   */
  @Override
  public void setIdentifier(String id) {
    this.id = id;
  }

  @Override
  public String generateIdentifier() {
    id = UUID.randomUUID().toString();
    return id;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getIdentifier()
   */
  @Override
  public String getIdentifier() {
    return id;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#setTags(java.lang.String[])
   */
  @Override
  public void setTags(String[] tags) {
    this.tags = new TreeSet<String>(Arrays.asList(tags));
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#addTag(java.lang.String)
   */
  @Override
  public void addTag(String tag) {
    if (tag == null)
      throw new IllegalArgumentException("Tag must not be null");
    tags.add(tag);
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#removeTag(java.lang.String)
   */
  @Override
  public void removeTag(String tag) {
    if (tag == null)
      return;
    tags.remove(tag);
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#containsTag(java.lang.String)
   */
  @Override
  public boolean containsTag(String tag) {
    if (tag == null || tags == null)
      return false;
    return tags.contains(tag);
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#containsTag(java.util.Collection)
   */
  @Override
  public boolean containsTag(Collection<String> tags) {
    if (tags == null || tags.size() == 0)
      return true;
    for (String tag : tags) {
      if (containsTag(tag))
        return true;
    }
    return false;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getTags()
   */
  @Override
  public String[] getTags() {
    return tags.toArray(new String[tags.size()]);
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#clearTags()
   */
  @Override
  public void clearTags() {
    if (tags != null)
      tags.clear();
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getMediaPackage()
   */
  @Override
  public MediaPackage getMediaPackage() {
    return mediaPackage;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getElementType()
   */
  @Override
  public Type getElementType() {
    return elementType;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getElementDescription()
   */
  @Override
  public String getElementDescription() {
    return (description != null) ? description : uri.toString();
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#setElementDescription(String)
   */
  @Override
  public void setElementDescription(String name) {
    this.description = name;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getReference()
   */
  @Override
  public MediaPackageReference getReference() {
    return reference;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#setReference(MediaPackageReference)
   */
  @Override
  public void setReference(MediaPackageReference reference) {
    this.reference = reference;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getURI()
   */
  @Override
  public URI getURI() {
    return uri;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#setURI(java.net.URI)
   */
  @Override
  public void setURI(URI uri) {
    this.uri = uri;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getChecksum()
   */
  @Override
  public Checksum getChecksum() {
    return checksum;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#setChecksum(org.opencastproject.util.Checksum)
   */
  @Override
  public void setChecksum(Checksum checksum) {
    this.checksum = checksum;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getMimeType()
   */
  @Override
  public MimeType getMimeType() {
    return mimeType;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#setMimeType(org.opencastproject.util.MimeType)
   */
  @Override
  public void setMimeType(MimeType mimeType) {
    this.mimeType = mimeType;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#setFlavor(MediaPackageElementFlavor)
   */
  @Override
  public void setFlavor(MediaPackageElementFlavor flavor) {
    this.flavor = flavor;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getFlavor()
   */
  @Override
  public MediaPackageElementFlavor getFlavor() {
    return flavor;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#getSize()
   */
  @Override
  public long getSize() {
    return size != null ? size : -1;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#setSize(long)
   */
  @Override
  public void setSize(long size) {
    this.size = size;
  }

  /**
   * Sets the parent media package.
   * <p>
   * <b>Note</b> This method is only used by the media package and should not be called from elsewhere.
   *
   * @param mediaPackage
   *          the parent media package
   */
  void setMediaPackage(MediaPackage mediaPackage) {
    this.mediaPackage = mediaPackage;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#referTo(org.opencastproject.mediapackage.MediaPackageElement)
   */
  @Override
  public void referTo(MediaPackageElement element) {
    referTo(new MediaPackageReferenceImpl(element));
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#referTo(org.opencastproject.mediapackage.MediaPackageReference)
   */
  @Override
  public void referTo(MediaPackageReference reference) {
    // TODO: Check reference consistency
    this.reference = reference;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#clearReference()
   */
  @Override
  public void clearReference() {
    this.reference = null;
  }

  /**
   * @see org.opencastproject.mediapackage.MediaPackageElement#verify()
   */
  @Override
  public void verify() throws MediaPackageException {
    // TODO: Check availability at url
    // TODO: Download (?) and check checksum
    // Checksum c = calculateChecksum();
    // if (checksum != null && !checksum.equals(c)) {
    // throw new MediaPackageException("Checksum mismatch for " + this);
    // }
    // checksum = c;
  }

  /**
   * @see java.lang.Comparable#compareTo(java.lang.Object)
   */
  @Override
  public int compareTo(MediaPackageElement o) {
    return uri.toString().compareTo(o.getURI().toString());
  }

  /**
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof MediaPackageElement))
      return false;
    MediaPackageElement e = (MediaPackageElement) obj;
    if (mediaPackage != null && e.getMediaPackage() != null && !mediaPackage.equals(e.getMediaPackage()))
      return false;
    if (id != null && !id.equals(e.getIdentifier()))
      return false;
    if (uri != null && !uri.equals(e.getURI()))
      return false;
    return true;
  }

  /**
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((id == null) ? 0 : id.hashCode());
    result = prime * result + ((mediaPackage == null) ? 0 : mediaPackage.hashCode());
    result = prime * result + ((uri == null) ? 0 : uri.hashCode());
    return result;
  }

  /**
   * @see org.opencastproject.mediapackage.ManifestContributor#toManifest(org.w3c.dom.Document,
   *      org.opencastproject.mediapackage.MediaPackageSerializer)
   */
  @Override
  public Node toManifest(Document document, MediaPackageSerializer serializer) throws MediaPackageException {
    Element node = document.createElement(elementType.toString().toLowerCase());
    if (id != null)
      node.setAttribute("id", id);

    // Flavor
    if (flavor != null)
      node.setAttribute("type", flavor.toString());

    // Reference
    if (reference != null)
      if (mediaPackage == null || !reference.matches(new MediaPackageReferenceImpl(mediaPackage)))
        node.setAttribute("ref", reference.toString());

    // Description
    if (description != null) {
      Element descriptionNode = document.createElement("description");
      descriptionNode.appendChild(document.createTextNode(description));
      node.appendChild(descriptionNode);
    }

    // Tags
    if (tags.size() > 0) {
      Element tagsNode = document.createElement("tags");
      node.appendChild(tagsNode);
      for (String tag : tags) {
        Element tagNode = document.createElement("tag");
        tagsNode.appendChild(tagNode);
        tagNode.appendChild(document.createTextNode(tag));
      }
    }

    // Url
    Element urlNode = document.createElement("url");
    String urlValue;
    try {
      urlValue = (serializer != null) ? serializer.encodeURI(uri).toString() : uri.toString();
    } catch (URISyntaxException e) {
      throw new MediaPackageException(e);
    }
    urlNode.appendChild(document.createTextNode(urlValue));
    node.appendChild(urlNode);

    // MimeType
    if (mimeType != null) {
      Element mimeNode = document.createElement("mimetype");
      mimeNode.appendChild(document.createTextNode(mimeType.toString()));
      node.appendChild(mimeNode);
    }

    // Size
    if (size != null && size != -1) {
      Element sizeNode = document.createElement("size");
      sizeNode.appendChild(document.createTextNode(Long.toString(size)));
      node.appendChild(sizeNode);
    }

    // Checksum
    if (checksum != null) {
      Element checksumNode = document.createElement("checksum");
      checksumNode.setAttribute("type", checksum.getType().getName());
      checksumNode.appendChild(document.createTextNode(checksum.getValue()));
      node.appendChild(checksumNode);
    }

    return node;
  }

  /**
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    String s = (description != null) ? description : uri.toString();
    return s.toLowerCase();
  }

  /**
   * Attention: The media package reference is not being cloned so that calling <code>getMediaPackage()</code> on the
   * clone yields null.
   */
  @Override
  public Object clone() {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    ByteArrayInputStream in = null;
    try {
      Marshaller marshaller = MediaPackageImpl.context.createMarshaller();
      marshaller.marshal(this, out);
      Unmarshaller unmarshaller = MediaPackageImpl.context.createUnmarshaller();
      in = new ByteArrayInputStream(out.toByteArray());
      // CHECKSTYLE:OFF
      // in was already parsed and is therefore save
      return unmarshaller.unmarshal(in);
      // CHECKSTYLE:ON
    } catch (JAXBException e) {
      throw new RuntimeException(e.getLinkedException() != null ? e.getLinkedException() : e);
    } finally {
      IoSupport.closeQuietly(in);
    }
  }

}