package brooklyn.util.collections;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
/** Jsonya = JSON-yet-another (tool)
*
* provides conveniences for working with maps and lists containing maps and lists,
* and other datatypes too, easily convertible to json.
*
* see {@link JsonyaTest} for examples
*
* @since 0.6.0
**/
@Beta
public class Jsonya {
private Jsonya() {}
/** creates a {@link Navigator} backed by the given map (focussed at the root) */
public static > Navigator of(T map) {
return new Navigator(map, MutableMap.class);
}
/** creates a {@link Navigator} backed by the map at the focus of the given navigator */
public static > Navigator of(Navigator navigator) {
return new Navigator(navigator.getFocusMap(), MutableMap.class);
}
/** creates a {@link Navigator} backed by a newly created map;
* the map can be accessed by {@link Navigator#getMap()} */
public static Navigator> newInstance() {
return new Navigator>(new MutableMap(), MutableMap.class);
}
/** convenience for {@link Navigator#at(Object, Object...)} on a {@link #newInstance()} */
public static Navigator> at(Object ...pathSegments) {
return newInstance().atArray(pathSegments);
}
@SuppressWarnings({"rawtypes","unchecked"})
public static class Navigator> {
protected final Object root;
protected final Class extends Map> mapType;
protected Object focus;
protected Function creationInPreviousFocus;
public Navigator(Object backingStore, Class extends Map> mapType) {
this.root = Preconditions.checkNotNull(backingStore);
this.focus = backingStore;
this.mapType = mapType;
}
// -------------- access
/** returns the object at the focus, or null if none */
public Object get() {
return focus;
}
/** returns the object at the focus, casted to the given type, null if none */
public V get(Class type) {
return (V)focus;
}
public Object get(Object pathSegment, Object ...furtherPathSegments) {
at(pathSegment, furtherPathSegments);
return get();
}
public Navigator root() {
focus = root;
return this;
}
/** returns the object at the root */
public Object getRoot() {
return root;
}
/** returns the {@link Map} at the root, throwing if root is not a map */
public T getRootMap() {
return (T) root;
}
/** returns a {@link Map} at the given focus, creating if needed (so never null),
* throwing if it exists already and is not a map */
public T getFocusMap() {
map();
return (T)focus;
}
// ------------- navigation (map mainly)
/** returns the navigator focussed at the indicated key sequence in the given map */
public Navigator at(Object pathSegment, Object ...furtherPathSegments) {
down(pathSegment);
return atArray(furtherPathSegments);
}
public Navigator atArray(Object[] furtherPathSegments) {
for (Object p: furtherPathSegments)
down(p);
return this;
}
/** ensures the given focus is a map, creating if needed (and creating inside the list if it is in a list) */
public Navigator map() {
if (focus==null) {
focus = newMap();
creationInPreviousFocus.apply(focus);
}
if (focus instanceof List) {
Map m = newMap();
((List)focus).add(m);
focus = m;
return this;
}
if (!(focus instanceof Map))
throw new IllegalStateException("focus here is "+focus+"; expected a map");
return this;
}
/** puts the given key-value pair at the current focus (or multiple such),
* creating a map if needed, replacing any values stored against keys supplied here;
* if you wish to merge deep maps, see {@link #add(Object, Object...)} */
public Navigator put(Object k1, Object v1, Object ...kvOthers) {
map();
putInternal((Map)focus, k1, v1, kvOthers);
return this;
}
protected static void putInternal(Map target, Object k1, Object v1, Object ...kvOthers) {
assert (kvOthers.length % 2) == 0 : "even number of arguments required for put";
target.put(k1, v1);
for (int i=0; i put(Map map) {
map();
((Map)focus).putAll(map);
return this;
}
protected Map newMap() {
try {
return mapType.newInstance();
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
/** utility for {@link #at(Object, Object...)}, taking one argument at a time */
protected Navigator down(final Object pathSegment) {
if (focus instanceof List) {
return downList(pathSegment);
}
if ((focus instanceof Map) || focus==null) {
return downMap(pathSegment);
}
throw new IllegalStateException("focus here is "+focus+"; cannot descend to '"+pathSegment+"'");
}
protected Navigator downMap(final Object pathSegment) {
final Map givenParentMap = (Map)focus;
if (givenParentMap!=null) {
creationInPreviousFocus = null;
focus = givenParentMap.get(pathSegment);
}
if (focus==null) {
final Function previousCreation = creationInPreviousFocus;
creationInPreviousFocus = new Function() {
public Void apply(Object input) {
creationInPreviousFocus = null;
Map parentMap = givenParentMap;
if (parentMap==null) {
parentMap = newMap();
previousCreation.apply(parentMap);
}
parentMap.put(pathSegment, input);
return null;
}
};
}
return this;
}
protected Navigator downList(final Object pathSegment) {
if (!(pathSegment instanceof Integer))
throw new IllegalStateException("focus here is a list ("+focus+"); cannot descend to '"+pathSegment+"'");
final List givenParentList = (List)focus;
// previous focus always non-null
creationInPreviousFocus = null;
focus = givenParentList.get((Integer)pathSegment);
if (focus==null) {
// don't need to worry about creation here; we don't create list entries simply by navigating
// TODO a nicer architecture would create a new object with focus for each traversal
// in that case we could create, filling other positions with null; but is there a need?
creationInPreviousFocus = new Function() {
public Void apply(Object input) {
throw new IllegalStateException("cannot create "+input+" here because we are at a non-existent position in a list");
}
};
}
return this;
}
// ------------- navigation (list mainly)
/** ensures the given focus is a list */
public Navigator list() {
if (focus==null) {
focus = newList();
creationInPreviousFocus.apply(focus);
}
if (!(focus instanceof List))
throw new IllegalStateException("focus here is "+focus+"; expected a list");
return this;
}
protected List newList() {
return new ArrayList();
}
/** adds the given items to the focus, whether a list or a map,
* creating the focus as a map if it doesn't already exist.
* to add items to a list which might not exist, precede by a call to {@link #list()}.
*
* when adding items to a list, iterable and array arguments are flattened because
* that makes the most sense when working with deep maps (adding one map to another where both contain lists, for example);
* to prevent flattening use {@link #addUnflattened(Object, Object...)}
*
* when adding to a map, arguments will be treated as things to put into the map,
* accepting either multiple arguments, as key1, value1, key2, value2, ...
* (and must be an event number); or a single argument which must be a map,
* in which case the value for each key in the supplied map is added to any existing value against that key in the target map
* (in other words, it will do a "deep put", where nested maps are effectively merged)
*
* this implementation will currently throw if you attempt to add a non-map to anything present which is not a list;
* auto-conversion to a list may be added in a future version
* */
public Navigator add(Object o1, Object ...others) {
if (focus==null) map();
addInternal(focus, focus, o1, others);
return this;
}
/** adds the given arguments to a list at this point (will not descend into maps, and will not flatten lists) */
public Navigator addUnflattened(Object o1, Object ...others) {
((Collection)focus).add(o1);
for (Object oi: others) ((Collection)focus).add(oi);
return this;
}
protected static void addInternal(Object initialFocus, Object currentFocus, Object o1, Object ...others) {
if (currentFocus instanceof Map) {
Map target = (Map)currentFocus;
Map source;
if (others.length==0) {
// add as a map
if (o1==null)
// ignore if null
return ;
if (!(o1 instanceof Map))
throw new IllegalStateException("cannot add: focus here is "+currentFocus+" (in "+initialFocus+"); expected a collection, or a map (with a map being added, not "+o1+")");
source = (Map)o1;
} else {
// build a source map from the arguments as key-value pairs
if ((others.length % 2)==0)
throw new IllegalArgumentException("cannot add an odd number of arguments to a map" +
" ("+o1+" then "+Arrays.toString(others)+" in "+currentFocus+" in "+initialFocus+")");
source = MutableMap.of(o1, others[0]);
for (int i=1; i