All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.lshift.diffa.participant.scanning.DigestBuilder Maven / Gradle / Ivy

Go to download

Diffa Participant Support provides tools for implementing Diffa participants in Java (and other JVM languages)

There is a newer version: 1.5.10
Show newest version
/**
 * Copyright (C) 2010-2011 LShift Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.lshift.diffa.participant.scanning;

import net.jcip.annotations.NotThreadSafe;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;


/**
 * Utility for building digests based on a series of entities.
 *
 * This caches each id that is added to it, in order to ensure that id re-ordering is detected.
 * The downside of the current implementation is that the id cache is not thread safe.
 *
 * The 2-argument constructor is present to support non-codepoint ordered
 * collations--eg: most locale-specific orderings will group upper-and lower
 * case letters together, eg:
 * [a b A B].sortBy(unicodeOrdering) -> [A a B b]
 * [a b A B].sortBy(asciiOrdering) -> [A B a b]
 *
 */
@NotThreadSafe
public class DigestBuilder {
  private final static Logger log = LoggerFactory.getLogger(DigestBuilder.class);
  private final Map digestBuckets;
  private final List aggregations;
  // this should be a Comparator, but both ICU and the JDK's Collators
  // implement Comparator. Even  though they both compare strings.
  // So much for Type safety.
  private final Collation collation;
  private String previousId = null;


  public DigestBuilder(List aggregations) {

      this(aggregations, new AsciiCollation());
  }

  public DigestBuilder(List aggregations, Collation collation) {
    this.aggregations = aggregations;
    this.digestBuckets = new HashMap();

    if (collation == null) {
      throw new NullPointerException("Collator is null");
    }
    this.collation = collation;
  }

  /**
   * Adds a scan result entry. Note that this entry is expected to be for an entity - ie, it should have it's ID
   * property present.
   * @param entry the entry to add.
   */
  public void add(ScanResultEntry entry) {
    add(entry.getId(), entry.getAttributes(), entry.getVersion());
  }

  /**
   * Adds a new version into the builder.
   * @param id the id of the entity being added
   * @param attributes the attributes of the entity being added. The builder expects that an attribute will be present
   *  for each of the aggregations specified.
   * @param vsn the version of the entity
   */
  public void add(String id, Map attributes, String vsn) {
    log.trace("Adding to bucket: " + id + ", " + attributes + ", " + vsn);

    if (!isCorrectlyOrdered(id)) {
     throw new OutOfOrderException(previousId,id);
    }
    previousId = id;

    Map partitions = new HashMap();
    partitions.putAll(attributes);    // Default partitions to the initial attribute set
    for (ScanAggregation aggregation : aggregations) {
      String attrVal = attributes.get(aggregation.getAttributeName());
      if (attrVal == null) {
        throw new MissingAttributeException(id, aggregation.getAttributeName());
      }

      String bucket = aggregation.bucket(attrVal);
      partitions.put(aggregation.getAttributeName(), bucket);
    }

    BucketKey key = new BucketKey(partitions);
    Bucket bucket = digestBuckets.get(key);
    if (bucket == null) {
      bucket = new Bucket(key, partitions);
      digestBuckets.put(key, bucket);
    }

    bucket.add(vsn);
  }

    private boolean isCorrectlyOrdered(String id) {
      if (previousId != null) {
        return !collation.sortsBefore(id, previousId);
      } else {
        return true;
      }
    }

    public List toDigests() {
    List result = new ArrayList();
    for (Bucket bucket : digestBuckets.values()) {
      result.add(bucket.toDigest());
    }

    return result;
  }

  private static class BucketKey {
    private final Map attributes;

    public BucketKey(Map attributes) {
      this.attributes = attributes;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      BucketKey bucketKey = (BucketKey) o;

      if (attributes != null ? !attributes.equals(bucketKey.attributes) : bucketKey.attributes != null) return false;

      return true;
    }

    @Override
    public int hashCode() {
      return attributes != null ? attributes.hashCode() : 0;
    }
  }

  private static class Bucket {
    private final BucketKey key;
    private final Map attributes;
    private final String digestAlgorithm = "MD5";
    private final MessageDigest messageDigest;
    private String digest = null;

    public Bucket(BucketKey key, Map attributes) {
      this.key = key;
      this.attributes = attributes;

      try {
        this.messageDigest = MessageDigest.getInstance(digestAlgorithm);
      } catch (NoSuchAlgorithmException ex) {
        throw new RuntimeException("MD5 digest algorithm no available");
      }
    }

    /**
     * Adds a version to be included the digest computation
     * @param vsn  The version string to add.
     * @throws SealedBucketException When the digest for the current builder instance has already been computed.
     */
    public void add(String vsn) {
      if (digest != null) {
        throw new SealedBucketException(vsn, getLabel());
      }
      byte[] vsnBytes = vsn.getBytes(Charset.forName("UTF-8"));
      messageDigest.update(vsnBytes, 0, vsnBytes.length);
    }

    public ScanResultEntry toDigest() {
      if (digest == null) {
        digest = new String(Hex.encodeHex(messageDigest.digest()));
      }

      return ScanResultEntry.forAggregate(digest, attributes);
    }

    public String getLabel() {
      StringBuilder labelBuilder = new StringBuilder();
      String[] keys = attributes.keySet().toArray(new String[attributes.size()]);
      Arrays.sort(keys);

      for(String key : keys) {
        if (labelBuilder.length() > 0) labelBuilder.append("_");
        labelBuilder.append(attributes.get(key));
      }

      return labelBuilder.toString();
    }
  }
}