QuakemlProductCreator.java

package gov.usgs.earthquake.eids;

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.earthquake.product.io.ObjectProductSource;
import gov.usgs.earthquake.product.io.XmlProductHandler;
import gov.usgs.earthquake.quakeml.FileToQuakemlConverter;
import gov.usgs.util.StreamUtils;
import gov.usgs.util.XmlUtils;

import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.quakeml_1_2.Axis;
import org.quakeml_1_2.ConfidenceEllipsoid;
import org.quakeml_1_2.CreationInfo;
import org.quakeml_1_2.EvaluationMode;
import org.quakeml_1_2.Event;
import org.quakeml_1_2.EventDescription;
import org.quakeml_1_2.EventDescriptionType;
import org.quakeml_1_2.EventParameters;
import org.quakeml_1_2.EventType;
import org.quakeml_1_2.FocalMechanism;
import org.quakeml_1_2.InternalEvent;
import org.quakeml_1_2.Magnitude;
import org.quakeml_1_2.MomentTensor;
import org.quakeml_1_2.NodalPlane;
import org.quakeml_1_2.NodalPlanes;
import org.quakeml_1_2.Origin;
import org.quakeml_1_2.OriginQuality;
import org.quakeml_1_2.OriginUncertainty;
import org.quakeml_1_2.OriginUncertaintyDescription;
import org.quakeml_1_2.Quakeml;
import org.quakeml_1_2.RealQuantity;
import org.quakeml_1_2.ScenarioEvent;
import org.quakeml_1_2.SourceTimeFunction;
import org.quakeml_1_2.SourceTimeFunctionType;
import org.quakeml_1_2.Tensor;
import org.quakeml_1_2.TimeQuantity;
import org.quakeml_1_2.PrincipalAxes;
import org.quakeml_1_2.EvaluationStatus;

/**
 * Create Products from ANSS Quakeml files.
 */
public class QuakemlProductCreator implements ProductCreator {

	/** For use in logging issues */
	public static final Logger LOGGER = Logger
			.getLogger(QuakemlProductCreator.class.getName());

	/** Content type for xml */
	public static final String XML_CONTENT_TYPE = "application/xml";
	/** Content path for quakeml */
	public static final String QUAKEML_CONTENT_PATH = "quakeml.xml";
	/** Contents XML path */
	public static final String CONTENTS_XML_PATH = "contents.xml";

	/** Var for number of meters/kilometer... */
	public static final BigDecimal METERS_PER_KILOMETER = new BigDecimal("1000");

	/** Version */
	public static final String VERSION = "1.0";

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

	// xml for the eqmessage currently being processed
	private String quakemlXML;
	private CreationInfo eventParametersCreationInfo;

	// attributes of current quakeml being processed
	private String productSource;
	private String productCode;
	private String eventSource;
	private String eventCode;
	private Date updateTime;

	private FileToQuakemlConverter converter = null;
	private Converter formatConverter = new Converter();
	private boolean validate = false;

	// default to fixing padding issues
	private boolean padForBase64Bug = true;

	/** Default Constructor */
	public QuakemlProductCreator() {
		super();
	}

	/**
	 * Constructor taking in argument for Base64 bug padding
	 * @param padForBase64Bug Boolean if needed to pad
	 */
	public QuakemlProductCreator(boolean padForBase64Bug) {
		this.padForBase64Bug = padForBase64Bug;
	}

	/**
	 * Gets Quakeml products with no rawQuakeml
	 * @param message Parsed quakeml message
	 * @return List of products
	 * @throws Exception if error occurs
	 */
	public List<Product> getQuakemlProducts(final Quakeml message)
			throws Exception {
		return getQuakemlProducts(message, null);
	}

	/**
	 * Gets Quakeml products with the message as a rawQuakeml
	 * @param message Parsed quakeml message
	 * @return List of products
	 * @throws Exception if error occurs
	 */
	public List<Product> getQuakemlProducts(final String message)
			throws Exception {
		Quakeml quakeml = formatConverter.getQuakeml(message, validate);
		return getQuakemlProducts(quakeml, message);
	}

