| /* |
| * 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); |
| } |
| } |
| |
| } |