BinaryProductHandler.java

package gov.usgs.earthquake.product.io;

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

import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.List;
import java.util.Map;

/**
 * Generator of binary format for product data.
 *
 * Binary representation of data types:
 * <dl>
 * <dt>Integer</dt>
 * <dd>4-bytes</dd>
 * <dt>Long</dt>
 * <dd>8-bytes</dd>
 * <dt>Date</dt>
 * <dd>Long (Date.getTime())</dd>
 * <dt>byte[]</dt>
 * <dd>Integer length, raw bytes</dd>
 * <dt>String</dt>
 * <dd>byte[] (String.getBytes(StandardCharsets.UTF_8))</dd>
 * <dt>URL/URI</dt>
 * <dd>String (URL.toString())</dd>
 * </dl>
 *
 *
 * Product is stored in this order:
 *
 * <ol>
 *
 * <li>Header, exactly 1
 * <ol>
 * <li>"BEGINPRODUCT" (string)</li>
 * <li>ProductId (String)</li>
 * <li>Status (String)</li>
 * </ol>
 * </li>
 *
 * <li>Properties, 0 to many:
 * <ol>
 * <li>"PROPERTY" (String)</li>
 * <li>name (String)</li>
 * <li>value (String)</li>
 * </ol>
 * </li>
 *
 * <li>Links, 0 to many:
 * <ol>
 * <li>"LINK" (String)</li>
 * <li>relation (String)</li>
 * <li>href (URI)</li>
 * </ol>
 * </li>
 *
 * <li>Contents, 0 to many:
 * <ol>
 * <li>"CONTENT" (String)</li>
 * <li>path (String)</li>
 * <li>contentType (String)</li>
 * <li>lastModified (Date)</li>
 * <li>length (Long)</li>
 * <li>raw bytes</li>
 * </ol>
 * </li>
 *
 * <li>Signature Version, 0 or 1. <em>Note, only sent when version !=
 * SIGNATURE_V1 for backward compatibility</em>
 * <ol>
 * <li>"SIGNATUREVERSION" (String)</li>
 * <li>version (String)</li>
 * </ol>
 * </li>
 *
 * <li>Signature, 0 or 1:
 * <ol>
 * <li>"SIGNATURE" (String)</li>
 * <li>signature (String)</li>
 * </ol>
 * </li>
 *
 * <li>Footer, exactly 1:
 * <ol>
 * <li>"ENDPRODUCT" (String)</li>
 * </ol>
 * </li>
 *
 * </ol>
 */
public class BinaryProductHandler implements ProductHandler {

  /** HEADER - BEGINPRODUCT */
  public static final String HEADER = "BEGINPRODUCT";
  /** PROPERTY */
  public static final String PROPERTY = "PROPERTY";
  /** LINK */
  public static final String LINK = "LINK";
  /** CONTENT */
  public static final String CONTENT = "CONTENT";
  /** SIGNATURE VERSION */
  public static final String SIGNATUREVERSION = "SIGNATUREVERSION";
  /** SIGNATURE */
  public static final String SIGNATURE = "SIGNATURE";
  /** SIGNATURE HISTORY */
  public static final String SIGNATUREHISTORY = "SIGNATUREHISTORY";
  /** map entry for SIGNATURE HISTORY list */
  public static final String SIGNATUREHISTORYENTRY = "SIGNATUREHISTORYENTRY";
  /** End of SIGNATURE HISTORY list */
  public static final String ENDSIGNATUREHISTORY = "ENDSIGNATUREHISTORY";
  /** ENDPRODUCT */
  public static final String FOOTER = "ENDPRODUCT";

  private OutputStream out;
  private BinaryIO io;

  /**
   * Constructor. Sets up a new BinaryIO
   *
   * @param out an OutputStream
   */
  public BinaryProductHandler(final OutputStream out) {
    this.out = out;
    this.io = new BinaryIO();
  }

  @Override
  public void onBeginProduct(ProductId id, String status) throws Exception {
    io.writeString(HEADER, out);
    io.writeString(id.toString(), out);
    io.writeString(status, out);
  }

  @Override
  public void onProperty(ProductId id, String name, String value) throws Exception {
    io.writeString(PROPERTY, out);
    io.writeString(name, out);
    io.writeString(value, out);
  }

  @Override
  public void onLink(ProductId id, String relation, URI href) throws Exception {
    io.writeString(LINK, out);
    io.writeString(relation, out);
    io.writeString(href.toString(), out);
  }

  @Override
  public void onContent(ProductId id, String path, Content content) throws Exception {
    if (content.getLength() == null || content.getLength() < 0) {
      // binary io only handles streams with length, convert to content
      // with length
      content = new ByteContent(content);
    }

    io.writeString(CONTENT, out);
    io.writeString(path, out);

    io.writeString(content.getContentType(), out);
    io.writeDate(content.getLastModified(), out);
    InputStream contentInputStream = content.getInputStream();
    try {
      io.writeStream(content.getLength().longValue(), contentInputStream, out);
    } finally {
      StreamUtils.closeStream(contentInputStream);
    }
  }

  @Override
  public void onSignatureVersion(ProductId id, Version version) throws Exception {
    if (version != Version.SIGNATURE_V1) {
      io.writeString(SIGNATUREVERSION, out);
      io.writeString(version.toString(), out);
    }
  }

  @Override
  public void onSignature(ProductId id, String signature) throws Exception {
    // allow signature to be null
    if (signature == null) {
      return;
    }

    io.writeString(SIGNATURE, out);
    io.writeString(signature, out);
  }

  public void onSignatureHistory(final ProductId id, final List<ProductSignature> signatureHistory)
      throws Exception {
    if (signatureHistory.isEmpty()) {
      return;
    }
    io.writeString(SIGNATUREHISTORY, out);
    for (ProductSignature entry : signatureHistory) {
      io.writeString(SIGNATUREHISTORYENTRY, out);
      io.writeString(entry.getSignature(), out);
      io.writeString(entry.getSignatureVersion().toString(), out);
    }
    io.writeString(ENDSIGNATUREHISTORY, out);
  }

  @Override
  public void onEndProduct(ProductId id) throws Exception {
    io.writeString(FOOTER, out);

    out.flush();
    out.close();
  }

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

}