	/**
	 * Get products in a quakeml message.
	 *
	 * @param message
	 *            the parsed quakeml message.
	 * @param rawQuakeml
	 *            bytes of quakeml message. If null, the quakeml object will be
	 *            serialized into xml. This parameter is used to preserve the
	 *            original input, instead of always serializing from the quakeml
	 *            object.
	 * @return list of products generated from quakeml message.
	 * @throws Exception if error occurs
	 */
	public List<Product> getQuakemlProducts(final Quakeml message,
			final String rawQuakeml) throws Exception {
		List<Product> products = new ArrayList<Product>();

		// serialize for embedding in product
		quakemlXML = rawQuakeml;
		if (quakemlXML == null) {
			quakemlXML = convertQuakemlToString(message);
		} else {
			quakemlXML = fixRawQuakeml(quakemlXML);
		}

		EventParameters eventParameters = message.getEventParameters();
		eventParametersCreationInfo = eventParameters.getCreationInfo();

		// only process first event
		Event firstEvent = QuakemlUtils.getFirstEvent(eventParameters);
		if (firstEvent != null) {
			if (firstEvent instanceof InternalEvent) {
				products.addAll(getInternalEventProducts(message, (InternalEvent) firstEvent));
			} else if (firstEvent instanceof ScenarioEvent) {
				products.addAll(getScenarioEventProducts(message, (ScenarioEvent) firstEvent));
			} else {
				products.addAll(getEventProducts(message, firstEvent));
			}
		}

		// add property to all products to indicate they all come from the same
		// eventParameters "envelope"
		String eventParametersPublicID = eventParameters.getPublicID();
		Iterator<Product> productIter = products.iterator();
		while (productIter.hasNext()) {
			Product next = productIter.next();
			setProperty(next.getProperties(), "eventParametersPublicID",
					eventParametersPublicID);
		}

		eventParametersCreationInfo = null;
		quakemlXML = null;

		return products;
	}

	/**
	 * Get internal products in quakeml event element.
	 *
	 * Calls {@link #getEventProducts(Quakeml, Event)}, and adds "internal-"
	 * prefix to each type in the returned list of products.
	 *
	 * @param message
	 *            the quakeml message.
	 * @param event
	 *            the internal event element.
	 * @return list of internal products found in event element, may be empty.
	 * @throws Exception if error occurs
	 */
	public List<Product> getInternalEventProducts(final Quakeml message,
			final InternalEvent event) throws Exception {
		List<Product> products = getEventProducts(message, event);
		Iterator<Product> iter = products.iterator();
		while (iter.hasNext()) {
			ProductId nextId = iter.next().getId();
			nextId.setType("internal-" + nextId.getType());
		}
		return products;
	}

	/**
	 * Get scenario products in quakeml event element.
	 *
	 * Calls {@link #getEventProducts(Quakeml, Event)}, and adds "-scenario"
	 * suffix to each type in the returned list of products.
	 *
	 * @param message
	 *            the quakeml message.
	 * @param event
	 *            the scenario event element.
	 * @return list of scenario products found in event element, may be empty.
	 * @throws Exception if error occurs
	 */
	public List<Product> getScenarioEventProducts(final Quakeml message,
			final ScenarioEvent event) throws Exception {
		List<Product> products = getEventProducts(message, event);
		Iterator<Product> iter = products.iterator();
		while (iter.hasNext()) {
			ProductId nextId = iter.next().getId();
			nextId.setType(nextId.getType() + "-scenario");
		}
		return products;
	}

