blob: e3a84f033deeb8d32be284ebd39aa8ef2924c2dc [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.brooklyn.util.collections;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import javax.annotation.Nonnull;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.Boxing;
import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.primitives.Primitives;
/** Jsonya = JSON-yet-another (tool)
* <p>
* provides conveniences for working with maps and lists containing maps and lists,
* and other datatypes too, easily convertible to json.
* <p>
* 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 <T extends Map<?,?>> Navigator<T> of(T map) {
return new Navigator<T>(map, MutableMap.class);
}
/** creates a {@link Navigator} backed by the map at the focus of the given navigator */
public static <T extends Map<?,?>> Navigator<T> of(Navigator<T> navigator) {
return new Navigator<T>(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<MutableMap<Object,Object>> newInstance() {
return new Navigator<MutableMap<Object,Object>>(MutableMap.class);
}
/** convenience for {@link Navigator#at(Object, Object...)} on a {@link #newInstance()} */
public static Navigator<MutableMap<Object,Object>> at(Object ...pathSegments) {
return newInstance().atArray(pathSegments);
}
/** as {@link #newInstance()} but using the given translator to massage objects inserted into the Jsonya structure */
public static Navigator<MutableMap<Object,Object>> newInstanceTranslating(Function<Object,Object> translator) {
return newInstance().useTranslator(translator);
}
/** as {@link #newInstanceTranslating(Function)} using an identity function
* (functionally equivalent to {@link #newInstance()} but explicit about it */
public static Navigator<MutableMap<Object,Object>> newInstanceLiteral() {
return newInstanceTranslating(Functions.identity());
}
/** as {@link #newInstanceTranslating(Function)} using a function which only supports JSON primitives:
* maps and collections are traversed, strings and primitives are inserted, and everything else has toString applied.
* see {@link JsonPrimitiveDeepTranslator} */
public static Navigator<MutableMap<Object,Object>> newInstancePrimitive() {
return newInstanceTranslating(new JsonPrimitiveDeepTranslator());
}
/** convenience for converting an object x to something which consists only of json primitives, doing
* {@link #toString()} on anything which is not recognised. see {@link JsonPrimitiveDeepTranslator} */
public static Object convertToJsonPrimitive(Object x) {
if (x==null) return null;
if (x instanceof Map) return newInstancePrimitive().put((Map<?,?>)x).getRootMap();
return newInstancePrimitive().put("data", x).getRootMap().get("data");
}
/** tells whether {@link #convertToJsonPrimitive(Object)} returns an object which is identical to
* the equivalent literal json structure. this is typically equivalent to saying serializing to json then
* deserializing will produce something where the result is equal to the input,
* modulo a few edge cases such as longs becoming ints.
* note that the converse (input equal to output) may not be the case,
* e.g. if the input contains special subclasses of collections of maps who care about type preservation. */
public static boolean isJsonPrimitiveCompatible(Object x) {
if (x==null) return true;
return convertToJsonPrimitive(x).equals(x);
}
public static boolean isTypeJsonPrimitiveCompatible(Object x) {
if (x==null) return true;
return x instanceof Map || x instanceof Collection || x instanceof String || Boxing.isPrimitiveOrBoxedObject(x);
}
public static boolean isJsonPrimitive(Object x) {
return Boxing.isPrimitiveOrStringOrBoxedObject(x);
}
public static boolean isJsonPrimitiveDeep(Object x) {
if (x==null) return true;
if (isJsonPrimitive(x)) return true;
if (x instanceof Map) return !((Map<?,?>)x).entrySet().stream().anyMatch(ent -> !isJsonPrimitiveDeep(ent.getKey()) || !isJsonPrimitiveDeep(ent.getValue()));
if (x instanceof Collection) return !((Collection)x).stream().anyMatch(ent -> !isJsonPrimitiveDeep(ent));
return false;
}
@SuppressWarnings({"rawtypes","unchecked"})
public static class Navigator<T extends Map<?,?>> {
protected Object root;
protected final Class<? extends Map> mapType;
protected Object focus;
protected Stack<Object> focusStack = new Stack<Object>();
protected Function<Object,Void> creationInPreviousFocus;
protected Function<Object,Object> translator;
public Navigator(Object backingStore, Class<? extends Map> mapType) {
this.root = Preconditions.checkNotNull(backingStore);
this.focus = backingStore;
this.mapType = mapType;
}
public Navigator(Class<? extends Map> mapType) {
this.root = null;
this.focus = null;
this.mapType = mapType;
this.creationInPreviousFocus = new Function<Object, Void>() {
@Override
public Void apply(Object o) {
root = o;
return null;
}
};
}
// -------------- access and configuration
/** returns the object at the focus, or null if none */
public Object get() {
return focus;
}
/** as {@link #get()} but always wrapped in a {@link Maybe}, absent if null */
public @Nonnull Maybe<Object> getMaybe() {
return Maybe.fromNullable(focus);
}
/** returns the object at the focus, casted to the given type, null if none
* @throws ClassCastException if object exists here but of the wrong type */
public <V> V get(Class<V> type) {
return (V)focus;
}
/** as {@link #get(Class)} but always wrapped in a {@link Maybe}, absent if null
* @throws ClassCastException if object exists here but of the wrong type */
public @Nonnull <V> Maybe<V> getMaybe(Class<V> type) {
return Maybe.fromNullable(get(type));
}
/** gets the object at the indicated path from the current focus
* (without changing the path to that focus; use {@link #at(Object, Object...)} to change focus) */
// Jun 2014, semantics changed so that focus does not change, which is more natural
public Object get(Object pathSegment, Object ...furtherPathSegments) {
push();
at(pathSegment, furtherPathSegments);
Object result = get();
pop();
return result;
}
public Navigator<T> 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;
}
/** as {@link #getFocusMap()} but always wrapped in a {@link Maybe}, absent if null
* @throws ClassCastException if object exists here but of the wrong type */
public @Nonnull Maybe<T> getFocusMapMaybe() {
return Maybe.fromNullable(getFocusMap());
}
/** specifies a translator function to use when new data is added;
* by default everything is added as a literal (ie {@link Functions#identity()}),
* but if you want to do translation on the way in,
* set a translation function
* <p>
* note that translation should be idempotent as implementation may apply it multiple times in certain cases
*/
public Navigator<T> useTranslator(Function<Object,Object> translator) {
this.translator = translator;
return this;
}
protected Object translate(Object x) {
if (translator==null) return x;
return translator.apply(x);
}
protected Object translateKey(Object x) {
if (translator==null) return x;
// this could return the toString to make it strict json
// but json libraries seem to do that so not strictly necessary
return translator.apply(x);
}
// ------------- navigation (map mainly)
/** pushes the current focus to a stack, so that this location will be restored on the corresponding {@link #pop()} */
public Navigator<T> push() {
focusStack.push(focus);
return this;
}
/** pops the most recently pushed focus, so that it returns to the last location {@link #push()}ed */
public Navigator<T> pop() {
focus = focusStack.pop();
return this;
}
/** returns the navigator moved to focus at the indicated key sequence in the given map, creating the path needed */
public Navigator<T> at(Object pathSegment, Object ...furtherPathSegments) {
down(pathSegment, false);
return atArray(furtherPathSegments);
}
public Navigator<T> atArray(Object[] furtherPathSegments) {
for (Object p: furtherPathSegments)
down(p, false);
return this;
}
/** returns the navigator moved to focus at the indicated key sequence in the given map, failing if not available */
public Navigator<T> atExisting(Object pathSegment, Object ...furtherPathSegments) {
down(pathSegment, true);
return atArray(furtherPathSegments);
}
/** ensures the given focus is a map, creating if needed (and creating inside the list if it is in a list) */
public Navigator<T> map() {
if (focus==null) {
focus = newMap();
creationInPreviousFocus.apply(focus);
}
if (focus instanceof List) {
Map m = newMap();
((List)focus).add(translate(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<T> put(Object k1, Object v1, Object ...kvOthers) {
map();
putInternal((Map)focus, k1, v1, kvOthers);
return this;
}
public Navigator<T> putIfNotNull(Object k1, Object v1) {
if (v1!=null) {
map();
putInternal((Map)focus, k1, v1);
}
return this;
}
protected void putInternal(Map target, Object k1, Object v1, Object ...kvOthers) {
assert (kvOthers.length % 2) == 0 : "even number of arguments required for put";
target.put(translateKey(k1), translate(v1));
for (int i=0; i<kvOthers.length; ) {
target.put(translateKey(kvOthers[i++]), translate(kvOthers[i++]));
}
}
/** as {@link #put(Object, Object, Object...)} for the kv-pairs in the given map; ignores null for convenience */
public Navigator<T> put(Map map) {
map();
if (map==null) return this;
((Map)focus).putAll((Map)translate(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<T> down(final Object pathSegment, boolean requireExisting) {
if (focus instanceof List) {
return downList(pathSegment, requireExisting);
}
if ((focus instanceof Map) || focus==null) {
return downMap(pathSegment, requireExisting);
}
throw new IllegalStateException("focus here is "+focus+"; cannot descend to '"+pathSegment+"'");
}
protected Navigator<T> downMap(Object pathSegmentO, boolean requireExisting) {
final Object pathSegment = translateKey(pathSegmentO);
final Map givenParentMap = (Map)focus;
if (givenParentMap!=null) {
creationInPreviousFocus = null;
focus = givenParentMap.get(pathSegment);
}
if (focus==null) {
if (requireExisting) {
throw new IllegalStateException("No key '"+pathSegmentO+"' found to descend");
}
final Function<Object, Void> previousCreation = creationInPreviousFocus;
creationInPreviousFocus = new Function<Object, Void>() {
@Override
public Void apply(Object input) {
creationInPreviousFocus = null;
Map parentMap = givenParentMap;
if (parentMap==null) {
parentMap = newMap();
previousCreation.apply(parentMap);
}
parentMap.put(pathSegment, translate(input));
return null;
}
};
}
return this;
}
protected Navigator<T> downList(final Object pathSegment, boolean requireExisting) {
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) {
if (requireExisting) {
throw new IllegalStateException("No index '"+pathSegment+"' found to descend");
}
// 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<Object, Void>() {
@Override
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<T> 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 if it doesn't already exist.
* if there is just one argument being added and the focus doesn't exist, that item is set as the focus.
* if there are more than one argument the focus is made as a map (and an even number of arguments is required).
* to add items to a list which might not exist, precede by a call to {@link #list()}.
* <p>
* 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...)}
* <p>
* 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)
* <p>
* 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<T> add(Object o1, Object ...others) {
if (focus==null) {
if (others.length>0) {
// default to map, but only if multiple args given
map();
} else {
// if single arg and no focus, focus becomes the arg, and no need to add
focus = o1;
if (creationInPreviousFocus!=null) {
creationInPreviousFocus.apply(o1);
}
return this;
}
}
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<T> addUnflattened(Object o1, Object ...others) {
((Collection)focus).add(translate(o1));
for (Object oi: others) ((Collection)focus).add(translate(oi));
return this;
}
protected void addInternal(Object initialFocus, Object currentFocus, Object o1, Object ...others) {
if (currentFocus instanceof Map) {
Map target = (Map)currentFocus;
Map source;
if (others.length==0) {
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)translate(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(translateKey(o1), translate(others[0]));
for (int i=1; i<others.length; )
source.put(translateKey(others[i++]), translate(others[i++]));
}
// and add the source map to the target
for (Object entry : source.entrySet()) {
Object key = ((Map.Entry)entry).getKey();
Object sv = ((Map.Entry)entry).getValue();
Object tv = target.get(key);
if (!target.containsKey(key)) {
target.put(key, sv);
} else {
addInternal(initialFocus, tv, sv);
}
}
return;
}
// lists are easy to add to, but remember we have to flatten
if (!(currentFocus instanceof Collection))
// TODO a nicer architecture might replace the current target with a list (also above where single non-map argument is supplied)
throw new IllegalStateException("cannot add: focus here is "+currentFocus+"; expected a collection");
addFlattened((Collection)currentFocus, o1);
for (Object oi: others) addFlattened((Collection)currentFocus, oi);
}
protected void addFlattened(Collection target, Object item) {
if (item instanceof Iterable) {
for (Object i: (Iterable)item)
addFlattened(target, i);
return;
}
if (item.getClass().isArray()) {
for (Object i: ((Object[])item))
addFlattened(target, i);
return;
}
// nothing to flatten
target.add(translate(item));
}
/** Returns JSON serialized output for given focus in the given jsonya;
* applies a naive toString for specialized types */
@Override
public String toString() {
return render(get());
}
}
public static String render(Object focus) {
if (focus instanceof Map) {
StringBuilder sb = new StringBuilder();
sb.append("{");
boolean first = true;
for (Object entry: ((Map<?,?>)focus).entrySet()) {
if (!first) sb.append(",");
else first = false;
sb.append(" ");
sb.append( render(((Map.Entry<?,?>)entry).getKey()) );
sb.append(": ");
sb.append( render(((Map.Entry<?,?>)entry).getValue()) );
}
sb.append(" }");
return sb.toString();
}
if (focus instanceof Collection) {
StringBuilder sb = new StringBuilder();
sb.append("[");
boolean first = true;
for (Object entry: (Collection<?>)focus) {
if (!first) sb.append(",");
else first = false;
sb.append( " " );
sb.append( render(entry) );
}
sb.append(" ]");
return sb.toString();
}
if (focus instanceof String) {
return JavaStringEscapes.wrapJavaString((String)focus);
}
if (focus == null || focus instanceof Number || focus instanceof Boolean)
return ""+focus;
return render(""+focus);
}
/** Converts an object to one which uses standard JSON objects where possible
* (strings, numbers, booleans, maps, lists), and uses toString elsewhere */
public static class JsonPrimitiveDeepTranslator implements Function<Object,Object> {
public static JsonPrimitiveDeepTranslator INSTANCE = new JsonPrimitiveDeepTranslator();
/** No need to instantiate except when subclassing. Use static {@link #INSTANCE}. */
protected JsonPrimitiveDeepTranslator() {}
@Override
public Object apply(Object input) {
return apply(input, new HashSet<Object>());
}
protected Object apply(Object input, Set<Object> stack) {
if (input==null) return applyNull(stack);
if (isPrimitiveOrBoxer(input.getClass()))
return applyPrimitiveOrBoxer(input, stack);
if (input instanceof String)
return applyString((String)input, stack);
stack = new HashSet<Object>(stack);
if (!stack.add(input))
// fail if object is self-recursive; don't even try toString as that is dangerous
// (extra measure of safety, since maps and lists generally fail elsewhere with recursive entries,
// eg in hashcode or toString)
return "[REF_ANCESTOR:"+stack.getClass()+"]";
if (input instanceof Collection<?>)
return applyCollection( (Collection<?>)input, stack );
if (input instanceof Map<?,?>)
return applyMap( (Map<?,?>)input, stack );
return applyOther(input, stack);
}
protected Object applyNull(Set<Object> stack) {
return null;
}
protected Object applyPrimitiveOrBoxer(Object input, Set<Object> stack) {
return input;
}
protected Object applyString(String input, Set<Object> stack) {
return input.toString();
}
protected Object applyCollection(Collection<?> input, Set<Object> stack) {
MutableList<Object> result = MutableList.of();
for (Object xi: input)
result.add(apply(xi, stack));
return result;
}
protected Object applyMap(Map<?, ?> input, Set<Object> stack) {
MutableMap<Object, Object> result = MutableMap.of();
for (Map.Entry<?,?> xi: input.entrySet())
result.put(apply(xi.getKey(), stack), apply(xi.getValue(), stack));
return result;
}
protected Object applyOther(Object input, Set<Object> stack) {
return input.toString();
}
public static boolean isPrimitiveOrBoxer(Class<?> type) {
return Primitives.allPrimitiveTypes().contains(type) || Primitives.allWrapperTypes().contains(type);
}
}
}