EQMessageProductCreator.java

package gov.usgs.earthquake.eids;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

//Does it make sense to import objects from quakeml when we're parsing eqxml?
//import org.quakeml.FocalMechanism;
//import org.quakeml.NodalPlane;
//import org.quakeml.NodalPlanes;

import gov.usgs.ansseqmsg.Action;
import gov.usgs.ansseqmsg.Comment;
import gov.usgs.ansseqmsg.EQMessage;
import gov.usgs.ansseqmsg.EventAction;
import gov.usgs.ansseqmsg.EventScope;
import gov.usgs.ansseqmsg.EventType;
import gov.usgs.ansseqmsg.EventUsage;
import gov.usgs.ansseqmsg.Fault;
import gov.usgs.ansseqmsg.Magnitude;
import gov.usgs.ansseqmsg.Method;
import gov.usgs.ansseqmsg.MomentTensor;
import gov.usgs.ansseqmsg.NodalPlanes;
import gov.usgs.ansseqmsg.Origin;
import gov.usgs.ansseqmsg.Event;
import gov.usgs.ansseqmsg.ProductLink;
import gov.usgs.ansseqmsg.Tensor;
import gov.usgs.earthquake.cube.CubeAddon;
import gov.usgs.earthquake.cube.CubeDelete;
import gov.usgs.earthquake.cube.CubeEvent;
import gov.usgs.earthquake.cube.CubeMessage;
import gov.usgs.earthquake.distribution.ProductCreator;
import gov.usgs.earthquake.eqxml.EQMessageParser;
import gov.usgs.earthquake.event.Converter;
import gov.usgs.earthquake.product.ByteContent;
import gov.usgs.earthquake.product.Content;
import gov.usgs.earthquake.product.Product;
import gov.usgs.earthquake.product.ProductId;
import gov.usgs.util.FileUtils;
import gov.usgs.util.StreamUtils;
import gov.usgs.util.XmlUtils;

/**
 * Convert EQXML messages to Products.
 *
 * <p>
 * Product source is EQMessage/Source.
 * </p>
 * <p>
 * Product type is "origin", "magnitude", or "addon". Types may be prefixed by
 * non-Public Event/Scope, and suffixed by non-Actual Event/Usage
 * (internal-magnitude-scenario).
 * </p>
 * <p>
 * Product code is Event/DataSource + Event/EventId. When an addon product,
 * either ProductLink/Code or Comment/TypeKey is appended to code.
 * </p>
 * <p>
 * Product updateTime is EQMessage/Sent.
 * </p>
 *
 * <p>
 * Origin properties appear only on origin type products. Magnitude properties
 * appear on both magnitude and origin products.
 * </p>
 */
public class EQMessageProductCreator implements ProductCreator {

  private static final Logger LOGGER = Logger.getLogger(EQMessageProductCreator.class.getName());

  /** Static var for the xml content type */
  public static final String XML_CONTENT_TYPE = "application/xml";

  /** Path to content where source message is stored in created product. */
  public static final String EQMESSAGE_CONTENT_PATH = "eqxml.xml";
  /** Path to contests xml */
  public static final String CONTENTS_XML_PATH = "contents.xml";

  /**
   * When phases exist is is a "phase" type product. When this flag is set to
   * true, a lightweight, origin-only type product is also sent.
   */
  private boolean sendOriginWhenPhasesExist = false;

  /**
   * Whether to validate when parsing and serializing. When validating, only
   * native EQXML is supported via the ProductCreator interface.
   */
  private boolean validate = false;

  // the eqmessage currently being processed.
  private EQMessage eqmessage;

  // xml for the eqmessage currently being processed
  private String eqmessageXML;
  private String eqmessageSource;
  private Date eqmessageSent;

  private String eventDataSource;
  private String eventEventId;
  private String eventVersion;
  private EventAction eventAction;
  private EventUsage eventUsage;
  private EventScope eventScope;

  private BigDecimal originLatitude;
  private BigDecimal originLongitude;
  private BigDecimal originDepth;
  private Date originEventTime;

  private BigDecimal magnitude;