	/**
	 * Get products in quakeml event element.
	 *
	 * @param message
	 *            the quakeml message.
	 * @param event
	 *            the event element in the quakeml message.
	 * @return list of products found in event element, may be empty.
	 * @throws Exception if error occurs
	 */
	public List<Product> getEventProducts(final Quakeml message, Event event)
			throws Exception {
		List<Product> products = new ArrayList<Product>();

		// read catalog attributes for product source and code, and event source
		// and code
		productSource = event.getDatasource();
		productCode = event.getDataid();
		eventSource = event.getEventsource();
		eventCode = event.getEventid();

		if (productSource == null || eventSource == null || eventCode == null) {
			LOGGER.warning("Missing catalog attributes from event element, skipping");
			// not anss information, don't convert to products
			return products;
		} else if (productCode == null) {
			productCode = eventSource + eventCode;
		}
		productSource = productSource.toLowerCase();

		// product update time
		updateTime = null;
		if (eventParametersCreationInfo != null) {
			updateTime = eventParametersCreationInfo.getCreationTime();
		} else {
			LOGGER.warning("Missing eventParameters creationTime, using now for update time");
			updateTime = new Date();
		}

		ByteContent quakemlContent = new ByteContent(quakemlXML.getBytes());
		quakemlContent.setContentType(XML_CONTENT_TYPE);
		quakemlContent.setLastModified(updateTime);

		boolean hasPhaseData = QuakemlUtils.hasPhaseData(event);

		// create this object, may go unused
		Product originProduct = new Product(new ProductId(productSource,
				(hasPhaseData ? "phase-data" : "origin"), productCode,
				updateTime));
		originProduct.setEventId(eventSource, eventCode);
		if (event.getCreationInfo() != null) {
			originProduct.setVersion(event.getCreationInfo().getVersion());
		}
		originProduct.getContents().put(QUAKEML_CONTENT_PATH, quakemlContent);
		originProduct.getContents().put(CONTENTS_XML_PATH, getContentsXML());

		// track which event this product is from
		setProperty(originProduct.getProperties(), "quakeml-publicid",
				event.getPublicID());

		// delete origin product
		if (event.getType() == EventType.NOT_EXISTING) {
			originProduct.setStatus(Product.STATUS_DELETE);
			products.add(originProduct);

			Product phaseDelete = new Product(originProduct);
			phaseDelete.getId().setType("phase-data");
			products.add(phaseDelete);

			// don't need to delete other stuff
			// when origins for event are deleted, event is deleted
			return products;
		}

		Origin origin = QuakemlUtils.getPreferredOrigin(event);
		if (origin != null) {
			// track which origin this product is from
			setProperty(originProduct.getProperties(), "quakeml-origin-publicid",
					origin.getPublicID());

			// update origin product
			Map<String, String> properties = originProduct.getProperties();

			TimeQuantity eventTime = origin.getTime();
			if (eventTime != null) {
				originProduct.setEventTime(eventTime.getValue());
				setProperty(properties, "eventtime-error",
						eventTime.getUncertainty());
			}
			RealQuantity eventLatitude = origin.getLatitude();
			if (eventLatitude != null) {
				originProduct.setLatitude(eventLatitude.getValue());
				setProperty(properties, "latitude-error",
						eventLatitude.getUncertainty());
			}
			RealQuantity eventLongitude = origin.getLongitude();
			if (eventLongitude != null) {
				originProduct.setLongitude(eventLongitude.getValue());
				setProperty(properties, "longitude-error",
						eventLongitude.getUncertainty());
			}
			RealQuantity depth = origin.getDepth();
			if (depth != null) {
				originProduct.setDepth(depth.getValue().divide(
						METERS_PER_KILOMETER));
				if (depth.getUncertainty() != null) {
					setProperty(properties, "vertical-error", depth
							.getUncertainty().divide(METERS_PER_KILOMETER));
				}
				if (origin.getDepthType() != null) {
					setProperty(properties, "depth-type", origin.getDepthType()
							.value());
				}
			}

			// read horizontal error
			OriginUncertainty originUncertainty = origin.getOriginUncertainty();
			if (originUncertainty != null) {
				if (originUncertainty.getHorizontalUncertainty() != null) {
					setProperty(properties, "horizontal-error",
							originUncertainty.getHorizontalUncertainty()
									.divide(METERS_PER_KILOMETER));
				} else if (originUncertainty.getPreferredDescription() == OriginUncertaintyDescription.HORIZONTAL_UNCERTAINTY) {
					throw new IllegalArgumentException(
							"Missing horizontal uncertainty value");
				}

				ConfidenceEllipsoid ellipse = originUncertainty.getConfidenceEllipsoid();
				if (ellipse != null) {
					setProperty(properties, "error-ellipse-azimuth",
							ellipse.getMajorAxisAzimuth());
					setProperty(properties, "error-ellipse-plunge",
							ellipse.getMajorAxisPlunge());
					setProperty(properties, "error-ellipse-rotation",
							ellipse.getMajorAxisRotation());
					setProperty(properties, "error-ellipse-major",
							ellipse.getSemiMajorAxisLength());
					setProperty(properties, "error-ellipse-minor",
							ellipse.getSemiMinorAxisLength());
					setProperty(properties, "error-ellipse-intermediate",
							ellipse.getSemiIntermediateAxisLength());
				}
			}

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

			CreationInfo originCreationInfo = origin.getCreationInfo();
			if (originCreationInfo != null) {
				setProperty(properties, "origin-source",
						originCreationInfo.getAgencyID());
			}

			OriginQuality originQuality = origin.getQuality();
			if (originQuality != null) {
				setProperty(properties, "azimuthal-gap",
						originQuality.getAzimuthalGap());
				setProperty(properties, "num-phases-used",
						originQuality.getUsedPhaseCount());
				setProperty(properties, "num-stations-used",
						originQuality.getUsedStationCount());
				setProperty(properties, "minimum-distance",
						originQuality.getMinimumDistance());
				setProperty(properties, "standard-error",
						originQuality.getStandardError());
			}

			if (origin.getEvaluationMode() == EvaluationMode.MANUAL) {
				properties.put("review-status", "reviewed");
			} else {
				properties.put("review-status", "automatic");
			}

			if (origin.getEvaluationStatus() != null) {
				properties.put("evaluation-status", origin
						.getEvaluationStatus().value());
			} else {
				properties.put("evaluation-status",
						EvaluationStatus.PRELIMINARY.value());
			}

			// add magnitude properties
			Magnitude magnitude = QuakemlUtils.getPreferredMagnitude(event);
			if (magnitude != null) {
				// track which origin this product is from
				setProperty(originProduct.getProperties(), "quakeml-magnitude-publicid",
						magnitude.getPublicID());

				CreationInfo magnitudeCreationInfo = magnitude
						.getCreationInfo();
				if (magnitudeCreationInfo != null) {
					setProperty(properties, "magnitude-source",
							magnitudeCreationInfo.getAgencyID());
				}

				originProduct.setMagnitude(QuakemlUtils.getValue(magnitude
						.getMag()));

				setProperty(properties, "magnitude-type",
						QuakemlUtils.getMagnitudeType(magnitude.getType()));
				setProperty(properties, "magnitude-azimuthal-gap",
						magnitude.getAzimuthalGap());
				try {
					setProperty(properties, "magnitude-error", magnitude
							.getMag().getUncertainty());
				} catch (Exception e) {
					// no magnitude uncertainty
				}
				setProperty(properties, "magnitude-num-stations-used",
						magnitude.getStationCount());
			} else {
				// a location without a magnitude
			}

			products.add(originProduct);
		} else {
			// no preferred origin found
			// signal origin product hasn't been created
			originProduct = null;
		}

		// look for event description products
		Iterator<EventDescription> iter = event.getDescriptions().iterator();
		while (iter.hasNext()) {
			EventDescription next = iter.next();
			EventDescriptionType type = next.getType();

			if (type == EventDescriptionType.EARTHQUAKE_NAME) {
				if (originProduct != null) {
					// Region name overrides should be sent
					// from the admin pages, or attached to an origin. That way
					// only a preferred origin, or a manually sent region,
					// can alter the displayed region name
					setProperty(originProduct.getProperties(), "title", next
							.getText().trim());
				}
			} else if (type == EventDescriptionType.TECTONIC_SUMMARY) {
				Product product = new Product(new ProductId(productSource,
						"tectonic-summary", productCode, updateTime));
				product.setEventId(eventSource, eventCode);
				ByteContent content = new ByteContent(next.getText().getBytes());
				content.setLastModified(updateTime);
				product.getContents().put("tectonic-summary.inc.html", content);

				products.add(product);
			} else if (type == EventDescriptionType.FELT_REPORT) {
				Product product = new Product(new ProductId(productSource,
						"impact-text", productCode, updateTime));
				product.setEventId(eventSource, eventCode);
				ByteContent content = new ByteContent(next.getText().getBytes());
				content.setLastModified(updateTime);
				product.getContents().put("", content);

				products.add(product);
			}
		}

		if (sendMechanismWhenPhasesExist || originProduct == null
				|| !hasPhaseData) {
			Iterator<FocalMechanism> focalMechanisms = event
					.getFocalMechanisms().iterator();
			while (focalMechanisms.hasNext()) {
				FocalMechanism mech = focalMechanisms.next();
				Product mechProduct = null;

				if (hasPhaseData && originProduct != null) {
					// when a phase data product was created, send lightweight
					// focal mechanism
					Quakeml lightweightQuakeml = QuakemlUtils
							.getLightweightFocalMechanism(message,
									mech.getPublicID());
					Event lightweightEvent = QuakemlUtils.getFirstEvent(
							lightweightQuakeml.getEventParameters());
					FocalMechanism lightweightMech = QuakemlUtils
							.getFocalMechanism(lightweightEvent,
									mech.getPublicID());

					mechProduct = getFocalMechanismProduct(lightweightQuakeml,
							lightweightEvent, lightweightMech,
							convertQuakemlToString(lightweightQuakeml));
				} else {
					// otherwise, fall back to original
					mechProduct = getFocalMechanismProduct(message, event,
							mech, quakemlXML);
				}

				if (mechProduct != null) {
					products.add(mechProduct);
				}
			}
		}

		// send lightweight origin product
		if (sendOriginWhenPhasesExist && hasPhaseData && originProduct != null) {
			// create lightweight origin product
			Product lightweightOrigin = new Product(originProduct);
			lightweightOrigin.getId().setType("origin");

			Quakeml lightweightQuakeml = QuakemlUtils
					.getLightweightOrigin(message);

			// serialize xml without phase data
			ByteContent lightweightContent = new ByteContent(convertQuakemlToString(lightweightQuakeml).getBytes());
			lightweightContent.setContentType(XML_CONTENT_TYPE);
			lightweightContent.setLastModified(updateTime);
			lightweightOrigin.getContents().put(QUAKEML_CONTENT_PATH,
					lightweightContent);

			// insert at front of list
			products.add(0, lightweightOrigin);
		}

		return products;
	}

