mmichaelis
11/22/2011 - 10:48 AM

BDD Design Pattern: Reference Tracker

BDD Design Pattern: Reference Tracker

import javax.annotation.ManagedBean;

/**
 * <p>
 * Tracks objects grouped by their classes.
 * </p>
 * <p><strong>Example:</strong></p>
 * <pre>
 * @Inject
 * private TypedReferenceTracker tracker;
 * ...
 * Content document = ...
 * tracker.track(Content.class, "A", document);
 * ...
 * Content content = tracker.get(Content.class, "A");
 * content.doSomething();
 * </pre>
 */
@SuppressWarnings("JavaDoc")
@ManagedBean
public class TypedReferenceTracker extends AbstractReferenceTracker<Class<?>, Object> {

  /**
   * Retrieve tracked object identified by its class and its reference name.
   * @param type the type of the object to retrieve
   * @param name the name of the reference
   * @param <T> type of the object
   * @return null if there is no such tracked object, otherwise the tracked object
   */
  public <T> T get(final Class<T> type, final String name) {
    return get(type, name, type);
  }
}
@Inject
private TypedReferenceTracker tracker;
...
Content document = ...
tracker.track(Content.class, "A", document);
...
Content content = tracker.get(Content.class, "A");
content.doSomething();
/**
 * <p>
 * Tracks references between BDD steps. Typically you need some global variables or something alike to remember
 * states between steps you perform. Extending classes will support you in doing so as you can
 * {@link #track(Object, String, Object) track} objects in one step and {@link #get(Object, String, Class) get}
 * them in the next step.
 * </p>
 * <p>
 * Mind that your project should decide one common tracker to use as otherwise you might have problems
 * asking the correct tracker for the object you want to retrieve.
 * </p>
 *
 * @param <K> the type of the key used for tracking
 * @param <V> type of the tracked values
 */
public interface ReferenceTracker<K, V> {
  /**
   * Tracks an object of a given type and name. If you track an object with the same parameters again it will
   * overwrite the existing tracked object without further notice.
   * @param type type of the object to track. Think of it like a namespace.
   * @param name the name to use for the reference
   * @param obj the object to track
   */
  void track(K type, String name, V obj);

  /**
   * Retrieve a tracked object and cast it to the given type.
   * @param type the type/namespace of the object to retrieve
   * @param name the reference name
   * @param targetType the class of the object to retrieve
   * @param <T> type of the tracked object
   * @return <code>null</code>, if there is no such tracked element, otherwise the tracked element
   */
  <T> T get(K type, String name, Class<T> targetType);

  /**
   * Cleanup tracked references. Should be called either on <code>tearDown</code> of JUnit tests or after each scenario
   * for example with the <code>@AfterScenario</code>-annotation.
   */
  void cleanup();
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * Tracks references between BDD steps. Typically you need some global variables or something alike to remember
 * states between steps you perform. Extending classes will support you in doing so as you can
 * {@link #track(Object, String, Object) track} objects in one step and {@link #get(Object, String, Class) get}
 * them in the next step.
 * </p>
 * <p>
 * Mind that your project should decide one common tracker to use as otherwise you might have problems
 * asking the correct tracker for the object you want to retrieve.
 * </p>
 *
 * @param <K> the type of the key used for tracking
 * @param <V> type of the tracked values
 */
public abstract class AbstractReferenceTracker<K, V> implements ReferenceTracker<K,V> {
  /**
   * My logger.
   */
  private static final Logger LOG = LoggerFactory.getLogger(AbstractReferenceTracker.class);
  /**
   * Initial size of the name-value map (the inner map).
   */
  private static final int INITIAL_NAME_VALUE_MAP_CAPACITY = 4;

  /**
   * Map which contains references where the outer map maps keys (most likely types) to name-value pairs in
   * the inner map.
   */
  private final Map<K, Map<String, V>> references = new HashMap<K, Map<String, V>>(16);

  /**
   * {@inheritDoc}
   */
  @Override
  public void track(final K type, final String name, final V obj) {
    final Map<String, V> refToObject;
    if (!references.containsKey(type)) {
      refToObject = new HashMap<String, V>(INITIAL_NAME_VALUE_MAP_CAPACITY);
      references.put(type, refToObject);
    } else {
      refToObject = references.get(type);
    }
    refToObject.put(name, obj);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public <T> T get(final K type, final String name, final Class<T> targetType) {
    if (references.containsKey(type)) {
      final V o = references.get(type).get(name);
      return targetType.cast(o);
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public final void cleanup() {
    if (LOG.isDebugEnabled()) {
      final Collection<Map<String, V>> values = references.values();
      int sum = 0;
      for (final Map<String, V> value : values) {
        sum += value.size();
      }
      LOG.debug("Clearing {} tracked references.", sum);
    }
    references.clear();
  }
}