  /**
   * Default, empty constructor.
   */
  public EQMessageProductCreator() {
  }

  /**
   * Get all the products contained in an EQMessage.
   *
   * Same as getEQMessageProducts(message, null).
   *
   * @param message the EQMessage containing products.
   * @return a list of created products.
   * @throws Exception if error occurs
   */
  public synchronized List<Product> getEQMessageProducts(final EQMessage message) throws Exception {
    return getEQMessageProducts(message, null);
  }

  /**
   * Get all the products contained in an EQMessage.
   *
   * Parses rawEqxml string into an EQMessage, but preserves raw eqxml in created
   * products.
   *
   * Same as getEQMessageProducts(EQMessageParser.parse(rawEqxml), rawEqxml);
   *
   * @param rawEqxml the raw EQXML message.
   * @return a list of created products.
   * @throws Exception if error occurs
   */
  public synchronized List<Product> getEQMessageProducts(final String rawEqxml) throws Exception {
    EQMessage message = EQMessageParser.parse(rawEqxml, validate);
    return getEQMessageProducts(message, rawEqxml);
  }

  /**
   * Get all the products contained in an EQMessage.
   *
   * @param message  the EQMessage containing products.
   * @param rawEqxml the raw EQXML message. When null, an EQXML message is
   *                 serialized from the object.
   * @return a list of created products.
   * @throws Exception if error occurs
   */
  public synchronized List<Product> getEQMessageProducts(final EQMessage message, final String rawEqxml)
      throws Exception {
    List<Product> products = new LinkedList<Product>();

    if (message == null) {
      return products;
    }

    this.eqmessage = message;
    this.eqmessageSource = message.getSource();
    this.eqmessageSent = message.getSent();

    if (this.eqmessageSent == null) {
      this.eqmessageSent = new Date();
    }

    // convert to xml
    if (rawEqxml != null) {
      this.eqmessageXML = rawEqxml;
    } else {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      EQMessageParser.serialize(message, baos, validate);
      this.eqmessageXML = baos.toString();
    }

    // process each event
    List<Event> events = message.getEvent();
    if (events != null) {
      Iterator<Event> iter = events.iterator();
      while (iter.hasNext()) {
        products.addAll(getEventProducts(iter.next()));
      }
    }

    this.eqmessageSource = null;
    this.eqmessageSent = null;
    this.eqmessageXML = null;

    return products;
  }

  /**
   * Get products from an event.
   *
   * @param event the event containing products.
   * @return a list of created products.
   * @throws Exception if error occurs
   */
  protected synchronized List<Product> getEventProducts(final Event event) throws Exception {
    List<Product> products = new LinkedList<Product>();
    if (event == null) {
      return products;
    }

    eventDataSource = event.getDataSource();
    eventEventId = event.getEventID();
    eventVersion = event.getVersion();
    eventAction = event.getAction();
    eventUsage = event.getUsage();
    eventScope = event.getScope();

    // default values
    if (eventAction == null) {
      eventAction = EventAction.UPDATE;
    }
    if (eventUsage == null) {
      eventUsage = EventUsage.ACTUAL;
    }
    if (eventScope == null) {
      eventScope = EventScope.PUBLIC;
    }

    if (eventAction == EventAction.DELETE) {
      // delete origin product (only product with location)
      Product deleteProduct = getProduct("origin", eventAction.toString());
      products.add(deleteProduct);
    } else {
      // update origin product
      products.addAll(getOriginProducts(event.getOrigin(), event));
    }

    for (ProductLink eventLink : event.getProductLink()) {
      products.addAll(getProductLinkProducts(eventLink));
    }
    for (Comment eventComment : event.getComment()) {
      products.addAll(getCommentProducts(eventComment));
    }

    eventDataSource = null;
    eventEventId = null;
    eventVersion = null;
    eventAction = null;
    eventUsage = null;
    eventScope = null;

    return products;
  }