	/**
	 * @param quakeml Quakeml
	 * @param event the event element in the quakeml message
	 * @param mech A focal mechanism
	 * @param quakemlContent String of content in Quakeml
	 * @return A product derived from a focal mechanism
	 */
	protected Product getFocalMechanismProduct(final Quakeml quakeml,
			final Event event, final FocalMechanism mech,
			final String quakemlContent) {
		MomentTensor momentTensor = mech.getMomentTensor();

		// determine product id
		String mechSource = mech.getDatasource();
		String mechCode = mech.getDataid();
		String mechType = mech.getDatatype();
		if (mechType == null) {
			// automatically determine mechanism type based on available data
			mechType = "focal-mechanism";
			if (momentTensor != null && momentTensor.getTensor() != null) {
				mechType = "moment-tensor";
				if (mechCode == null && momentTensor.getMethodID() != null) {
					mechCode = productCode + "_" + momentTensor.getMethodID();
				}
			}
		}
		if (mechSource == null) {
			mechSource = productSource;
		}
		if (mechCode == null) {
			mechCode = productCode;
		}
		Product product = new Product(new ProductId(mechSource, mechType,
				mechCode, updateTime));
		if (mech.getEvaluationStatus() == EvaluationStatus.REJECTED) {
			// this is a delete
			product.setStatus(Product.STATUS_DELETE);
		}

		// product is being contributed to containing event
		product.setEventId(eventSource, eventCode);

		ByteContent tensorContent = new ByteContent(quakemlContent.getBytes());
		tensorContent.setContentType(XML_CONTENT_TYPE);
		tensorContent.setLastModified(updateTime);
		product.getContents().put(QUAKEML_CONTENT_PATH, tensorContent);
		product.getContents().put(CONTENTS_XML_PATH, getContentsXML());

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

		// track which mechanism is used in this product, for later
		// extraction
		setProperty(properties, "quakeml-publicid", mech.getPublicID());
		// also track original event if set, in case this is a recontributed
		// product
		setProperty(properties, "original-eventsource", mech.getEventsource());
		setProperty(properties, "original-eventsourcecode", mech.getEventid());

		if (mech.getEvaluationMode() == EvaluationMode.MANUAL) {
			properties.put("review-status", "reviewed");
		} else {
			properties.put("review-status", "automatic");
		}

		if (mech.getEvaluationStatus() != null) {
			properties.put("evaluation-status", mech.getEvaluationStatus()
					.value());
		} else {
			properties.put("evaluation-status", EvaluationStatus.PRELIMINARY
					.value());
		}

		CreationInfo focalMechanismCreationInfo = mech.getCreationInfo();
		if (focalMechanismCreationInfo != null) {
			product.setVersion(focalMechanismCreationInfo.getVersion());
			setProperty(properties, "beachball-source",
					focalMechanismCreationInfo.getAgencyID());
		}

		NodalPlanes planes = mech.getNodalPlanes();
		if (planes != null) {
			NodalPlane plane1 = planes.getNodalPlane1();
			setProperty(properties, "nodal-plane-1-strike", plane1.getStrike());
			setProperty(properties, "nodal-plane-1-dip", plane1.getDip());
			setProperty(properties, "nodal-plane-1-rake", plane1.getRake());

			NodalPlane plane2 = planes.getNodalPlane2();
			setProperty(properties, "nodal-plane-2-strike", plane2.getStrike());
			setProperty(properties, "nodal-plane-2-dip", plane2.getDip());
			setProperty(properties, "nodal-plane-2-rake", plane2.getRake());
		}

		// add properties for principal axes
		PrincipalAxes axes = mech.getPrincipalAxes();
		if (axes != null) {
			Axis tAxis = axes.getTAxis();
			if (tAxis != null) {
				setProperty(properties, "t-axis-azimuth", tAxis.getAzimuth());
				setProperty(properties, "t-axis-plunge", tAxis.getPlunge());
				setProperty(properties, "t-axis-length", tAxis.getLength(), true);
			}
			Axis nAxis = axes.getNAxis();
			if (nAxis != null) {
				setProperty(properties, "n-axis-azimuth", nAxis.getAzimuth());
				setProperty(properties, "n-axis-plunge", nAxis.getPlunge());
				setProperty(properties, "n-axis-length", nAxis.getLength(), true);
			}
			Axis pAxis = axes.getPAxis();
			if (pAxis != null) {
				setProperty(properties, "p-axis-azimuth", pAxis.getAzimuth());
				setProperty(properties, "p-axis-plunge", pAxis.getPlunge());
				setProperty(properties, "p-axis-length", pAxis.getLength(), true);
			}
		}

		setProperty(properties, "num-stations-used",
				mech.getStationPolarityCount());

		// try to read triggering origin attributes for association
		Origin triggeringOrigin = QuakemlUtils.getOrigin(event,
				mech.getTriggeringOriginID());
		if (triggeringOrigin != null) {
			// set properties for association purposes.
			product.setLatitude(triggeringOrigin.getLatitude().getValue());
			product.setLongitude(triggeringOrigin.getLongitude().getValue());
			product.setEventTime(triggeringOrigin.getTime().getValue());
			// add triggering depth if present
			if (triggeringOrigin.getDepth() != null) {
				product.setDepth(triggeringOrigin.getDepth().getValue()
						.divide(METERS_PER_KILOMETER));
			}
		}

		Tensor tensor = null;
		if (momentTensor != null) {
			setProperty(properties, "percent-double-couple",
					momentTensor.getDoubleCouple());
			setProperty(properties, "scalar-moment",
					momentTensor.getScalarMoment(), true);
			setProperty(properties, "beachball-type",
					momentTensor.getMethodID());
			if (momentTensor.getInversionType() != null) {
				setProperty(properties, "inversion-type",
						momentTensor.getInversionType().value());
			}

			tensor = momentTensor.getTensor();
			if (tensor != null) {
				setProperty(properties, "tensor-mpp", tensor.getMpp(), true);
				setProperty(properties, "tensor-mrp", tensor.getMrp(), true);
				setProperty(properties, "tensor-mrr", tensor.getMrr(), true);
				setProperty(properties, "tensor-mrt", tensor.getMrt(), true);
				setProperty(properties, "tensor-mtp", tensor.getMtp(), true);
				setProperty(properties, "tensor-mtt", tensor.getMtt(), true);
			}

			SourceTimeFunction sourceTimeFunction = momentTensor
					.getSourceTimeFunction();
			if (sourceTimeFunction != null) {
				SourceTimeFunctionType sourceTimeFunctionType = sourceTimeFunction
						.getType();
				if (sourceTimeFunctionType != null) {
					setProperty(properties, "sourcetime-type",
							sourceTimeFunctionType.value());
				}
				setProperty(properties, "sourcetime-duration",
						sourceTimeFunction.getDuration());
				setProperty(properties, "sourcetime-risetime",
						sourceTimeFunction.getRiseTime());
				setProperty(properties, "sourcetime-decaytime",
						sourceTimeFunction.getDecayTime());
			}

			Origin derivedOrigin = QuakemlUtils.getOrigin(event,
					momentTensor.getDerivedOriginID());

			if (derivedOrigin != null) {
				setProperty(properties, "derived-latitude",
						derivedOrigin.getLatitude());
				setProperty(properties, "derived-longitude",
						derivedOrigin.getLongitude());
				RealQuantity depth = derivedOrigin.getDepth();
				if (depth != null) {
					setProperty(properties, "derived-depth", depth.getValue()
							.divide(METERS_PER_KILOMETER));
				}
				setProperty(properties, "derived-eventtime",
						derivedOrigin.getTime());
			}

			Magnitude derivedMagnitude = QuakemlUtils.getMagnitude(event,
					momentTensor.getMomentMagnitudeID());
			if (derivedMagnitude != null) {
				String derivedMagnitudeType = derivedMagnitude.getType();
				setProperty(properties, "derived-magnitude-type",
						derivedMagnitudeType);
				setProperty(properties, "derived-magnitude",
						derivedMagnitude.getMag());

				if (derivedMagnitudeType.equalsIgnoreCase("Mwd")) {
					product.getId().setType("broadband-depth");
				}
			}
		}

		if (!Product.STATUS_DELETE.equals(product.getStatus())) {
			// if not deleting, do some validation
			String type = product.getId().getType();
			if ("focal-mechanism".equals(type) && planes == null) {
				LOGGER.warning("Focal mechanism missing nodal planes");
				return null;
			} else if ("moment-tensor".equals(type) && tensor == null) {
				LOGGER.warning("Moment tensor missing tensor parameters");
				return null;
			}
		}

		return product;
	}

