package ecologylab.collections;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import ecologylab.generic.StringBuilderBaseUtils;
/**
* A scope (map: String -> T) with multiple ancestors.
* <p>
* NOTE that currently this class uses a LRU cache to cache look-ups for values from ancestors for
* efficiency, thus removing a value from an ancestor may not work properly. if you need removing
* please set CACHE_SIZE to zero or modify the class.
*
* @author quyin
*
* @param <T>
* The value type.
*/
@SuppressWarnings({ "serial", "unchecked" })
public class MultiAncestorScope<T> extends HashMap<String, T>
{
public static int CACHE_SIZE = 16;
/**
* ancestors of this scope.
*/
private List<Map<String, T>> ancestors;
/**
* for caching results from ancestors.
*/
private LinkedHashMap<String, T> queryCache;
public MultiAncestorScope()
{
super();
}
public MultiAncestorScope(int initialCapacity)
{
super(initialCapacity);
}
public MultiAncestorScope(int initialCapacity, float loadFactor)
{
super(initialCapacity, loadFactor);
}
public MultiAncestorScope(Map<String, T>... ancestors)
{
super();
this.addAncestors(ancestors);
}
public MultiAncestorScope(int initialCapacity, Map<String, T>... ancestors)
{
super(initialCapacity);
this.addAncestors(ancestors);
}
public MultiAncestorScope(int initialCapacity, float loadFactor, Map<String, T>... ancestors)
{
super(initialCapacity, loadFactor);
this.addAncestors(ancestors);
}
/**
* get the value of the given key from this scope. will look up this scope first. if not found,
* look up ancestors one by one, in the order of being added.
*/
@Override
public T get(Object key)
{
HashSet<Map<String, T>> visited = new HashSet<Map<String, T>>();
return getHelper(key, visited);
}
/**
* helper method that uses a visited hash set to reduce the time complexity of map look-up.
*
* @param key
* @param visited
* @return
*/
private T getHelper(Object key, HashSet<Map<String, T>> visited)
{
T result = super.get(key);
if (result == null)
result = this.getFromCache(key);
if (result == null)
{
if (this.ancestors != null)
for (Map<String, T> ancestor : this.ancestors)
if (containsSame(visited, ancestor))
continue;
else
{
visited.add(ancestor);
if (ancestor instanceof MultiAncestorScope)
result = ((MultiAncestorScope<T>) ancestor).getHelper(key, visited);
else
result = ancestor.get(key);
if (result != null)
{
this.putToCache(key, result);
break;
}
}
}
return result;
}
private T getFromCache(Object key)
{
return this.queryCache == null ? null : this.queryCache.get(key);
}
/**
* get a List of values from this scope AND its ancestors. values ordered from near to far.
*
* @param key
* @return
*/
public List<T> getAll(Object key)
{
List<T> results = new ArrayList<T>();
HashSet<Map<String, T>> visited = new HashSet<Map<String, T>>();
getAllHelper(key, visited, results);
return results;
}
private void getAllHelper(Object key, HashSet<Map<String, T>> visited, List<T> results)
{
T result = super.get(key);
if (result != null)
results.add(result);
if (this.ancestors != null)
for (Map<String, T> ancestor : this.ancestors)
if (containsSame(visited, ancestor))
continue;
else
{
visited.add(ancestor);
if (ancestor instanceof MultiAncestorScope)
((MultiAncestorScope<T>) ancestor).getAllHelper(key, visited, results);
else
{
result = ancestor.get(key);
if (result != null)
results.add(result);
}
}
}
/**
* only put value into the scope when it is not null. this prevents shadowing values with the
* same key in ancestors.
*
* @param key
* @param value
*/
public void putIfValueNotNull(String key, T value)
{
if (value != null)
put(key, value);
}
private void putToCache(Object key, T value)
{
if (this.queryCache == null)
this.queryCache = new LinkedHashMap<String, T>() {
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<String, T> eldest)
{
if (this.size() > CACHE_SIZE)
return true;
return false;
}
};
this.queryCache.put((String) key, value);
}
/**
* we need this because the ordinary contains() compares using equals() which is overridden by
* HashMap, thus not meeting our needs. We want to compare using == in some places in this class.
*
* @param <T>
* @param theCollection
* @param theElement
* @return
*/
private static <T> boolean containsSame(Collection<T> theCollection, T theElement)
{
for (T element : theCollection)
if (element == theElement)
return true;
return false;
}
/**
*
* @return the list of ancestors.
*/
public List<Map<String, T>> getAncestors()
{
return this.ancestors;
}
/**
* add an ancestor to (the end of) the ancestor list.
*
* @param ancestor
*/
public void addAncestor(Map<String, T> ancestor)
{
if (ancestor == null)
return;
if (this.ancestors == null)
this.ancestors = new ArrayList<Map<String, T>>();
else if (containsSame(this.ancestors, ancestor))
return;
this.ancestors.add(ancestor);
if (this.queryCache != null)
this.queryCache.clear();
}
/**
* add ancestors to (the end of) the ancestor list.
*
* @param ancestors
*/
public void addAncestors(Map<String, T>... ancestors)
{
if (ancestors != null)
for (Map<String, T> ancestor : ancestors)
this.addAncestor(ancestor);
}
/**
* remove an ancestor from the ancestor list.
*
* @param ancestor
*/
public void removeAncestor(Map<String, T> ancestor)
{
if (this.ancestors != null)
this.ancestors.remove(ancestor);
if (this.queryCache != null)
this.queryCache.clear();
}
@Override
public String toString()
{
StringBuilder sb = StringBuilderBaseUtils.acquire();
sb.append(this.getClass().getSimpleName()).append("[size: ").append(this.size()).append("]: ")
.append(super.toString());
if (this.ancestors != null && this.ancestors.size() > 0)
{
for (Map<String, T> ancestor : this.ancestors)
{
if (ancestor != null)
{
String ancestorStr = ancestor.toString();
sb.append("\n\t -> ");
sb.append(ancestorStr.replace("\n", "\n\t"));
}
}
}
String result = sb.toString();
StringBuilderBaseUtils.release(sb);
return result;
}
public void reset()
{
this.ancestors = null;
this.queryCache = null;
this.clear();
}
/**
* @param args
*/
public static void main(String[] args)
{
// inheritance relation:
// s1(1, 2) -> s2(3) -> s4(5)
// \-> s3() /
MultiAncestorScope<Integer> s1 = new MultiAncestorScope<Integer>();
s1.put("one", 1);
s1.put("two", 2);
MultiAncestorScope<Integer> s2 = new MultiAncestorScope<Integer>(0, s1);
s2.put("three", 3);
MultiAncestorScope<Integer> s3 = new MultiAncestorScope<Integer>(0, s1);
// s3.put("four", 4);
MultiAncestorScope<Integer> s4 = new MultiAncestorScope<Integer>(0, s2, s3);
s4.put("five", 5);
System.out.println(s4);
System.out.println(s4.get("five"));
System.out.println(s4.get("two"));
}
}