  /**
   * Get origin product(s).
   *
   * This implementation only creates one origin (the first one) regardless of how
   * many origins are provided.
   *
   * @param origins the list of origins.
   * @param event   A specific event
   * @return a list of created products.
   * @throws Exception if error occurs
   */
  protected synchronized List<Product> getOriginProducts(final List<Origin> origins, final Event event)
      throws Exception {
    List<Product> products = new LinkedList<Product>();
    if (origins == null || origins.size() == 0) {
      return products;
    }

    // only process first origin
    Origin origin = origins.get(0);

    // get sub-products
    products.addAll(getFocalMechanismProducts(origin.getMomentTensor()));

    this.originLatitude = origin.getLatitude();
    this.originLongitude = origin.getLongitude();
    this.originDepth = origin.getDepth();
    this.originEventTime = origin.getTime();

    boolean preferred = (origin.getPreferredFlag() == null || origin.getPreferredFlag());

    // only process "preferred" origins
    // this is how hydra differentiates between origins as input parameters
    // to focal mechanisms, and origins as origins
    if (preferred && this.originLatitude != null && this.originLongitude != null && this.originEventTime != null) {
      // create an origin/magnitude product only if origin has
      // lat+lon+time

      List<Product> magnitudeProducts = getMagnitudeProducts(origin.getMagnitude());

      // now build origin product
      Action originAction = origin.getAction();
      Product originProduct = getProduct("origin", originAction == null ? null : originAction.toString());
      // origin specific properties
      Map<String, String> properties = originProduct.getProperties();

      // set event type
      properties.put("event-type",
          (event.getType() == null ? EventType.EARTHQUAKE : event.getType()).value().toLowerCase());

      if (magnitudeProducts.size() > 0) {
        // transfer magnitude product properties to origin
        properties.putAll(magnitudeProducts.get(0).getProperties());
      }

      if (origin.getSourceKey() != null) {
        properties.put("origin-source", origin.getSourceKey());
      }
      if (origin.getAzimGap() != null) {
        properties.put("azimuthal-gap", origin.getAzimGap().toString());
      }
      if (origin.getDepthError() != null) {
        properties.put("depth-error", origin.getDepthError().toString());
      }
      if (origin.getDepthMethod() != null) {
        properties.put("depth-method", origin.getDepthMethod());
      }
      if (origin.getErrh() != null) {
        properties.put("horizontal-error", origin.getErrh().toString());
      }
      if (origin.getErrz() != null) {
        properties.put("vertical-error", origin.getErrz().toString());
      }
      if (origin.getLatError() != null) {
        properties.put("latitude-error", origin.getLatError().toString());
      }
      if (origin.getLonError() != null) {
        properties.put("longitude-error", origin.getLonError().toString());
      }
      if (origin.getMinDist() != null) {
        properties.put("minimum-distance", origin.getMinDist().toString());
      }
      if (origin.getNumPhaUsed() != null) {
        properties.put("num-phases-used", origin.getNumPhaUsed().toString());
      }
      if (origin.getNumStaUsed() != null) {
        properties.put("num-stations-used", origin.getNumStaUsed().toString());
      }
      if (origin.getRegion() != null) {
        properties.put("region", origin.getRegion());
      }
      if (origin.getStatus() != null) {
        properties.put("review-status", origin.getStatus().toString());
      }
      if (origin.getStdError() != null) {
        properties.put("standard-error", origin.getStdError().toString());
      }

      // origin method
      Iterator<Method> methods = origin.getMethod().iterator();
      if (methods.hasNext()) {
        Method method = methods.next();
        if (method.getClazz() != null) {
          properties.put("location-method-class", method.getClazz());
        }
        if (method.getAlgorithm() != null) {
          properties.put("location-method-algorithm", method.getAlgorithm());
        }
        if (method.getModel() != null) {
          properties.put("location-method-model", method.getModel());
        }

        String cubeLocationMethod = getCubeCode(method.getComment());
        if (cubeLocationMethod != null) {
          properties.put("cube-location-method", cubeLocationMethod);
        }
      }

      if (origin.getPhase() != null && origin.getPhase().size() > 0) {
        originProduct.getId().setType("phase-data");
        products.add(originProduct);

        if (sendOriginWhenPhasesExist) {
          // create lightweight origin product
          Product lightweightOrigin = new Product(originProduct);
          lightweightOrigin.getId().setType("origin");
          lightweightOrigin.getContents().remove(EQMESSAGE_CONTENT_PATH);

          // seek and destroy phases
          Iterator<Origin> iter = origins.iterator();
          while (iter.hasNext()) {
            Origin next = iter.next();
            if (next.getPhase() != null) {
              next.getPhase().clear();
            }
          }

          // serialize xml without phase data
          ByteArrayOutputStream baos = new ByteArrayOutputStream();
          EQMessageParser.serialize(this.eqmessage, baos, validate);
          lightweightOrigin.getContents().put(EQMESSAGE_CONTENT_PATH, new ByteContent(baos.toByteArray()));

          products.add(0, lightweightOrigin);
        }
      } else {
        // insert origin at start of list
        products.add(0, originProduct);
      }
    }

    this.originDepth = null;
    this.originEventTime = null;
    this.originLatitude = null;
    this.originLongitude = null;
    this.magnitude = null;

    for (Comment originComment : origin.getComment()) {
      products.addAll(getCommentProducts(originComment));
    }

    return products;
  }

