ProductId.java

/*
 * ProductId
 */
package gov.usgs.earthquake.product;

import java.util.Date;
import java.util.Objects;

import javax.json.JsonObject;

import gov.usgs.util.XmlUtils;

/**
 * Attributes that uniquely identify a product.
 *
 * <dl>
 * <dt>Source</dt>
 * <dd>The organization <u>sending</u> the product; not necessarily the author
 * of the product.
 *
 * Typically a FDSN network code.</dd>
 *
 * <dt>Type</dt>
 * <dd>The type of product being sent.</dd>
 *
 * <dt>Code</dt>
 * <dd>A unique code assigned by the <code>source</code> and <code>type</code>.
 * Source and Type are effectively a namespace for codes.
 *
 * If the same <code>code</code> is re-used, it indicates a different version of
 * the same product.</dd>
 *
 * <dt>Update Time</dt>
 * <dd>A timestamp representing when a product was created.
 *
 * Update Time is also used as a <strong>version</strong>. Products from the
 * same <code>source</code> and <code>type</code> with the same
 * <code>code</code> are considered different versions of the same product.
 *
 * More recent (newer) <code>updateTime</code>s supersede less recent (older)
 * <code>updateTimes</code>.</dd>
 * </dl>
 */
public class ProductId implements Comparable<ProductId> {

  /** Product source. */
  private String source;

  /** Product type. */
  private String type;

  /** Product code. */
  private String code;

  /** Product update time. */
  private Date updateTime;

  /**
   * Create a new ProductId.
   *
   * Same as new ProductId(type, code, source, new Date()).
   *
   * @param source the product source.
   * @param type   the product type.
   * @param code   the product code.
   */
  public ProductId(final String source, final String type, final String code) {
    this(source, type, code, new Date());
  }

  /**
   * Create a new ProductId.
   *
   * @param source     the product source.
   * @param type       the product type.
   * @param code       the product code.
   * @param updateTime when the product was updated.
   */
  public ProductId(final String source, final String type, final String code, final Date updateTime) {
    setSource(source);
    setType(type);
    setCode(code);
    setUpdateTime(updateTime);
  }

  /**
   * Create a new ProductId from JsonObject
   *
   * @param jsonObject JsonObject that contains source/type/code/updateTime fields
   *                   of a productId.
   * @return The parsed out ProductId
   * @throws InvalidProductIdException when the source/type/code are not present
   */
  public static ProductId fromJson(final JsonObject jsonObject) throws InvalidProductIdException {
    try {
      return new ProductId(jsonObject.getString("source"),
          jsonObject.getString("type"),
          jsonObject.getString("code"),
          XmlUtils.getDate(jsonObject.getString("updateTime")));
    } catch (NullPointerException npe) {
      throw new InvalidProductIdException("Invalid ProductId");
    }
  }

  /**
   * @return the source
   */
  public String getSource() {
    return source;
  }

  /**
   * @param source the source to set
   */
  public void setSource(String source) {
    this.source = escapeIdPart(source);
  }

  /**
   * @return the type
   */
  public String getType() {
    return type;
  }

  /**
   * @param type the type to set
   */
  public void setType(String type) {
    this.type = escapeIdPart(type);
  }

  /**
   * @return the code
   */
  public String getCode() {
    return code;
  }

  /**
   * @param code the code to set
   */
  public void setCode(String code) {
    this.code = escapeIdPart(code);
  }

  /**
   * @return the updateTime
   */
  public Date getUpdateTime() {
    return updateTime;
  }

  /**
   * @param updateTime the updateTime to set
   */
  public void setUpdateTime(Date updateTime) {
    this.updateTime = updateTime;
  }

  /**
   * Convert this product id to a string. This string does not include the update
   * time.
   *
   * @return a product id string.
   */
  public String toString() {
    return "urn:usgs-product:" + source + ":" + type + ":" + code + ":" + Long.toString(updateTime.getTime());
  }

  /**
   * Parse a product id string.
   *
   * @param str a valid product id string.
   * @return a ProductId object.
   */
  public static ProductId parse(final String str) {
    String[] parts = str.split(":");
    try {
      if (!"urn".equals(parts[0]) || !"usgs-product".equals(parts[1])) {
        throw new Exception("Expected product urn");
      }
      String source = parts[2];
      String type = parts[3];
      String code = parts[4];
      String updateTime = parts[5];
      return new ProductId(source, type, code, new Date(Long.valueOf(updateTime)));
    } catch (Exception e) {
      throw new IllegalArgumentException("Invalid ProductId '" + str + "'");
    }
  }

  /**
   * Override the default Object.equals().
   */
  @Override
  public boolean equals(final Object obj) {
    return obj != null && obj instanceof ProductId && this.compareTo((ProductId) obj) == 0;
  }

  /**
   * Implement the Comparable interface.
   *
   * @param that product id being compared.
   * @return -1 if this precedes that, 0 if same, and 1 if that precedes this.
   */
  @Override
  public int compareTo(ProductId that) {
    int compare = getUpdateTime().compareTo(that.getUpdateTime());

    // same update time?
    if (compare == 0) {
      // to string includes source, type, code
      compare = toString().compareTo(that.toString());
    }

    return compare;
  }

  /**
   * Override default Object.hashCode().
   */
  @Override
  public int hashCode() {
    return Objects.hash(getSource(), getType(), getCode(), getUpdateTime());
  }

  /**
   * Whether these are the same product, even if they are different versions.
   *
   * It is possible for isSameProduct to return true if equals returns false, but
   * if equals returns true isSameProduct will also return true.
   *
   * @param that a ProductId to test.
   * @return true if these are the same product (source,type,code), false
   *         otherwise.
   */
  public boolean isSameProduct(ProductId that) {
    if (getSource().equals(that.getSource()) && getType().equals(that.getType()) && getCode().equals(that.getCode())) {
      return true;
    }
    return false;
  }

  /**
   * Escape id parts so they do not interfere with formatting/parsing.
   *
   * @param part part to escape.
   * @return escaped part.
   */
  private String escapeIdPart(final String part) {
    if (part == null) {
      return null;
    }

    String escaped = part;
    if (escaped.indexOf(":") != -1) {
      escaped = escaped.replace(":", "_");
    }
    return escaped;
  }
}