	/**
	 * setProperty for RealQuantity values. No exponentials
	 * @param properties to add
	 * @param name of property
	 * @param value of property
	 */
	public void setProperty(final Map<String, String> properties,
			final String name, final RealQuantity value) {
		setProperty(properties, name, value, false);
	}

	/**
	 * setProperty for RealQuantity values
	 * @param properties to add
	 * @param name of property
	 * @param value of property
	 * @param allowExponential if allowed
	 */
	public void setProperty(final Map<String, String> properties,
			final String name, final RealQuantity value,
			final boolean allowExponential) {
		if (value == null) {
			return;
		}

		setProperty(properties, name, value.getValue(), allowExponential);
	}

	/**
	 * setProperty for strings
	 * @param properties to add
	 * @param name of property
	 * @param value of property
	 */
	public void setProperty(final Map<String, String> properties,
			final String name, final String value) {
		if (value == null) {
			return;
		}

		properties.put(name, value);
	}

	/**
	 * setProperty for TimeQuantities
	 * @param properties to add
	 * @param name of property
	 * @param value of property
	 */
	public void setProperty(final Map<String, String> properties,
			final String name, final TimeQuantity value) {
		if (value == null) {
			return;
		}

		properties.put(name, XmlUtils.formatDate(value.getValue()));
	}