  /**
   * Build magnitude products.
   *
   * This implementation builds at most one magnitude product (the first).
   *
   * @param magnitudes a list of candidate magsnitude objects.
   * @return a list of built magnitude products, which may be empty.
   */
  protected synchronized List<Product> getMagnitudeProducts(final List<Magnitude> magnitudes) {
    List<Product> products = new LinkedList<Product>();
    if (magnitudes == null || magnitudes.size() == 0) {
      return products;
    }

    // build product based on the first magnitude
    Magnitude magnitude = magnitudes.get(0);
    // set "globalish" property before getProduct()
    this.magnitude = magnitude.getValue();

    Action magnitudeAction = magnitude.getAction();
    // build magnitude product
    Product product = getProduct("magnitude", magnitudeAction == null ? null : magnitudeAction.toString());

    Map<String, String> properties = product.getProperties();
    if (magnitude.getSourceKey() != null) {
      properties.put("magnitude-source", magnitude.getSourceKey());
    }
    if (magnitude.getTypeKey() != null) {
      properties.put("magnitude-type", magnitude.getTypeKey());
    }
    if (magnitude.getAzimGap() != null) {
      properties.put("magnitude-azimuthal-gap", magnitude.getAzimGap().toString());
    }
    if (magnitude.getError() != null) {
      properties.put("magnitude-error", magnitude.getError().toString());
    }
    if (magnitude.getNumStations() != null) {
      properties.put("magnitude-num-stations-used", magnitude.getNumStations().toString());
    }

    String cubeMagnitudeType = getCubeCode(magnitude.getComment());
    if (cubeMagnitudeType != null) {
      properties.put("cube-magnitude-type", cubeMagnitudeType);
    }

    // don't clear property here, so origin can borrow magnitude property
    // this.magnitude = null;

    products.add(product);
    return products;
  }

