Product.java
/*
* Product
*/
package gov.usgs.earthquake.product;
import gov.usgs.util.CryptoUtils;
import gov.usgs.util.XmlUtils;
import gov.usgs.util.CryptoUtils.Version;
import java.security.PublicKey;
import java.security.PrivateKey;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* One or more pieces of Content with metadata.
*
* <dl>
* <dt><strong>ID</strong></dt>
* <dd>
* Products each have a unique {@link ProductId}.
* </dd>
*
* <dt><strong>Versioning</strong></dt>
* <dd>
* It is possible to create multiple versions of the same product,
* by reusing the same <code>source</code>, <code>type</code>, and
* <code>code</code>, with a different <code>updateTime</code>.
* <br>
* More recent (newer) <code>updateTime</code>s <strong>supersede</strong>
* Less recent (older) <code>updateTime</code>s.
* </dd>
*
* <dt><strong>Status</strong></dt>
* <dd>
* To <strong>delete</strong> a product, create a new version (updateTime)
* and set it's status to {@link STATUS_DELETE}. All other statuses
* ({@link STATUS_UPDATE} by default) are considered updates, and any
* value can be used in product-specific ways.
* </dd>
*
* <dt><strong>Properties</strong></dt>
* <dd>
* Products have key/value attributes that are Strings.
* These can be useful to convey summary information about a product,
* so consumers can quickly decide whether to process before opening
* any product contents.
* </dd>
*
* <dt><strong>Links</strong></dt>
* <dd>
* Similar to properties, links allow a Product to specify a
* <code>relation</code> and one or more <code>link</code> for each
* relation type.
* Links must be {@link java.net.URI}s, and may be {@link ProductId}s.
* </dd>
*
* <dt><strong>Contents</strong></dt>
* <dd>
* Many Products start as a directory of files, and metadata is determined later.
* It's also possible to create products without any Contents attached,
* if all the necessary information can be encoded using Properties or Links.
* <br>
* One special "empty path" content, literally at the empty-string path,
* is handled differently; since an empty path cannot be written to a file.
* PDL typically reads this in from standard input, or delivers this on
* standard input to external processes.
* </dd>
*
* <dt><strong>Signature</strong></dt>
* <dd>
* A product can have a digital signature, based on a digest of all
* product contents and metadata. These are required for most purposes.
* {@link CryptoUtils} provides utilities for working with OpenSSH keypairs.
* </dd>
*
* <dt><strong>Tracker URL (Deprecated)</strong></dt>
* <dd>
* Tracker URLs were initially used to track processing status as
* distribution progressed. These are no longer supported, and often
* introduced new problems.
* </dd>
* </dl>
*/
public class Product {
private static final Logger LOGGER = Logger.getLogger(Product.class
.getName());
/** The status message when a product is being updated. */
public static final String STATUS_UPDATE = "UPDATE";
/** The status message when a product is being deleted. */
public static final String STATUS_DELETE = "DELETE";
/** Property for eventsource */
public static final String EVENTSOURCE_PROPERTY = "eventsource";
/** Property for eventsourcecode */
public static final String EVENTSOURCECODE_PROPERTY = "eventsourcecode";
/** Property for eventtime */
public static final String EVENTTIME_PROPERTY = "eventtime";
/** Property for magnitude */
public static final String MAGNITUDE_PROPERTY = "magnitude";
/** Property for latitude */
public static final String LATITUDE_PROPERTY = "latitude";
/** Property for longitude */
public static final String LONGITUDE_PROPERTY = "longitude";
/** Property for depth */
public static final String DEPTH_PROPERTY = "depth";
/** Property for version */
public static final String VERSION_PROPERTY = "version";
/** A unique identifier for this product. */
private ProductId id;
/** A terse status message. */
private String status;
/** Properties of this product. */
private Map<String, String> properties = new HashMap<String, String>();
/** Links to other products and related resources. */
private Map<String, List<URI>> links = new HashMap<String, List<URI>>();
/** Product contents. Mapping from path to content. */
private Map<String, Content> contents = new HashMap<String, Content>();
/** A URL where status updates are sent. */
private URL trackerURL = null;
/** A signature generated by the product creator. */
private String signature = null;
/** Signature version. */
private Version signatureVersion = Version.SIGNATURE_V1;
/**
* Construct a new Product with status "UPDATE".
*
* @param id
* the product's unique Id.
*/
public Product(final ProductId id) {
this(id, STATUS_UPDATE);
}
/**
* Construct a new Product.
*
* @param id
* the product's unique Id.
* @param status
* the product's status.
*/
public Product(final ProductId id, final String status) {
setId(id);
setStatus(status);
}
/**
* Copy constructor.
*
* @param that
* the product to copy.
*/
public Product(final Product that) {
this(new ProductId(that.getId().getSource(), that.getId().getType(),
that.getId().getCode(), that.getId().getUpdateTime()), that
.getStatus());
this.setTrackerURL(that.getTrackerURL());
this.setProperties(that.getProperties());
this.setLinks(that.getLinks());
this.setContents(that.getContents());
this.setSignature(that.getSignature());
}
/**
* @return the id
*/
public ProductId getId() {
return id;
}
/**
* @param id
* the id to set
*/
public void setId(final ProductId id) {
this.id = id;
}
/**
* @return the status
*/
public String getStatus() {
return status;
}
/**
* @param status
* the status to set
*/
public void setStatus(final String status) {
this.status = status;
}
/**
* Product.STATUS_DELETE.equalsIgnoreCase(status).
*
* @return whether this product is deleted
*/
public boolean isDeleted() {
if (STATUS_DELETE.equalsIgnoreCase(this.status)) {
return true;
} else {
return false;
}
}
/**
* @return the properties
*/
public Map<String, String> getProperties() {
return properties;
}
/**
* @param properties
* the properties to set
*/
public void setProperties(final Map<String, String> properties) {
this.properties.putAll(properties);
}
/**
* Returns a reference to the links map.
*
* @return the links
*/
public Map<String, List<URI>> getLinks() {
return links;
}
/**
* Copies entries from provided map.
*
* @param links
* the links to set
*/
public void setLinks(final Map<String, List<URI>> links) {
this.links.putAll(links);
}
/**
* Add a link to a product.
*
* @param relation
* how link is related to product.
* @param href
* actual link.
*/
public void addLink(final String relation, final URI href) {
List<URI> relationLinks = links.get(relation);
if (relationLinks == null) {
relationLinks = new LinkedList<URI>();
links.put(relation, relationLinks);
}
relationLinks.add(href);
}
/**
* Returns a reference to the contents map.
*
* @return the contents
*/
public Map<String, Content> getContents() {
return contents;
}
/**
* Copies entries from provided map.
*
* @param contents
* the contents to set
*/
public void setContents(final Map<String, Content> contents) {
this.contents.clear();
this.contents.putAll(contents);
}
/**
* @return the trackerURL
*/
public URL getTrackerURL() {
return trackerURL;
}
/**
* @param trackerURL
* the trackerURL to set
*/
public void setTrackerURL(final URL trackerURL) {
this.trackerURL = trackerURL;
}
/**
* @return the signature
*/
public String getSignature() {
return signature;
}
/**
* @return the signature
*/
public Version getSignatureVersion() {
return signatureVersion;
}
/**
* @param signature
* the signature to set
*/
public void setSignature(final String signature) {
this.signature = signature;
}
/**
* @param version
* the signature version to set
*/
public void setSignatureVersion(final Version version) {
this.signatureVersion = version;
}
/**
* Sign this product using a PrivateKey and signature v1.
* @param privateKey used to sign
* @throws Exception if error occurs
*/
public void sign(final PrivateKey privateKey) throws Exception {
this.sign(privateKey, Version.SIGNATURE_V1);
}
/**
* Sign this product using a PrivateKey.
*
* @param privateKey
* a DSAPrivateKey or RSAPrivateKey.
* @param version
* the signature version to use.
* @throws Exception if error occurs
*/
public void sign(final PrivateKey privateKey, final Version version) throws Exception {
setSignature(CryptoUtils.sign(
privateKey,
ProductDigest.digestProduct(this, version),
version));
setSignatureVersion(version);
}
/**
* Verify this product's signature using Signature V1.
* @param publicKeys Array of public keys to verify
* @throws Exception if error occurs
* @return true if valid, false otherwise.
*/
public boolean verifySignature(final PublicKey[] publicKeys)
throws Exception {
return verifySignature(publicKeys, getSignatureVersion());
}
/**
* Verify this product's signature.
*
* When a product has no signature, this method returns false. The array of
* public keys corresponds to one or more keys that may have generated the
* signature. If any of the keys verify, this method returns true.
*
* @param publicKeys
* an array of publicKeys to test.
* @param version
* the signature version to use.
* @return true if valid, false otherwise.
* @throws Exception if error occurs
*/
public boolean verifySignature(final PublicKey[] publicKeys, final Version version)
throws Exception {
return verifySignatureKey(publicKeys, version) != null;
}
/**
* Try to verify using multiple candidate keys.
* @param publicKeys an array of publicKeys to test
* @param version the signature version to use.
* @return true if valid, false otherwise.
* @throws Exception if error occurs
*/
public PublicKey verifySignatureKey(final PublicKey[] publicKeys, final Version version) throws Exception {
if (signature == null) {
return null;
}
byte[] digest = ProductDigest.digestProduct(this, version);
for (PublicKey key : publicKeys) {
try {
if (CryptoUtils.verify(key, digest, getSignature(), version)) {
return key;
}
} catch (Exception e) {
LOGGER.log(Level.FINEST, "Exception while verifying signature",
e);
}
}
return null;
}
/**
* Get the event id.
*
* The event id is the combination of event source and event source code.
*
* @return the event id, or null if either event source or event source code
* is null.
*/
public String getEventId() {
String eventSource = getEventSource();
String eventSourceCode = getEventSourceCode();
if (eventSource == null && eventSourceCode == null) {
return null;
}
return (eventSource + eventSourceCode).toLowerCase();
}
/**
* Set both the network and networkId at the same time.
*
* @param source
* the originating network.
* @param sourceCode
* the originating network's id.
*/
public void setEventId(final String source, final String sourceCode) {
setEventSource(source);
setEventSourceCode(sourceCode);
}
/**
* Get the event source property.
*
* @return the event source property, or null if no event source property
* set.
*/
public String getEventSource() {
return this.properties.get(EVENTSOURCE_PROPERTY);
}
/**
* Set the event source property.
*
* @param eventSource
* the event source to set.
*/
public void setEventSource(final String eventSource) {
if (eventSource == null) {
this.properties.remove(EVENTSOURCE_PROPERTY);
} else {
this.properties
.put(EVENTSOURCE_PROPERTY, eventSource.toLowerCase());
}
}
/**
* Get the event source code property.
*
* @return the event source code property, or null if no event source code
* property set.
*/
public String getEventSourceCode() {
return this.properties.get(EVENTSOURCECODE_PROPERTY);
}
/**
* Set the event id property.
*
* @param eventSourceCode
* the event id to set.
*/
public void setEventSourceCode(final String eventSourceCode) {
if (eventSourceCode == null) {
this.properties.remove(EVENTSOURCECODE_PROPERTY);
} else {
this.properties.put(EVENTSOURCECODE_PROPERTY,
eventSourceCode.toLowerCase());
}
}
/**
* Get the event time property as a date.
*
* @return the event time property as a date, or null if no date property
* set.
*/
public Date getEventTime() {
String strDate = this.properties.get(EVENTTIME_PROPERTY);
if (strDate == null) {
return null;
}
return XmlUtils.getDate(strDate);
}
/**
* Set the event time property as a date.
*
* @param eventTime
* the event time to set.
*/
public void setEventTime(final Date eventTime) {
if (eventTime == null) {
this.properties.remove(EVENTTIME_PROPERTY);
} else {
this.properties.put(EVENTTIME_PROPERTY,
XmlUtils.formatDate(eventTime));
}
}
/**
* Get the magnitude property as a big decimal.
*
* @return the magnitude property as a big decimal, or null if no magnitude
* property set.
*/
public BigDecimal getMagnitude() {
String strMag = this.properties.get(MAGNITUDE_PROPERTY);
if (strMag == null) {
return null;
}
return new BigDecimal(strMag);
}
/**
* Set the magnitude property as a big decimal.
*
* @param magnitude
* the magnitude to set.
*/
public void setMagnitude(final BigDecimal magnitude) {
if (magnitude == null) {
this.properties.remove(MAGNITUDE_PROPERTY);
} else {
this.properties.put(MAGNITUDE_PROPERTY, magnitude.toPlainString());
}
}
/**
* Get the latitude property as a big decimal.
*
* @return latitude property as a big decimal, or null if no latitude
* property set.
*/
public BigDecimal getLatitude() {
String strLat = this.properties.get(LATITUDE_PROPERTY);
if (strLat == null) {
return null;
}
return new BigDecimal(strLat);
}
/**
* Set the latitude property as a big decimal.
*
* @param latitude
* the latitude to set.
*/
public void setLatitude(final BigDecimal latitude) {
if (latitude == null) {
this.properties.remove(LATITUDE_PROPERTY);
} else {
this.properties.put(LATITUDE_PROPERTY, latitude.toPlainString());
}
}
/**
* Get the longitude property as a big decimal.
*
* @return longitude property as a big decimal, or null if no longitude
* property set.
*/
public BigDecimal getLongitude() {
String strLon = this.properties.get(LONGITUDE_PROPERTY);
if (strLon == null) {
return null;
}
return new BigDecimal(strLon);
}
/**
* Set the longitude property as a big decimal.
*
* @param longitude
* the longitude to set.
*/
public void setLongitude(final BigDecimal longitude) {
if (longitude == null) {
this.properties.remove(LONGITUDE_PROPERTY);
} else {
this.properties.put(LONGITUDE_PROPERTY, longitude.toPlainString());
}
}
/**
* Get the depth property as a big decimal.
*
* @return depth property as big decimal, or null if no depth property set.
*/
public BigDecimal getDepth() {
String strDepth = this.properties.get(DEPTH_PROPERTY);
if (strDepth == null) {
return null;
}
return new BigDecimal(strDepth);
}
/**
* Set the depth property as a big decimal.
*
* @param depth
* the depth to set.
*/
public void setDepth(final BigDecimal depth) {
if (depth == null) {
this.properties.remove(DEPTH_PROPERTY);
} else {
this.properties.put(DEPTH_PROPERTY, depth.toPlainString());
}
}
/**
* Get the version property.
*
* @return the version property, or null if no version property set.
*/
public String getVersion() {
return this.properties.get(VERSION_PROPERTY);
}
/**
* Set the version property.
*
* @param version
* the version to set.
*/
public void setVersion(final String version) {
if (version == null) {
this.properties.remove(VERSION_PROPERTY);
} else {
this.properties.put(VERSION_PROPERTY, version);
}
}
}