XmlProductHandler.java

/*
 * XmlProductHandler
 */
package gov.usgs.earthquake.product.io;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlElement;

import gov.usgs.util.StreamUtils;
import gov.usgs.util.XmlUtils;
import gov.usgs.util.CryptoUtils.Version;
import gov.usgs.earthquake.product.Content;
import gov.usgs.earthquake.product.URLContent;
import gov.usgs.earthquake.product.ProductId;
import gov.usgs.earthquake.product.ProductSignature;

/**
 * Store a product to an OutputStream using XML.
 */
public class XmlProductHandler implements ProductHandler {

  /** Declaration for XML and version */
  public static final String XML_DECLARATION = "<?xml version=\"1.0\"?>\n";
  /** Namespace for XML product */
  public static final String PRODUCT_XML_NAMESPACE = "http://earthquake.usgs.gov/distribution/product";

  /** Element for product */
  public static final String PRODUCT_ELEMENT = "product";
  /** Product Attribute for id */
  public static final String PRODUCT_ATTRIBUTE_ID = "id";
  /** Product Attribute for updateTime */
  public static final String PRODUCT_ATTRIBUTE_UPDATED = "updateTime";
  /** Product Attribute for status */
  public static final String PRODUCT_ATTRIBUTE_STATUS = "status";

  /** Element for property */
  public static final String PROPERTY_ELEMENT = "property";
  /** Property attribute for name */
  public static final String PROPERTY_ATTRIBUTE_NAME = "name";
  /** Property attribute for value */
  public static final String PROPERTY_ATTRIBUTE_VALUE = "value";

  /** Element for link */
  public static final String LINK_ELEMENT = "link";
  /** Link attribute for relation */
  public static final String LINK_ATTRIBUTE_RELATION = "rel";
  /** Link attribute for href */
  public static final String LINK_ATTRIBUTE_HREF = "href";

  /** Element for content */
  public static final String CONTENT_ELEMENT = "content";
  /** Content attribute for path */
  public static final String CONTENT_ATTRIBUTE_PATH = "path";
  /** Content attribute for type */
  public static final String CONTENT_ATTRIBUTE_TYPE = "type";
  /** Content attribute for length */
  public static final String CONTENT_ATTRIBUTE_LENGTH = "length";
  /** Content attribute for modified */
  public static final String CONTENT_ATTRIBUTE_MODIFIED = "modified";
  /** Used with URLContent. */
  public static final String CONTENT_ATTRIBUTE_HREF = "href";
  /** Content attribute for encoded */
  public static final String CONTENT_ATTRIBUTE_ENCODED = "encoded";

  /** Element for signature */
  public static final String SIGNATURE_ELEMENT = "signature";
  /** Signature attribute for version */
  public static final String SIGNATURE_ATTRIBUTE_VERSION = "version";

  /** Element for signature history */
  public static final String SIGNATURE_HISTORY_ELEMENT = "history";
  /** Element for entries to the signature history */
  public static final String SIGNATURE_HISTORY_ENTRY_ELEMENT = "entry";
  /** Signature history attribute for signature */
  public static final String SIGNATURE_HISTORY_SIGNATURE_ATTRIBUTE_ = "signature";
  /** Signature history attribute for signature version */
  public static final String SIGNATURE_HISTORY_VERSION_ATTRIBUTE_ = "signatureVersion";

  /** The OutputStream where xml is written. */
  private OutputStream out;

  /** Controls whether the XML Declaration is output with the XML. */
  private boolean includeDeclaration = true;

  /** Signature version. */
  private Version signatureVersion = Version.SIGNATURE_V2;

  /**
   * Create a new XmlProductHandler object.
   *
   * @param out the OutputStream where xml will be written.
   */
  public XmlProductHandler(final OutputStream out) {
    this.out = out;
  }

  /**
   * Create a new XmlProductHandler object.
   *
   * @param out                the OutputStream where xml will be written.
   * @param includeDeclaration whether to include the XML declaration with output
   */
  public XmlProductHandler(final OutputStream out, boolean includeDeclaration) {
    this.out = out;
    this.includeDeclaration = includeDeclaration;
  }

  /**
   * Output the product root element.
   */
  public void onBeginProduct(ProductId id, String status) throws Exception {
    StringBuffer buf = new StringBuffer();
    if (includeDeclaration) {
      buf.append(XML_DECLARATION);
    }
    buf.append("<").append(PRODUCT_ELEMENT);
    buf.append(" xmlns=\"").append(PRODUCT_XML_NAMESPACE).append("\"");
    buf.append(" ").append(PRODUCT_ATTRIBUTE_ID).append("=\"").append(XmlUtils.escape(id.toString())).append("\"");
    buf.append(" ").append(PRODUCT_ATTRIBUTE_UPDATED).append("=\"").append(XmlUtils.formatDate(id.getUpdateTime()))
        .append("\"");
    buf.append(" ").append(PRODUCT_ATTRIBUTE_STATUS).append("=\"").append(XmlUtils.escape(status)).append("\"");
    buf.append(">\n");

    out.write(buf.toString().getBytes());
  }