  /**
   * Gets a list of Focal Mechanism products from momentTensors
   *
   * @param momentTensors List of Moment Tensors
   * @return a list of products
   */
  protected synchronized List<Product> getFocalMechanismProducts(final List<MomentTensor> momentTensors) {
    List<Product> products = new LinkedList<Product>();
    if (momentTensors == null || momentTensors.size() == 0) {
      return products;
    }

    // build moment tensors
    Iterator<MomentTensor> iter = momentTensors.iterator();
    while (iter.hasNext()) {
      MomentTensor mt = iter.next();

      Action mtAction = mt.getAction();
      // may be set to "moment-tensor" below
      Product product = getProduct("focal-mechanism", mtAction == null ? null : mtAction.toString());

      Map<String, String> properties = product.getProperties();

      // fill in product properties
      if (mt.getSourceKey() != null) {
        properties.put("beachball-source", mt.getSourceKey());
      }
      if (mt.getTypeKey() != null) {
        properties.put("beachball-type", mt.getTypeKey());
        // append source+type to code
        ProductId productId = product.getId();
        productId.setCode((productId.getCode() + "-" + mt.getSourceKey() + "-" + mt.getTypeKey()).toLowerCase());
      }
      if (mt.getMagMw() != null) {
        product.setMagnitude(mt.getMagMw());
      }
      if (mt.getM0() != null) {
        properties.put("scalar-moment", mt.getM0().toString());
      }

      if (mt.getTensor() != null) {
        // if the tensor is included, it is a "moment-tensor" instead of
        // a "focal-mechanism"
        product.getId().setType("moment-tensor");

        Tensor t = mt.getTensor();
        if (t.getMtt() != null) {
          properties.put("tensor-mtt", t.getMtt().toString());
        }
        if (t.getMpp() != null) {
          properties.put("tensor-mpp", t.getMpp().toString());
        }
        if (t.getMrr() != null) {
          properties.put("tensor-mrr", t.getMrr().toString());
        }
        if (t.getMtp() != null) {
          properties.put("tensor-mtp", t.getMtp().toString());
        }
        if (t.getMrp() != null) {
          properties.put("tensor-mrp", t.getMrp().toString());
        }
        if (t.getMrt() != null) {
          properties.put("tensor-mrt", t.getMrt().toString());
        }
      }

      if (mt.getNodalPlanes() != null) {
        NodalPlanes np = mt.getNodalPlanes();
        List<Fault> faults = np.getFault();
        if (faults.size() == 2) {
          Fault fault1 = faults.get(0);
          if (fault1.getDip() != null) {
            properties.put("nodal-plane-1-dip", fault1.getDip().toString());
          }
          if (fault1.getSlip() != null) {
            properties.put("nodal-plane-1-slip", fault1.getSlip().toString());
          }
          if (fault1.getStrike() != null) {
            properties.put("nodal-plane-1-strike", fault1.getStrike().toString());
          }
          Fault fault2 = faults.get(1);
          if (fault2.getDip() != null) {
            properties.put("nodal-plane-2-dip", fault2.getDip().toString());
          }
          if (fault2.getSlip() != null) {
            properties.put("nodal-plane-2-slip", fault2.getSlip().toString());
          }
          if (fault2.getStrike() != null) {
            properties.put("nodal-plane-2-strike", fault2.getStrike().toString());
          }
        }
      }

      if (mt.getDerivedOriginTime() != null) {
        properties.put("derived-eventtime", XmlUtils.formatDate(mt.getDerivedOriginTime()));
      }
      if (mt.getDerivedLatitude() != null) {
        properties.put("derived-latitude", mt.getDerivedLatitude().toString());
      }
      if (mt.getDerivedLongitude() != null) {
        properties.put("derived-longitude", mt.getDerivedLongitude().toString());
      }
      if (mt.getDerivedDepth() != null) {
        properties.put("derived-depth", mt.getDerivedDepth().toString());
      }

      if (mt.getPerDblCpl() != null) {
        properties.put("percent-double-couple", mt.getPerDblCpl().toString());
      }
      if (mt.getNumObs() != null) {
        properties.put("num-stations-used", mt.getNumObs().toString());
      }

      // attach original message as product content
      ByteContent xml = new ByteContent(eqmessageXML.getBytes());
      xml.setLastModified(eqmessageSent);
      xml.setContentType("application/xml");
      product.getContents().put(EQMESSAGE_CONTENT_PATH, xml);

      // add to list of built products
      products.add(product);
    }

    return products;
  }

  /**
   * Get product(s) from a ProductLink object.
   *
   * @param link the link object.
   * @return a list of found products.
   * @throws Exception if error occurs
   */
  protected synchronized List<Product> getProductLinkProducts(final ProductLink link) throws Exception {
    List<Product> products = new LinkedList<Product>();

    String linkType = getLinkAddonProductType(link.getCode());
    if (linkType == null) {
      LOGGER.finer("No product type found for productlink with code '" + link.getCode() + "', skipping");
      return products;
    }

    Action linkAction = link.getAction();
    Product linkProduct = getProduct(linkType, linkAction == null ? null : linkAction.toString());
    // remove the EQXML, only send product link attributes with link
    // products
    linkProduct.getContents().clear();

    // add addon code to product code
    ProductId id = linkProduct.getId();
    id.setCode(id.getCode() + "-" + link.getCode().toLowerCase());

    if (link.getVersion() != null) {
      linkProduct.setVersion(link.getVersion());
    }

    Map<String, String> properties = linkProduct.getProperties();
    if (link.getLink() != null) {
      properties.put("url", link.getLink());
    }
    if (link.getNote() != null) {
      properties.put("text", link.getNote());
    }
    if (link.getCode() != null) {
      properties.put("addon-code", link.getCode());
    }
    properties.put("addon-type", link.getTypeKey());

    products.add(linkProduct);
    return products;
  }

