/**
*
*/
package photoSpreadUtilities;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import photoSpreadObjects.PhotoSpreadObject;
/**
* @author paepcke
*
* Indexer over metadata. Each indexer indexes given PhotoSpreadObjects
* by a pre-defined set of metadata keys. Example:
*
* Let an indexer be initialized to the key set "Name" and "Height".
* When a PhotoSpreadObject is passed to the add() method, that method
* will retrieve the values of the metadata keys "Name" and "Height"
* from the given object. The method will then add the given object
* under both the value of "Name", and under the value of "Height".
* So, beginning with a metadata value we can later get all the
* objects that have a given value for "Name", or another given
* value for "Height".
*
* The get(<metadataKey>, <metadataValue>) method returns a HashSet
* with all the PhotoSpreadObjects that contain <metadataValue> as
* the value for <metadataKey>.
*
* Assume an indexer is set for indexing
* on metadata keys key_1 through key_n.
* Then the mapping from metadata values
* (mdv) to PhotoSpreadObject sets looks
* like this:
* key_1: {...}
*
* mdv_1 ...
*
* key_n: {...}
*
*
*
*
* key_1: {...}
*
* mdv_2 ...
*
* key_n: {...}
*
* The data structure for all this is like this:
* HashMap<metadataValues, HashMap<Key_x, HashSet<PhotoSpreadObject>>>
*
* We call the outer HashMap the 'ValueMap.' We call each
* of the inner HashMaps 'KeyMap's. We call each HashSet of
* PhotoSpreadObjects a 'Bucket'. Respective inner classes
* are defined for clarity in the code.
*/
public class MetadataIndexer {
private static final long serialVersionUID = 1L;
ArrayList<String> _keysToIndex = new ArrayList<String>();
ValueMap _valueMap = new ValueMap();
/****************************************************
* Constructor
*****************************************************/
public MetadataIndexer (ArrayList<String> metadataKeys) {
_keysToIndex = metadataKeys;
}
/****************************************************
* Inner Classes Bucket, ValueMap, and KeyMap
*****************************************************/
class ValueMap extends HashMap<String, KeyMap> {
private static final long serialVersionUID = 1L;
}
class KeyMap extends HashMap<String, Bucket> {
private static final long serialVersionUID = 1L;
}
class Bucket extends HashSet<PhotoSpreadObject> {
private static final long serialVersionUID = 1L;
}
/****************************************************
* Methods
*****************************************************/
public ArrayList<String> getIndexedMetadataKeys () {
return _keysToIndex;
}
/**
* Index all the given object by all of the
* metadata keys that this index indexes.
* @param objToIndex
*/
public void add (PhotoSpreadObject objToIndex) {
for (String keyToIndex : _keysToIndex) {
String metadataValue = objToIndex.getMetaData(keyToIndex);
add(objToIndex, keyToIndex, metadataValue);
}
}
public void add (
PhotoSpreadObject objToIndex,
String metadataKey,
String metadataValue) {
Bucket bucket = null;
KeyMap keyMap = null;
if (!_keysToIndex.contains(metadataKey))
return;
// Get the KeyMap for the metadata value we just retrieved:
keyMap = _valueMap.get(metadataValue);
// If we never saw this value before,
// we need to create a new map that maps
// metadata keys to buckets:
if (keyMap == null) {
keyMap = installNewKeymap(metadataValue);
}
// In the resulting keymap: put the object into the
// keyToIndex bucket:
bucket = keyMap.get(metadataKey);
bucket.add(objToIndex);
}
public void addAll (TreeSetRandomSubsetIterable<PhotoSpreadObject> objs) {
for (PhotoSpreadObject obj : objs)
add(obj);
}
/**
* @param metadataKey
* @param metadataValue
* @return All objects that contain the given metadataValue for
* the given metadataKey. Null if value not found.
*/
public Bucket get (String metadataKey, String metadataValue) {
// See whether the given metadata value
// leads to a KeyMap:
KeyMap keyMap = _valueMap.get(metadataValue);
if (keyMap == null)
return null;
// Got a KeyMap for the given value. Get the bucket
// for the given metadata key (for the value):
return keyMap.get(metadataKey);
}
/**
* Remove the given object's index entry for
* the given metadata value/key pair.
* @param obj
* @param metadataKey
* @param metadataValue
* @return Bucket with the object removed.
*/
public Bucket remove (
PhotoSpreadObject obj, String metadataKey, String metadataValue) {
// Need to act only if this indexer
// indexes the given key:
if (! _keysToIndex.contains(metadataKey))
return null;
Bucket bucket = get(metadataKey, metadataValue);
bucket.remove(obj);
return bucket;
}
/**
* Update a given object's entry in this index.
* This means removing the entry for the given
* metadata key's old value, and adding a new
* entry for the new key.
* @param obj
* @param metadataKey
* @param oldValue
* @param newValue
*/
public void updateIndex (
PhotoSpreadObject obj,
String metadataKey,
String oldValue,
String newValue) {
// If this index isnt' set to index this
// metadataKey, do nothing:
if (!_keysToIndex.contains(metadataKey))
return;
// If oldValue was ever set, remove that value's
// entry from the index:
if (!oldValue.equals(Const.NULL_VALUE_STRING)){
Bucket bucket = remove(obj, metadataKey, oldValue);
// If no bucket existed that contained the
// given object for the old value, then something
// went wrong:
if (bucket == null)
throw new RuntimeException(
"Metadata index corrupt: should contain object '" +
obj +
"' as having metadata '" +
metadataKey +
" = " +
oldValue +
"'. \nDetected while updating index to '" +
metadataKey +
" = " +
newValue +
"'.");
}
add(obj, metadataKey, newValue);
}
/**
* Empty the entire index.
*/
public void clear() {
// Clearing the whole ValueMap will
// properly orphan all KeyMaps and their
// buckets:
_valueMap.clear();
}
/**
* Add a new metadata key to the list of keys that
* this indexer indexes. The method accepts a collection
* of objects that were only indexed with the old metadata
* key set. These objects will be indexed additionally
* by the new key. This method will do nothing if the
* given 'new' metadata key is already part of the indexed
* set.
* @param newKey
* @param objsToIndex
*/
public void addMetadataKeyToIndex (
String newKey,
TreeSetRandomSubsetIterable<PhotoSpreadObject> objsToIndex) {
// If the 'new' key is already being indexed by
// this indexer, then no work is to be done:
if (_keysToIndex.contains(newKey))
return;
_keysToIndex.add(newKey);
if (objsToIndex == null)
return;
// Run through every object in objsToIndex, get
// their metadata for the new key, and index
// the object for that new key
for (PhotoSpreadObject obj : objsToIndex) {
add(obj, newKey, obj.getMetaData(newKey));
}
}
public void addMetadataKeyToIndex (String newKey) {
addMetadataKeyToIndex(newKey, null);
}
public void addMetadataKeysToIndex (
ArrayList<String> newKeys,
TreeSetRandomSubsetIterable<PhotoSpreadObject> objsToIndex) {
for (String key : newKeys)
addMetadataKeyToIndex(key, objsToIndex);
}
public void addMetadataKeysToIndex (ArrayList<String> newKeys) {
for (String key : newKeys)
addMetadataKeyToIndex(key, null);
}
/**
* Clear the index. Then run over all the given objects
* and index them for all of this indexer's metadata keys.
* @param objs
*/
public void rebuildIndex(TreeSetRandomSubsetIterable<PhotoSpreadObject> objs) {
clear();
for (PhotoSpreadObject obj : objs)
add(obj);
}
/**
* Service method: make a new keymap and initialize it
* with all the metadata keys that this indexer indexes.
* Install the new keymap as the target of the given
* metadataValue in the ValueMap. If a KeyMap already
* exists in this indexer for the given metadata value,
* this method does nothing.
* @param metadataValue
* @return The newly created KeyMap.
*/
private KeyMap installNewKeymap (String metadataValue) {
KeyMap keyMap = null;
keyMap = _valueMap.get(metadataValue);
// If ValueMap already contains a KeyMap for
// the given value, do nothing:
if (keyMap != null)
return keyMap;
keyMap = new KeyMap();
for (String mdKey : _keysToIndex)
keyMap.put(mdKey, new Bucket());
_valueMap.put(metadataValue, keyMap);
return keyMap;
}
}