  /**
   * Output a content object as xml.
   */
  public void onContent(ProductId id, String path, Content content) throws Exception {
    // open element
    StringBuffer buf = new StringBuffer();
    buf.append("\t<").append(CONTENT_ELEMENT);
    buf.append(" ").append(CONTENT_ATTRIBUTE_PATH).append("=\"").append(XmlUtils.escape(path)).append("\"");
    buf.append(" ").append(CONTENT_ATTRIBUTE_TYPE).append("=\"").append(XmlUtils.escape(content.getContentType()))
        .append("\"");
    buf.append(" ").append(CONTENT_ATTRIBUTE_LENGTH).append("=\"").append(content.getLength()).append("\"");
    buf.append(" ").append(CONTENT_ATTRIBUTE_MODIFIED).append("=\"")
        .append(XmlUtils.formatDate(content.getLastModified())).append("\"");

    if (content instanceof URLContent) {
      // URL CONTENT
      buf.append(" ").append(CONTENT_ATTRIBUTE_HREF).append("=\"").append(((URLContent) content).getURL().toString())
          .append("\"");

      // close element early, url is alternative to embedded content
      buf.append("/>");
      out.write(buf.toString().getBytes());
      return;
    }

    else {
      // EMBEDDED CONTENT
      buf.append(" ").append(CONTENT_ATTRIBUTE_ENCODED).append("=\"true\"");

      buf.append(">");
      out.write(buf.toString().getBytes());

      InputStream in = null;
      OutputStream base64out = null;
      try {
        in = content.getInputStream();
        base64out = Base64.getEncoder().wrap(new StreamUtils.UnclosableOutputStream(out));
        // write element content
        StreamUtils.transferStream(in, base64out);
      } finally {
        StreamUtils.closeStream(in);
        StreamUtils.closeStream(base64out);
      }

      // close element
      buf = new StringBuffer();
      buf.append("</").append(CONTENT_ELEMENT).append(">\n");
      out.write(buf.toString().getBytes());
    }
  }

  /**
   * Output the closing product element.
   */
  public void onEndProduct(ProductId id) throws Exception {
    StringBuffer buf = new StringBuffer();
    buf.append("</").append(PRODUCT_ELEMENT).append(">\n");

    out.write(buf.toString().getBytes());
    out.flush();
    out.close();
  }

  /**
   * Output a link element as xml.
   */
  public void onLink(ProductId id, String relation, URI href) throws Exception {
    StringBuffer buf = new StringBuffer();
    buf.append("\t<").append(LINK_ELEMENT);
    buf.append(" ").append(LINK_ATTRIBUTE_RELATION).append("=\"").append(XmlUtils.escape(relation)).append("\"");
    buf.append(" ").append(LINK_ATTRIBUTE_HREF).append("=\"").append(XmlUtils.escape(href.toString())).append("\"");
    buf.append("/>\n");

    out.write(buf.toString().getBytes());
  }

  /**
   * Output the property element as xml.
   */
  public void onProperty(ProductId id, String name, String value) throws Exception {
    StringBuffer buf = new StringBuffer();
    buf.append("\t<").append(PROPERTY_ELEMENT);
    buf.append(" ").append(PROPERTY_ATTRIBUTE_NAME).append("=\"").append(XmlUtils.escape(name)).append("\"");
    buf.append(" ").append(PROPERTY_ATTRIBUTE_VALUE).append("=\"").append(XmlUtils.escape(value)).append("\"");
    buf.append("/>\n");

    out.write(buf.toString().getBytes());
  }

  public void onSignatureVersion(ProductId id, Version version) throws Exception {
    // save for output during onSignature
    this.signatureVersion = version;
  }

  /**
   * Output the signature element as xml.
   */
  public void onSignature(ProductId id, String signature) throws Exception {
    if (signature == null) {
      return;
    }

    StringBuffer buf = new StringBuffer();
    buf.append("\t<").append(SIGNATURE_ELEMENT);
    buf.append(" ").append(SIGNATURE_ATTRIBUTE_VERSION).append("=\"").append(signatureVersion.toString()).append("\"");
    buf.append(">");
    buf.append(signature);
    buf.append("</").append(SIGNATURE_ELEMENT).append(">\n");

    out.write(buf.toString().getBytes());
  }

  public void onSignatureHistory(ProductId id, final List<ProductSignature> signatureHistory) throws Exception {
    if (signatureHistory.isEmpty()) {
      return;
    }
    StringBuffer buf = new StringBuffer();
    buf.append("\t<").append(SIGNATURE_HISTORY_ELEMENT).append(">\n");
    for (ProductSignature entry : signatureHistory) {
      buf.append("\t\t<entry");
      buf.append(" ").append("signature").append("=\"").append(XmlUtils.escape(entry.getSignature())).append("\"");
      buf.append(" ").append("signatureVersion").append("=\"")
          .append(XmlUtils.escape(entry.getSignatureVersion().toString()))
          .append("\"");
      buf.append("/>\n");
    }

    buf.append("\t</").append(SIGNATURE_HISTORY_ELEMENT).append(">\n");
    out.write(buf.toString().getBytes());

  }

  /**
   * Free any resources associated with this handler.
   */
  @Override
  public void close() {
    StreamUtils.closeStream(out);
  }

}