  /**
   * Get product(s) from a Comment object.
   *
   * @param comment the comment object.
   * @return a list of found products.
   * @throws Exception if error occurs
   */
  protected synchronized List<Product> getCommentProducts(final Comment comment) throws Exception {
    List<Product> products = new LinkedList<Product>();

    // CUBE_Codes are attributes of the containing product.
    String typeKey = comment.getTypeKey();
    if (typeKey != null && !typeKey.equals("CUBE_Code")) {
      String commentType = getTextAddonProductType(typeKey);
      if (commentType == null) {
        LOGGER.finer("No product type found for comment with type '" + comment.getTypeKey() + "'");
        return products;
      }

      Action commentAction = comment.getAction();
      Product commentProduct = getProduct(commentType, commentAction == null ? null : commentAction.toString());
      // remove the EQXML, only send comment text with comment products
      commentProduct.getContents().clear();

      // one product per comment type
      ProductId id = commentProduct.getId();
      id.setCode(id.getCode() + "_" + comment.getTypeKey().toLowerCase());

      Map<String, String> properties = commentProduct.getProperties();
      properties.put("addon-type", "comment");
      if (comment.getTypeKey() != null) {
        properties.put("code", comment.getTypeKey());
      }

      // store the comment text as content instead of a property, it may
      // contain newlines
      commentProduct.getContents().put("", new ByteContent(comment.getText().getBytes()));
      products.add(commentProduct);
    }

    return products;
  }

  /**
   * Build a product skeleton based on the current state.
   *
   * Product type is : [internal-](origin,magnitude,addon)[-(scenario|test)] where
   * the optional scope is not "Public", and the optional usage is not "Actual".
   *
   * @param type   short product type, like "origin", "magnitude".
   * @param action override the global message action.
   * @return a Product so that properties and content can be added.
   */
  protected synchronized Product getProduct(final String type, final String action) {

    String productType = type;
    // prepend type with non Public scopes (Internal)
    if (eventScope != EventScope.PUBLIC) {
      productType = eventScope.toString() + "-" + productType;
    }
    // append to type with non Actual usages
    if (eventUsage != EventUsage.ACTUAL) {
      productType = productType + "-" + eventUsage.toString();
    }
    // make it all lower case
    productType = productType.toLowerCase();

    // use event id
    String productCode = eventDataSource + eventEventId;
    productCode = productCode.toLowerCase();

    ProductId id = new ProductId(eqmessageSource.toLowerCase(), productType, productCode, eqmessageSent);
    Product product = new Product(id);

    // figure out whether this is a delete
    String productAction = action;
    if (productAction == null) {
      productAction = eventAction.toString();
      if (productAction == null) {
        productAction = "Update";
      }
    }
    String productStatus;
    if (productAction.equalsIgnoreCase("Delete")) {
      productStatus = Product.STATUS_DELETE;
    } else {
      productStatus = Product.STATUS_UPDATE;
    }
    product.setStatus(productStatus);

    if (eventDataSource != null && eventEventId != null) {
      product.setEventId(eventDataSource, eventEventId);
    }
    if (originEventTime != null) {
      product.setEventTime(originEventTime);
    }
    if (originLongitude != null) {
      product.setLongitude(originLongitude);
    }
    if (originLatitude != null) {
      product.setLatitude(originLatitude);
    }
    if (originDepth != null) {
      product.setDepth(originDepth);
    }
    if (magnitude != null) {
      product.setMagnitude(magnitude);
    }
    if (eventVersion != null) {
      product.setVersion(eventVersion);
    }

    /*
     * Map<String, String> properties = product.getProperties(); if (eventUsage !=
     * null) { properties.put("eqxml-usage", eventUsage.toString()); } if
     * (eventScope != null) { properties.put("eqxml-scope", eventScope.toString());
     * } if (eventAction != null) { properties.put("eqxml-action",
     * eventAction.toString()); }
     */

    ByteContent xml = new ByteContent(eqmessageXML.getBytes());
    xml.setLastModified(eqmessageSent);
    product.getContents().put(EQMESSAGE_CONTENT_PATH, xml);

    // add contents.xml to product to describe above content
    product.getContents().put(CONTENTS_XML_PATH, getContentsXML());

    return product;
  }

