/**
*
*/
package com.trendrr.oss;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.trendrr.json.simple.JSONAware;
import com.trendrr.json.simple.JSONFormatter;
import com.trendrr.json.simple.JSONObject;
import com.trendrr.json.simple.JSONValue;
/**
*
* A dynamic map.
*
*
* caching:
*
* set cacheEnabled to use an internal cache of TypeCasted results.
* This is usefull for frequently read maps, with expensive conversions (i.e. string -> map, or list conversions)
* Raises the memory footprint somewhat and adds some time to puts and removes
*
*
*
*
*
*
* @author Dustin Norlander
* @created Dec 29, 2010
*
*/
public class DynMap extends HashMap<String,Object> implements JSONAware{
private static final long serialVersionUID = 6342683643643465570L;
private static Logger log = Logger.getLogger(DynMap.class.getCanonicalName());
private ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<String, Object>();
private boolean cacheEnabled = false;
public DynMap () {
super();
}
/**
* creates a new DynMap with an initial key, val
* @param key
* @param val
*/
public DynMap(String key, Object val) {
this();
this.put(key, val);
}
/**
* Creates a new dynMap based on the passed in object. This is just a wrapper
* around DynMapFactory.instance().
*
* if object is already a DynMap then that dynmap is returned.
*
* @param object
* @return
*/
public static DynMap instance(Object object) {
return DynMapFactory.instance(object);
}
/**
* Creates a new dynMap based on the passed in object. This is just a wrapper
* around DynMapFactory.instance()
*
* @param object
* @param
* @return
*/
public static DynMap instance(Object object, DynMap defaultMap) {
DynMap obj = DynMapFactory.instance(object);
if (obj == null)
return defaultMap;
return obj;
}
/*
* Register Date and with the json formatter so we get properly encoded strings.
*/
static {
JSONValue.registerFormatter(Date.class, new JSONFormatter() {
@Override
public String toJSONString(Object value) {
return "\"" + IsoDateUtil.getIsoDate((Date)value) + "\"";
}
});
}
/**
* puts the value if the key is absent (or null).
* @param key
* @param val
* @return null if the key is absent, else returns the existing val for that key (as in java 8 java.util.HashMap)
*/
public Object putIfAbsent(String key, Object val) {
if (this.get(key) == null) {
this.put(key, val);
return null;
} else {
return this.get(key);
}
}
/**
* puts the value if the key is absent (or null).
* @param key
* @param val
*/
public void putIfAbsentWithDot(String key, Object val) {
if (this.get(key) == null) {
this.putWithDot(key, val);
}
}
/**
* just like putAll from map, will return if passed in map is null instead of throwing NPE
*/
@Override
public void putAll(Map mp) {
if (mp == null)
return;
for (Object k : mp.keySet()) {
this.put(k.toString(), mp.get(k));
}
}
/**
* like the regular putAll but only copies the
* passed in keys
* @param mp
* @param keys
*/
public void putAll(Map mp, String ...keys) {
for (String k : keys) {
this.put(k, mp.get(k));
}
}
/**
* like the regular putAll but honors dot notation.
* passed in keys
* @param mp
*/
public void putAllWithDot(Map mp) {
if (mp == null)
return;
for (Object k : mp.keySet()) {
this.putWithDot(k.toString(), mp.get(k));
}
}
@Override
public Object put(String key, Object val) {
this.ejectFromCache(key);
return super.put(key, val);
}
/**
* Puts the value if and only if the key and val are not null.
*
* @param key
* @param val
* @return
*/
public void putIfNotNull(String key, Object val) {
if (key == null || val == null)
return;
put(key, val);
}
/**
* Puts the value if and only if the key and val are not null.
* Will honor the dot operator of the key
* @param key
* @param val
* @return
*/
public void putIfNotNullWithDot(String key, Object val) {
if (key == null || val == null)
return;
putWithDot(key, val);
}
/**
* does a put but will honor the dot operator. ex:
*
* put("this.that.val", 0);
*
* will do:
*
* {
* this : {
* that : {
* val : 0
* }
* }
* }
* @param key
* @param val
*/
public void putWithDot(String key, Object val) {
if (key == null || key.isEmpty())
return;
String[] keys = key.split("\\.");
if (keys.length == 1) {
this.put(key, val);
return;
}
DynMap mp = this;
for (int i=0; i < keys.length-1; i++) {
String k = keys[i];
mp.putIfAbsent(k, new DynMap());
DynMap tmp = mp.getMap(k);
mp.put(k, tmp); //we readd it in case the map needed to be typecasted.
mp = tmp;
}
mp.put(keys[keys.length-1], val);
}
public void removeAll(String...keys) {
for (String k : keys) {
this.remove(k);
}
}
/**
* renames a key. this is just shorthand for:
*
* mp.put(newKey, mp.remove(currentKey));
*
* @param currentKey
* @param newKey
*/
public void rename(String currentKey, String newKey) {
Object v = this.remove(currentKey);
if (v == null)
return;
this.put(newKey, v);
}
@Override
public DynMap clone() {
return DynMapFactory.clone(this);
}
private void ejectFromCache(String key) {
if (!this.isCacheEnabled())
return;
//TODO: this is a dreadful implementation.
Set<String> keys = new HashSet<String>();
for (String k : cache.keySet()) {
if (k.startsWith(key + ".")) {
keys.add(k);
}
}
for (String k : keys) {
cache.remove(k);
}
cache.remove(key);
}
boolean isCacheEnabled() {
return cacheEnabled;
}
void setCacheEnabled(boolean cacheEnabled) {
this.cacheEnabled = cacheEnabled;
if (!this.cacheEnabled)
this.cache.clear();
}
/**
* Gets the requested object from the map.
*
* this differs from the standard map.get in that you can
* use the dot operator to get a nested value:
*
* map.get("key1.key2.key3");
*
* @param k
* @return
*/
@Override
public Object get(Object k) {
String key = (String)k;
Object val = super.get(key);
if (val != null) {
return val;
}
if (key.contains(".")) {
//try to reach into the object..
String[] items = key.split("\\.");
DynMap cur = this.get(DynMap.class, items[0]);
if (cur == null) {
return null;
}
for (int i= 1; i < items.length-1; i++) {
cur = cur.get(DynMap.class, items[i]);
if (cur == null)
return null;
}
return cur.get(items[items.length-1]);
}
return null;
}
public <T> T get(Class<T> cls, String key) {
//cache the result..
if (this.cacheEnabled) {
String cacheKey = key + "." + cls.getCanonicalName();
if (this.cache.containsKey(cacheKey)) {
//null is an acceptable cache result.
return (T)this.cache.get(cacheKey);
} else {
T val = TypeCast.cast(cls,this.get(key));
this.cache.put(cacheKey, val);
return val;
}
}
return TypeCast.cast(cls, this.get(key));
}
/**
* Removes key from the mapping. works with dot operator.
*/
@Override
public Object remove(Object k)
{
this.ejectFromCache((String)k);
String key = (String)k;
Object val = super.remove(key);
if (val == null && key.contains(".")) {
//try to reach into the object..
String[] items = key.split("\\.");
DynMap cur = this.getMap(items[0]);
if (cur == null) {
return null;
}
this.put(items[0], cur);
for (int i= 1; i < items.length-1; i++) {
DynMap cur1 = cur.getMap(items[i]);
if (cur1 == null)
return null;
cur.put(items[i], cur1); //we readd it in case the map needed to be typecasted.
cur = cur1;
}
return cur.remove(items[items.length-1]);
}
return val;
}
public <T> T get(Class<T> cls, String key, T defaultValue) {
T val = this.get(cls, key);
if (val == null )
return defaultValue;
return val;
}
public Boolean getBoolean(String key) {
return this.get(Boolean.class, key);
}
public Boolean getBoolean(String key, Boolean defaultValue) {
return this.get(Boolean.class, key, defaultValue);
}
public String getString(String key) {
return this.get(String.class, key);
}
public String getString(String key, String defaultValue) {
return this.get(String.class, key, defaultValue);
}
public Integer getInteger(String key) {
return this.get(Integer.class, key);
}
public Integer getInteger(String key, Integer defaultValue) {
return this.get(Integer.class, key, defaultValue);
}
public Double getDouble(String key) {
return this.get(Double.class, key);
}
public Double getDouble(String key, Double defaultValue) {
return this.get(Double.class, key, defaultValue);
}
public Long getLong(String key) {
return this.get(Long.class, key);
}
public Long getLong(String key, Long defaultValue) {
return this.get(Long.class, key, defaultValue);
}
public DynMap getMap(String key) {
return this.get(DynMap.class, key);
}
public DynMap getMap(String key, DynMap defaultValue) {
return this.get(DynMap.class, key, defaultValue);
}
public Date getDate(String key) {
return this.get(Date.class, key);
}
public Date getDate(String key, Date defaultValue) {
return this.get(Date.class, key, defaultValue);
}
/**
* Returns a keyset with fullkeys
* @return
*/
public Set<String> keySetWithDot(){
Set<String> keyset = new HashSet<String>();
keyset = getFullKey(this);
return keyset;
}
private Set<String> getFullKey(DynMap map){
//String fullkey = "";
Set<String> keyset = new HashSet<String>();
for(String key:map.keySet()){
if(map.getMap((String) key)!=null){
Iterator it = (getFullKey(map.getMap((String) key))).iterator();
while(it.hasNext()){
keyset.add((String)key + "." + it.next());
}
}
else
{
keyset.add((String) key);
}
}
return keyset;
}
/**
* Returns a typed list. See TypeCast.getTypedList
*
* returns the typed list, or null, never empty.
* @param <T>
* @param cls
* @param key
* @param delimiters
* @return
*/
public <T> List<T> getList(Class<T> cls, String key, String... delimiters) {
//cache the result..
if (this.cacheEnabled) {
String cacheKey = key + ".LIST." + cls.getCanonicalName() + ".";
if (this.cache.containsKey(cacheKey)) {
//null is an acceptable cache result.
return (List<T>)this.cache.get(cacheKey);
} else {
List<T> val = this.getListForKey(cls, key, delimiters);
this.cache.put(cacheKey, val);
return val;
}
}
return this.getListForKey(cls, key, delimiters);
}
/**
* Recursively searches through a dynmap to return a list of values that match the input key.
* If "key1.key2.key3" is the key
* @param <T>
* @param cls
* @param key
* @param delimiters
* @return
*/
public <T> List<T> getListForKey(Class<T> cls, String key, String... delimiters) {
String[] keyArray = key.split("\\.",2);
String topKey = keyArray[0];
String remainKey = (keyArray.length >1) ? keyArray[1] : key;
List<T> retList = new ArrayList<T>();
if(keyArray.length < 2){//down to the last subkey, take whatever we have there
List<T> val = TypeCast.toTypedList(cls, this.get(topKey), delimiters);
if(val!=null){
// System.out.println("adding val: "+val);
retList.addAll(val);
}
}else{
List<DynMap> dynMapList = TypeCast.toTypedList(DynMap.class, this.get(topKey), delimiters);
// System.out.println("dynlist for "+topKey+" : "+dynMapList);
if(dynMapList != null){
for(DynMap map : dynMapList){
retList.addAll(map.getListForKey(cls, remainKey, delimiters));
}
}
}
// if(dynMapList==null){
// if(keyArray.length==1){
// List<T> val = TypeCast.toTypedList(cls, this.get(topKey), delimiters);
// if(val!=null){
// System.out.println("adding val nullist: "+val);
// retList.addAll(val);
// }
// }
//
// }else if(keyArray.length==1){
// List<T> val = TypeCast.toTypedList(cls, this.get(topKey), delimiters);
// if(val!=null){
// System.out.println("adding val: "+val);
// retList.addAll(val);
// }
// }else{
// for(DynMap map : dynMapList){
// retList.addAll(map.getListForKey(cls, remainKey, delimiters));
// }
// }
// if(dynMapList==null || (cls.equals(DynMap.class) && keyArray.length==1)){
// //if we're looking for a dynmap and we got to the bottom search key, stop and take the dynmap
// List<T> val = TypeCast.toTypedList(cls, this.get(topKey), delimiters);
// if(val!=null){
// System.out.println("adding val: "+val);
// retList.addAll(val);
// }
// }else{
// for(DynMap map : dynMapList){
// retList.addAll(map.getListForKey(cls, remainKey, delimiters));
// }
// }
return retList;
}
/**
* Same as getList only returns an empty typed list, never null.
* @param <T>
* @param cls
* @param key
* @param delimiters
* @return
*/
public <T> List<T> getListOrEmpty(Class<T> cls, String key, String... delimiters) {
List<T> lst = this.getList(cls, key, delimiters);
if (lst == null) {
return new ArrayList<T>();
}
return lst;
}
/**
* adds the following elements to a list at the requested key.
* if the item at the key is not currently a list, then it is converted to a list.
*
* @param key
* @param elements
*/
public void addToList(String key, Object ...elements) {
List lst = this.getListOrEmpty(Object.class, key);
for (Object obj : elements) {
lst.add(obj);
}
this.put(key,lst);
}
/**
* same as addToList, but will honor the dot operator.
* @param key
* @param elements
*/
public void addToListWithDot(String key, Object ...elements) {
List lst = this.getListOrEmpty(Object.class, key);
for (Object obj : elements) {
lst.add(obj);
}
this.putWithDot(key,lst);
}
/**
* same principle as jquery extend.
*
* each successive map will override any properties in the one before it.
*
* this works recursively, so properties in embedded maps are extended instead of overwritten.
*
* Last map in the params is considered the most important one.
*
*
*
* @param map1
* @param maps
* @return this, allows for chaining
*/
public DynMap extend(Object map1, Object ...maps) {
if (map1 == null)
return this;
DynMap mp1 = DynMapFactory.instance(map1);
if (mp1 == null)
return this;
_extend(this, mp1);
for (Object m : maps) {
_extend(this, DynMapFactory.instance(m));
}
return this;
}
/**
* updates the mp1 map inline, recursively extends the map
* @param mp1
* @param mp2
* @return
*/
private static DynMap _extend(DynMap mp1, DynMap mp2) {
if (mp2 == null)
return mp1;
for (String key : mp2.keySet()) {
if (mp1.containsKey(key)) {
//need to check if this is a map.
DynMap mpA = mp1.getMap(key);
if (mpA != null && !mpA.isEmpty()) {
DynMap mpB = mp2.getMap(key);
if (mpB != null) {
mp1.put(key, _extend(mpA, mpB));
continue;
} else {
mp1.put(key, mp2.get(key));
continue;
}
}
}
mp1.put(key, mp2.get(key));
}
return mp1;
}
/**
* returns true if the passed in object map is equivelent
* to this map. will check members of lists, String, maps, numbers.
*
*
* does not check order
*
* @param map
* @return
*/
public boolean equivalent(Object map) {
DynMap other = DynMap.instance(map, new DynMap());
if (!ListHelper.equivalent(other.keySet(), this.keySet())) {
return false;
}
for (String key : this.keySet()) {
Object mine = this.get(key);
Object yours = other.get(key);
// log.info("mine: " + mine + " VS yours: " + yours);
if (mine == null && yours == null)
continue;
if (mine == null || yours == null) {
// log.info("key : " + key + " is null ");
return false;
}
if (ListHelper.isCollection(mine)) {
if (!ListHelper.isCollection(yours)) {
// log.info("key : " + key + " is not a collection ");
return false;
}
if (!ListHelper.equivalent(mine, yours)) {
// log.info("key : " + key + " collection not equiv ");
return false;
}
} else if (isMap(mine)) {
if (!DynMap.instance(mine, new DynMap()).equivalent(yours)) {
// log.info("key : " + key + " map not equiv ");
return false;
}
} else {
//default to string compare.
if (!this.getString(key).equals(other.getString(key))) {
// log.info("key : " + key + " " + this.getString(key) + " VS " + other.getString(key));
return false;
}
}
}
return true;
}
public static boolean isMap(Object obj) {
if (obj instanceof Map)
return true;
if (obj instanceof DynMapConvertable)
return true;
if (Reflection.hasMethod(obj, "toMap"))
return true;
return false;
}
public String toJSONString() {
return JSONObject.toJSONString(this);
}
/**
* returns [key,value]
* @param mp
* @return
* @throws UnsupportedEncodingException
*/
private Map<String,Object> toUrlMap(String keyStart, Map mp) throws UnsupportedEncodingException {
Map<String, Object> ret = new TreeMap<String,Object>();
for (Object key : mp.keySet()) {
Object val = mp.get(key);
String k = keyStart + "[" + URLEncoder.encode(key.toString(), "utf-8") + "]";
if (val instanceof Map) {
ret.putAll(toUrlMap(k, (Map)val));
} else {
ret.put(k, val);
}
}
return ret;
}
/**
* adds an encoded key, not encoded value.
* @param str
* @param k
* @param value
* @throws UnsupportedEncodingException
*/
private void addUrlKey(StringBuilder str, String k, Object value) throws UnsupportedEncodingException {
if (k == null || value == null)
return;
if (ListHelper.isCollection(value)) {
for (String v : TypeCast.toTypedList(String.class, value)) {
this.addUrlKey(str, k, v);
}
} else {
String v = URLEncoder.encode(TypeCast.cast(String.class, value), "utf-8");
str.append(k);
str.append("=");
str.append(v);
str.append("&");
}
}
/**
* will return the map as a url encoded string in the form:
* key1=val1&key2=val2& ...
*
* This can be used as getstring or form-encoded post.
* Lists are handled as multiple key /value pairs.
*
* Maps are encoded rails style, so key[key1][key2]=value
*
* will skip keys that contain null values.
* keys are sorted alphabetically so ordering is consistent
*
*
*
*
* @return The url encoded string, or empty string.
*/
public String toURLString() {
StringBuilder str = new StringBuilder();
List<String> keys = new ArrayList<String>();
keys.addAll(this.keySet());
Collections.sort(keys);
for (String key : keys) {
try {
String k = URLEncoder.encode(key, "utf-8");
Object val = this.get(k);
if (val instanceof Map) {
Map<String, Object> kv = this.toUrlMap(k, (Map)val);
for (String mpkey : kv.keySet()) {
this.addUrlKey(str, mpkey, kv.get(mpkey));
}
} else {
this.addUrlKey(str, k, val);
}
} catch (Exception x) {
log.log(Level.INFO, "Caught", x);
continue;
}
}
//trim trailing amp?
return StringHelper.trim(str.toString(), "&");
}
private String toXMLStringCollection(java.util.Collection c, XMLFormatter xmlFormatter) {
if (c == null)
return "";
String collection = "";
for (Object o : c) {
collection += "<item>";
if (o instanceof DynMap)
collection += ((DynMap) o).toXMLString(xmlFormatter);
else if (o instanceof java.util.Collection) {
for (Object b : (java.util.Collection) o) {
collection += "<item>";
if (b instanceof java.util.Collection)
collection += this
.toXMLStringCollection((java.util.Collection) b, xmlFormatter);
else
collection += xmlFormatter.cleanValue(b.toString());
collection += "</item>";
}
} else if (o instanceof java.util.Map) {
DynMap dm = new DynMap();
dm.putAll((java.util.Map) o);
collection += dm.toXMLString(xmlFormatter);
} else
collection += xmlFormatter.cleanValue(o.toString());
collection += "</item>";
}
return collection;
}
/**
* Constructs an xml string from this dynmap.
* @return
*/
public String toXMLString() {
return toXMLString(new SimpleXmlFormatter());
}
public String toXMLString(XMLFormatter xmlFormatter) {
if (this.isEmpty())
return null;
StringBuilder buf = new StringBuilder();
Iterator iter = this.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
String element = xmlFormatter.cleanKey(String.valueOf(entry.getKey()));
buf.append("<" + element + ">");
if (entry.getValue() instanceof DynMap) {
buf.append(((DynMap) entry.getValue())
.toXMLString(xmlFormatter));
} else if ((entry.getValue()) instanceof java.util.Collection) {
buf.append(this
.toXMLStringCollection((java.util.Collection) entry
.getValue(), xmlFormatter));
} else if ((entry.getValue()) instanceof java.util.Map) {
DynMap dm = DynMapFactory.instance(entry.getValue());
buf.append(dm.toXMLString(xmlFormatter));
} else if ((entry.getValue()) instanceof Date) {
buf.append(IsoDateUtil.getIsoDateNoMillis(((Date)entry.getValue())));
} else {
if (entry.getValue() != null) {
buf.append(xmlFormatter.cleanValue(entry.getValue().toString()));
} else {
buf.append(entry.getValue());
}
}
buf.append("</" + element + ">");
}
return buf.toString();
}
}