	/**
	 * setProperty taking in BigDecimals. No exponentials
	 * @param properties to add
	 * @param name of property
	 * @param value of property
	 */
	public void setProperty(final Map<String, String> properties,
			final String name, final BigDecimal value) {
		setProperty(properties, name, value, false);
	}

	/**
	 * setProperty taking in BigDecimals
	 * @param properties to add
	 * @param name of property
	 * @param value of property
	 * @param allowExponential boolean
	 */
	public void setProperty(final Map<String, String> properties,
			final String name, final BigDecimal value,
			final boolean allowExponential) {
		if (value == null) {
			return;
		}

		properties.put(name, allowExponential ? value.toString()
				: value.toPlainString());
	}

	/**
	 * setProperty taking in BigIntegers. No exponentials
	 * @param properties to add
	 * @param name of property
	 * @param value of property
	 */
	public void setProperty(final Map<String, String> properties,
			final String name, final BigInteger value) {
		if (value == null) {
			return;
		}

		properties.put(name, value.toString());
	}

	/**
	 * setProperty taking in Integers
	 * @param properties to add
	 * @param name of property
	 * @param value of property
	 */
	public void setProperty(final Map<String, String> properties,
			final String name, final Integer value) {
		if (value == null) {
			return;
		}

		properties.put(name, value.toString());
	}