  /**
   * @return a buffer of XML content
   */
  protected Content getContentsXML() {
    StringBuffer buf = new StringBuffer();
    buf.append("<?xml version=\"1.0\"?>\n");
    buf.append("<contents xmlns=\"http://earthquake.usgs.gov/earthquakes/event/contents\">\n");
    buf.append("<file title=\"Earthquake XML (EQXML)\" id=\"eqxml\">\n");
    buf.append("<format type=\"xml\" href=\"").append(EQMESSAGE_CONTENT_PATH).append("\"/>\n");
    buf.append("</file>\n");
    buf.append("<page title=\"Location\" slug=\"location\">\n");
    buf.append("<file refid=\"eqxml\"/>\n");
    buf.append("</page>\n");
    buf.append("</contents>\n");

    ByteContent content = new ByteContent(buf.toString().getBytes());
    content.setLastModified(eqmessageSent);
    // this breaks things
    // content.setContentType("application/xml");
    return content;
  }

  /**
   * Extract a CUBE_Code from a Comment.
   *
   * This is the ISTI convention for preserving CUBE information in EQXML
   * messages. Checks a list of Comment objects for one with TypeKey="CUBE_Code"
   * and Text="CUBE_Code X", where X is the returned cube code.
   *
   * @param comments the list of comments.
   * @return the cube code, or null if not found.
   */
  protected synchronized String getCubeCode(final List<Comment> comments) {
    String cubeCode = null;

    if (comments != null) {
      Iterator<Comment> iter = comments.iterator();
      while (iter.hasNext()) {
        Comment comment = iter.next();
        if (comment.getTypeKey().equals("CUBE_Code")) {
          cubeCode = comment.getText().replace("CUBE_Code ", "");
          break;
        }
      }
    }

    return cubeCode;
  }

  /**
   * @return boolean sendOriginWhenPhasesExist
   */
  @Override
  public boolean isSendOriginWhenPhasesExist() {
    return sendOriginWhenPhasesExist;
  }

  /** @param sendOriginWhenPhasesExist boolean to set */
  @Override
  public void setSendOriginWhenPhasesExist(boolean sendOriginWhenPhasesExist) {
    this.sendOriginWhenPhasesExist = sendOriginWhenPhasesExist;
  }

  /** Unsupported functionality for an EQMessageProductCreator */
  @Override
  public void setSendMechanismWhenPhasesExist(boolean sendMechanismWhenPhasesExist) {
    LOGGER.warning("sendMechanismWhenPhasesExist is not supported for EQMessageProductCreator");
    return;
  }

  /** Unsupported functionality for an EQMessageProductCreator */
  @Override
  public boolean isSendMechanismWhenPhasesExist() {
    return false;
  }

  @Override
  public boolean isValidate() {
    return validate;
  }

  @Override
  public void setValidate(boolean validate) {
    this.validate = validate;
  }

