All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.brooklyn.util.collections.Jsonya Maven / Gradle / Ivy

Go to download

Utility classes and methods developed for Brooklyn but not dependendent on Brooklyn or much else

There is a newer version: 1.1.0
Show newest version
/*
 * 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.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) 
 * 

* 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>(MutableMap.class); } /** convenience for {@link Navigator#at(Object, Object...)} on a {@link #newInstance()} */ public static Navigator> 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> newInstanceTranslating(Function 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> 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> 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); } @SuppressWarnings({"rawtypes","unchecked"}) public static class Navigator> { protected Object root; protected final Class mapType; protected Object focus; protected Stack focusStack = new Stack(); protected Function creationInPreviousFocus; protected Function translator; public Navigator(Object backingStore, Class mapType) { this.root = Preconditions.checkNotNull(backingStore); this.focus = backingStore; this.mapType = mapType; } public Navigator(Class mapType) { this.root = null; this.focus = null; this.mapType = mapType; this.creationInPreviousFocus = new Function() { @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 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 get(Class 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 Maybe getMaybe(Class 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 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 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 *

* note that translation should be idempotent as implementation may apply it multiple times in certain cases */ public Navigator useTranslator(Function 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 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 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 at(Object pathSegment, Object ...furtherPathSegments) { down(pathSegment, false); return atArray(furtherPathSegments); } public Navigator 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 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 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 put(Object k1, Object v1, Object ...kvOthers) { map(); putInternal((Map)focus, k1, v1, kvOthers); return this; } public Navigator 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 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 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 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 previousCreation = creationInPreviousFocus; creationInPreviousFocus = new Function() { @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 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() { @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 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()}. *

* 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) { 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 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)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 { 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()); } protected Object apply(Object input, Set 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(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 stack) { return null; } protected Object applyPrimitiveOrBoxer(Object input, Set stack) { return input; } protected Object applyString(String input, Set stack) { return input.toString(); } protected Object applyCollection(Collection input, Set stack) { MutableList result = MutableList.of(); for (Object xi: input) result.add(apply(xi, stack)); return result; } protected Object applyMap(Map input, Set stack) { MutableMap 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 stack) { return input.toString(); } public static boolean isPrimitiveOrBoxer(Class type) { return Primitives.allPrimitiveTypes().contains(type) || Primitives.allWrapperTypes().contains(type); } } }