	/** @param converter FileToQuakeml Converter to set */
	public void setConverter(FileToQuakemlConverter converter) {
		this.converter = converter;
	}

	/** @return FileToQuakeml converter */
	public FileToQuakemlConverter getConverter() {
		return converter;
	}

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

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

	/**
	 * Implement the ProductCreator interface.
	 */
	@Override
	public List<Product> getProducts(File file) throws Exception {
		if (this.converter == null) {
			// preserve quakeml input
			String contents = new String(StreamUtils.readStream(file));
			return this.getQuakemlProducts(contents);
		} else {
			Quakeml quakeml = this.converter.parseFile(file);
			return this.getQuakemlProducts(quakeml);
		}
	}

	/**
	 * @return XML contents
	 */
	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 (Quakeml)\">\n");
		buf.append("<format type=\"xml\" href=\"").append(QUAKEML_CONTENT_PATH)
				.append("\"/>\n");
		buf.append("</file>\n");
		buf.append("</contents>\n");

		ByteContent content = new ByteContent(buf.toString().getBytes());
		content.setContentType("application/xml");

		return content;
	}

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

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

	/** @param sendMechanismWhenPhasesExist boolean to set */
	public void setSendMechanismWhenPhasesExist(
			boolean sendMechanismWhenPhasesExist) {
		this.sendMechanismWhenPhasesExist = sendMechanismWhenPhasesExist;
	}

	/** @return sendMechanismWhenPhasesExist boolean */
	public boolean isSendMechanismWhenPhasesExist() {
		return sendMechanismWhenPhasesExist;
	}

	/** @param padForBase64Bug to set */
	public void setPadForBase64Bug(boolean padForBase64Bug) {
		this.padForBase64Bug = padForBase64Bug;
	}

	/** @return padForBase64Bug */
	public boolean isPadForBase64Bug() {
		return padForBase64Bug;
	}

	/**
	 * Utility function that converts quakeml to a string
	 *
	 * @param message
	 * 						The quakeml to be converted
	 *
	 * @return raw string
	 * @throws Exception if quakeml doesn't validate
	 */
	private String convertQuakemlToString(Quakeml message) throws Exception{
		return fixRawQuakeml(formatConverter.getString(message,validate));
	}

	/**
	 * Fixes base 64 bug: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8222187
	 *
	 * @param rawMessage
	 * 							the message to edit
	 * @return the fixed string
	 */
	public String fixRawQuakeml(String rawMessage) {
		if (padForBase64Bug && rawMessage.getBytes().length % 4096 == 1) {
			rawMessage += ' ';
		}
		return rawMessage;
	}

	/**
	 * Convert quakeml files to products.
	 *
	 * @param args
	 *            a list of files to convert from quakeml to products.
	 * @throws Exception if error occurs
	 */
	public static void main(final String[] args) throws Exception {
		QuakemlProductCreator creator = new QuakemlProductCreator();
		creator.setSendOriginWhenPhasesExist(true);
		creator.setSendMechanismWhenPhasesExist(true);

		if (args.length == 0) {
			System.err
					.println("Quakeml to product converter utility.  "
							+ "For visually inspecting products that would be created from quakeml files.");
			System.err.println();
			System.err.println("Usage: QuakemlProductCreator FILE [ FILE ]");
			System.err
					.println("\tFILE - a quakeml file to convert to products, repeat as needed");
			System.exit(1);
		}

		for (String arg : args) {
			File quakeml = new File(arg);
			System.err.println("reading quakeml from "
					+ quakeml.getCanonicalPath());

			Iterator<Product> iter = creator.getProducts(quakeml).iterator();
			while (iter.hasNext()) {
				Product next = iter.next();
				new ObjectProductSource(next).streamTo(new XmlProductHandler(
						new StreamUtils.UnclosableOutputStream(System.err)));
			}

			System.err.println();
		}
	}

}