HashFileProductStorage.java

package gov.usgs.earthquake.distribution;

import gov.usgs.earthquake.product.ProductId;

import java.io.File;
import java.security.MessageDigest;
import java.util.logging.Logger;

/**
 * A FileProductStorage that builds directory paths based on a SHA-1 hash of the
 * product id.
 *
 * This helps overcome a limitation of the ext3 filesystem which limits the
 * number of subdirectories any one directory may contain to 32000. This
 * implementation should generate no more than 4096 (16 ^ 3) subdirectories of
 * any one subdirectory.
 *
 * Note: no collision handling has been implemented, although hash collisions
 * are not expected.
 *
 * Examples: <br>
 * Product ID: urn:usgs-product:us:shakemap:abcd1234:1304116272636 <br>
 * SHA-1 hash: dde7b3986ee2fda8a793b599b6ae725ab35df58b <br>
 * Directory: shakemap/dde/7b3/986/ee2/fda/8a7/93b/599/b6a/e72/5ab/35d/f58/b
 * <br>
 * <br>
 * Product ID: urn:usgs-product:us:shakemap2:efgh5678:1304116272711 <br>
 * SHA-1 hash: 8174d0f8d961d48c8a94a6bd0ab2a882e01173c6 <br>
 * Directory: shakemap2/817/4d0/f8d/961/d48/c8a/94a/6bd/0ab/2a8/82e/011/73c/6
 *
 * @deprecated
 * @see FileProductStorage
 */
@Deprecated
public class HashFileProductStorage extends FileProductStorage {

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

  // create this digest once, and clone it later
  private static final MessageDigest SHA_DIGEST;
  static {
    MessageDigest digest = null;
    try {
      digest = MessageDigest.getInstance("SHA");
    } catch (Exception e) {
      LOGGER.warning("Unable to create SHA Digest for HashFileProductStorage");
      digest = null;
    }
    SHA_DIGEST = digest;
  }

  /**
   * This is chosen because 16^3 = 4096 &lt; 32000, which is the ext3 subdirectory
   * limit.
   */
  public static final int DIRECTORY_NAME_LENGTH = 3;

  /**
   * Basic Constructor Sets baseDirectory to FileProductsStorage'
   * DEFAULT_DIRECTORY of 'Storage'
   */
  public HashFileProductStorage() {
    super();
  }

  /**
   * Constructor taking in specific File directory
   *
   * @param directory base directory for storage path
   */
  public HashFileProductStorage(final File directory) {
    super(directory);
  }

  /**
   * A method for subclasses to override the storage path.
   *
   * The returned path is appended to the base directory when storing and
   * retrieving products.
   *
   * @param id the product id to convert.
   * @return the directory used to store id.
   */
  @Override
  public String getProductPath(final ProductId id) {
    try {
      MessageDigest digest;
      synchronized (SHA_DIGEST) {
        digest = ((MessageDigest) SHA_DIGEST.clone());
      }

      String hexDigest = toHexString(digest.digest(id.toString().getBytes()));

      StringBuffer buf = new StringBuffer();
      // start with product type, to give idea of available products and
      // disk usage when looking at filesystem
      buf.append(id.getType());

      // sub directories based on hash
      int length = hexDigest.length();
      for (int i = 0; i < length; i += DIRECTORY_NAME_LENGTH) {
        String part;
        if (i + DIRECTORY_NAME_LENGTH < length) {
          part = hexDigest.substring(i, i + DIRECTORY_NAME_LENGTH);
        } else {
          part = hexDigest.substring(i);
        }
        buf.append(File.separator);
        buf.append(part);
      }

      return buf.toString();
    } catch (CloneNotSupportedException e) {
      // fall back to parent class
      return super.getProductPath(id);
    }
  }

  /**
   * Convert an array of bytes into a hex string. The string will always be twice
   * as long as the input byte array, because bytes < 0x10 are zero padded.
   *
   * @param bytes byte array to convert to hex.
   * @return hex string equivalent of input byte array.
   */
  private String toHexString(final byte[] bytes) {
    StringBuffer buf = new StringBuffer();
    int length = bytes.length;
    for (int i = 0; i < length; i++) {
      String hex = Integer.toHexString(0xFF & bytes[i]);
      if (hex.length() == 1) {
        buf.append('0');
      }
      buf.append(hex);
    }
    return buf.toString();
  }

}