  @Override
  public List<Product> getProducts(File file) throws Exception {
    EQMessage eqxml = null;
    String content = new String(FileUtils.readFile(file));
    String rawEqxml = null;

    // try to read eqxml
    try {
      eqxml = EQMessageParser.parse(StreamUtils.getInputStream(content.getBytes()), validate);
      rawEqxml = content;
    } catch (Exception e) {
      if (validate) {
        throw e;
      }

      // try to read cube
      try {
        Converter converter = new Converter();
        CubeMessage cube = converter.getCubeMessage(content);
        eqxml = converter.getEQMessage(cube);
      } catch (Exception e2) {
        if (content.startsWith(CubeEvent.TYPE) || content.startsWith(CubeDelete.TYPE)
            || content.startsWith(CubeAddon.TYPE)) {
          // throw cube parsing exception
          throw e2;
        } else {
          // log cube parsing exception
          LOGGER.log(Level.FINE, "Unable to parse cube message", e2);
        }

        // try to read eventaddon xml
        try {
          EventAddonParser parser = new EventAddonParser();
          parser.parse(content);
          eqxml = parser.getAddon().getEQMessage();
        } catch (Exception e3) {
          // log eventaddon parsing exception
          LOGGER.log(Level.FINE, "Unable to parse eventaddon", e3);
          // throw original exception
          throw e;
        }
      }
    }

    return this.getEQMessageProducts(eqxml, rawEqxml);
  }

  /** Type for general text */
  public static final String GENERAL_TEXT_TYPE = "general-text";
  /** Empty string array for general text addons */
  public static final String[] GENERAL_TEXT_ADDONS = new String[] {};

  /** Type for scitech text */
  public static final String SCITECH_TEXT_TYPE = "scitech-text";
  /** Empty string array for scitech text addons */
  public static final String[] SCITECH_TEXT_ADDONS = new String[] {};

  /** Type for impact text */
  public static final String IMPACT_TEXT_TYPE = "impact-text";
  /** String array for impact text addons */
  public static final String[] IMPACT_TEXT_ADDONS = new String[] { "feltreports" };

  /** Selected link type products have a mapping. */
  public static final String GENERAL_LINK_TYPE = "general-link";
  /** String array for general link addons */
  public static final String[] GENERAL_LINK_ADDONS = new String[] { "aftershock", "afterwarn", "asw", "generalmisc" };

  /** Type for scitech link */
  public static final String SCITECH_LINK_TYPE = "scitech-link";
  /** String array for scitech link */
  public static final String[] SCITECH_LINK_ADDONS = new String[] { "energy", "focalmech", "ncfm", "histmomenttensor",
      "finitefault", "momenttensor", "mtensor", "phase", "seiscrosssec", "seisrecsec", "traveltimes", "waveform",
      "seismograms", "scitechmisc" };

  /** Type for impact link */
  public static final String IMPACT_LINK_TYPE = "impact-link";
  /** String array for impact link */
  public static final String[] IMPACT_LINK_ADDONS = new String[] { "tsunamilinks", "impactmisc" };

  /**
   * Map from cube style link addon to product type.
   *
   * @param addonType String to find correct link type
   * @return null if link should not be converted to a product.
   */
  public String getLinkAddonProductType(final String addonType) {
    String c = addonType.toLowerCase();

    for (String general : GENERAL_LINK_ADDONS) {
      if (c.startsWith(general)) {
        return GENERAL_LINK_TYPE;
      }
    }

    for (String scitech : SCITECH_LINK_ADDONS) {
      if (c.startsWith(scitech)) {
        return SCITECH_LINK_TYPE;
      }
    }

    for (String impact : IMPACT_LINK_ADDONS) {
      if (c.startsWith(impact)) {
        return IMPACT_LINK_TYPE;
      }
    }

    return null;
  }

  /**
   * Map from cube style text addon to product type.
   *
   * @param addonType to find correct addon type
   * @return null if comment should not be converted to a product.
   */
  public String getTextAddonProductType(final String addonType) {
    String c = addonType.toLowerCase();

    for (String general : GENERAL_TEXT_ADDONS) {
      if (c.startsWith(general)) {
        return GENERAL_TEXT_TYPE;
      }
    }

    for (String impact : IMPACT_TEXT_ADDONS) {
      if (c.startsWith(impact)) {
        return IMPACT_TEXT_TYPE;
      }
    }

    for (String scitech : SCITECH_TEXT_ADDONS) {
      if (c.startsWith(scitech)) {
        return SCITECH_TEXT_TYPE;
      }
    }

    return null;
  }

}