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

org.apache.juneau.collections.JsonMap Maven / Gradle / Ivy

// ***************************************************************************************************************************
// * 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.juneau.collections;

import static org.apache.juneau.common.internal.StringUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import static org.apache.juneau.internal.ConsumerUtils.*;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;

import org.apache.juneau.*;
import org.apache.juneau.common.internal.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;
import org.apache.juneau.marshaller.*;
import org.apache.juneau.objecttools.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.swap.*;

/**
 * Java implementation of a JSON object.
 *
 * 

* An extension of {@link LinkedHashMap}, so all methods available in that class are also available to this class. * *

* Note that the use of this class is optional for generating JSON. The serializers will accept any objects that implement the * {@link java.util.Map} interface. But this class provides some useful additional functionality when working with * JSON models constructed from Java Collections Framework objects. For example, a constructor is provided for * converting a JSON object string directly into a {@link Map}. It also contains accessor methods for to avoid common * typecasting when accessing elements in a list. * *

Example:
*

* // Construct an empty Map * JsonMap map = JsonMap.of(); * * // Construct a Map from JSON * map = JsonMap.ofJson("{a:'A',b:{c:'C',d:123}}"); * * // Construct a Map using the append method * map = JsonMap.of().a("foo","x").a("bar",123).a("baz",true); * * // Construct a Map from XML generated by XmlSerializer * String xml = "<object><a type='string'>A</a><b type='object'><c type='string'>C</c><d type='number'>123</d></b></object>"; * map = JsonMap.of(xml, XmlParser.DEFAULT); * * // Construct a Map from a URL GET parameter string generated by UrlEncodingParser * String urlParams = "?a='A'&b={c:'C',d:123}"; * map = JsonMap.of(urlParams, UrlEncodingParser.DEFAULT); * * // Construct JSON from JsonMap * map = JsonMap.ofJson("{foo:'bar'},{baz:[123,true]}"); * String json = map.toString(); // Produces "{foo:'bar'},{baz:[123,true]}" * json = map.toString(JsonSerializer.DEFAULT); // Equivalent * json = JsonSerializer.DEFAULT.serialize(map); // Equivalent * * // Get a map entry as an Integer * map = JsonMap.ofJson("{foo:123}"); * Integer integer = map.getInt("foo"); * integer = map.get(Integer.class, "foo"); // Equivalent * * // Get a map entry as a Float * map = JsonMap.ofJson("{foo:123}"); * Float _float = map.getFloat("foo"); * _float = map.get(Float.class, "foo"); // Equivalent * * // Same as above, except converted to a String * map = JsonMap.ofJson("{foo:123}"); * String string = map.getString("foo"); // Returns "123" * string = map.get(String.class, "foo"); // Equivalent * * // Get one of the entries in the list as a bean (converted to a bean if it isn't already one) * map = JsonMap.ofJson("{person:{name:'John Smith',age:45}}"); * Person person = map.get(Person.class, "person"); * * // Add an inner map * JsonMap map1 = JsonMap.ofJson("{a:1}"); * JsonMap map2 = JsonMap.ofJson("{b:2}").setInner(map1); * int _int = map2.getInt("a"); // a == 1 *

* *
Notes:
    *
  • This class is not thread safe. *
*/ public class JsonMap extends LinkedHashMap { //------------------------------------------------------------------------------------------------------------------ // Static //------------------------------------------------------------------------------------------------------------------ private static final long serialVersionUID = 1L; /** * An empty read-only JsonMap. * * @serial exclude */ public static final JsonMap EMPTY_MAP = new JsonMap() { private static final long serialVersionUID = 1L; @Override /* Map */ public Set> entrySet() { return Collections.emptyMap().entrySet(); } @Override /* Map */ public Set keySet() { return Collections.emptyMap().keySet(); } @Override /* Map */ public Object put(String key, Object value) { throw new UnsupportedOperationException("Not supported on read-only object."); } @Override /* Map */ public Object remove(Object key) { throw new UnsupportedOperationException("Not supported on read-only object."); } @Override /* Map */ public Collection values() { return Collections.emptyMap().values(); } }; /** * Construct an empty map. * * @return An empty map. */ public static JsonMap create() { return new JsonMap(); } /** * Construct an empty map. * * @return An empty map. */ public static JsonMap filteredMap() { return create().filtered(); } /** * Construct a map initialized with the specified map. * * @param values * The map to copy. *
Can be null. *
Keys will be converted to strings using {@link Object#toString()}. * @return A new map or null if the map was null. */ public static JsonMap of(Map values) { return values == null ? null : new JsonMap(values); } /** * Construct a map initialized with the specified JSON string. * * @param json * The JSON text to parse. *
Can be normal or simplified JSON. * @return A new map or null if the string was null. * @throws ParseException Malformed input encountered. */ public static JsonMap ofJson(CharSequence json) throws ParseException { return json == null ? null : new JsonMap(json); } /** * Construct a map initialized with the specified string. * * @param in * The input being parsed. *
Can be null. * @param p * The parser to use to parse the input. *
If null, uses {@link JsonParser}. * @return A new map or null if the input was null. * @throws ParseException Malformed input encountered. */ public static JsonMap ofText(CharSequence in, Parser p) throws ParseException { return in == null ? null : new JsonMap(in, p); } /** * Construct a map initialized with the specified reader containing JSON. * * @param json * The reader containing JSON text to parse. *
Can contain normal or simplified JSON. * @return A new map or null if the input was null. * @throws ParseException Malformed input encountered. */ public static JsonMap ofJson(Reader json) throws ParseException { return json == null ? null : new JsonMap(json); } /** * Construct a map initialized with the specified string. * * @param in * The reader containing the input being parsed. *
Can contain normal or simplified JSON. * @param p * The parser to use to parse the input. *
If null, uses {@link JsonParser}. * @return A new map or null if the input was null. * @throws ParseException Malformed input encountered. */ public static JsonMap ofText(Reader in, Parser p) throws ParseException { return in == null ? null : new JsonMap(in); } /** * Construct a map initialized with the specified key/value pairs. * *
Examples:
*

* JsonMap map = new JsonMap("key1","val1","key2","val2"); *

* * @param keyValuePairs A list of key/value pairs to add to this map. * @return A new map, never null. */ public static JsonMap of(Object... keyValuePairs) { return new JsonMap(keyValuePairs); } /** * Construct a map initialized with the specified key/value pairs. * *

* Same as {@link #of(Object...)} but calls {@link #filtered()} on the created map. * *

Examples:
*

* JsonMap map = new JsonMap("key1","val1","key2","val2"); *

* * @param keyValuePairs A list of key/value pairs to add to this map. * @return A new map, never null. */ public static JsonMap filteredMap(Object... keyValuePairs) { return new JsonMap(keyValuePairs).filtered(); } //------------------------------------------------------------------------------------------------------------------ // Instance //------------------------------------------------------------------------------------------------------------------ private transient BeanSession session; private Map inner; private transient ObjectRest objectRest; private transient Predicate valueFilter = x -> true; /** * Construct an empty map. */ public JsonMap() {} /** * Construct an empty map with the specified bean context. * * @param session The bean session to use for creating beans. */ public JsonMap(BeanSession session) { this.session = session; } /** * Construct a map initialized with the specified map. * * @param in * The map to copy. *
Can be null. *
Keys will be converted to strings using {@link Object#toString()}. */ public JsonMap(Map in) { this(); if (in != null) in.forEach((k,v) -> put(k.toString(), v)); } /** * Construct a map initialized with the specified JSON. * * @param json * The JSON text to parse. *
Can be normal or simplified JSON. * @throws ParseException Malformed input encountered. */ public JsonMap(CharSequence json) throws ParseException { this(json, JsonParser.DEFAULT); } /** * Construct a map initialized with the specified string. * * @param in * The input being parsed. *
Can be null. * @param p * The parser to use to parse the input. *
If null, uses {@link JsonParser}. * @throws ParseException Malformed input encountered. */ public JsonMap(CharSequence in, Parser p) throws ParseException { this(p == null ? BeanContext.DEFAULT_SESSION : p.getBeanContext().getSession()); if (p == null) p = JsonParser.DEFAULT; if (! StringUtils.isEmpty(in)) p.parseIntoMap(in, this, bs().string(), bs().object()); } /** * Construct a map initialized with the specified reader containing JSON. * * @param json * The reader containing JSON text to parse. *
Can contain normal or simplified JSON. * @throws ParseException Malformed input encountered. */ public JsonMap(Reader json) throws ParseException { parse(json, JsonParser.DEFAULT); } /** * Construct a map initialized with the specified string. * * @param in * The reader containing the input being parsed. *
Can contain normal or simplified JSON. * @param p * The parser to use to parse the input. *
If null, uses {@link JsonParser}. * @throws ParseException Malformed input encountered. */ public JsonMap(Reader in, Parser p) throws ParseException { this(p == null ? BeanContext.DEFAULT_SESSION : p.getBeanContext().getSession()); parse(in, p); } /** * Construct a map initialized with the specified key/value pairs. * *
Examples:
*

* JsonMap map = new JsonMap("key1","val1","key2","val2"); *

* * @param keyValuePairs A list of key/value pairs to add to this map. */ public JsonMap(Object... keyValuePairs) { if (keyValuePairs.length % 2 != 0) throw new IllegalArgumentException("Odd number of parameters passed into JsonMap(Object...)"); for (int i = 0; i < keyValuePairs.length; i+=2) put(stringify(keyValuePairs[i]), keyValuePairs[i+1]); } //------------------------------------------------------------------------------------------------------------------ // Initializers //------------------------------------------------------------------------------------------------------------------ /** * Set an inner map in this map to allow for chained get calls. * *

* If {@link #get(Object)} returns null, then {@link #get(Object)} will be called on the inner map. * *

* In addition to providing the ability to chain maps, this method also provides the ability to wrap an existing map * inside another map so that you can add entries to the outer map without affecting the values on the inner map. * *

* JsonMap map1 = JsonMap.ofJson("{foo:1}"); * JsonMap map2 = JsonMap.of().setInner(map1); * map2.put("foo", 2); // Overwrite the entry * int foo1 = map1.getInt("foo"); // foo1 == 1 * int foo2 = map2.getInt("foo"); // foo2 == 2 *

* * @param inner * The inner map. * Can be null to remove the inner map from an existing map. * @return This object. */ public JsonMap inner(Map inner) { this.inner = inner; return this; } /** * Override the default bean session used for converting POJOs. * *

* Default is {@link BeanContext#DEFAULT}, which is sufficient in most cases. * *

* Useful if you're serializing/parsing beans with transforms defined. * * @param session The new bean session. * @return This object. */ public JsonMap session(BeanSession session) { this.session = session; return this; } //------------------------------------------------------------------------------------------------------------------ // Appenders //------------------------------------------------------------------------------------------------------------------ /** * Adds an entry to this map. * * @param key The key. * @param value The value. * @return This object. */ public JsonMap append(String key, Object value) { put(key, value); return this; } /** * Appends all the entries in the specified map to this map. * * @param values The map to copy. Can be null. * @return This object. */ public JsonMap append(Map values) { if (values != null) super.putAll(values); return this; } /** * Add if flag is true. * * @param flag The flag to check. * @param key The key. * @param value The value. * @return This object. */ public JsonMap appendIf(boolean flag, String key, Object value) { if (flag) append(key, value); return this; } /** * Add if predicate matches value. * * @param The value type. * @param test The predicate to match against. * @param key The key. * @param value The value. * @return This object. */ public JsonMap appendIf(Predicate test, String key, T value) { return appendIf(test(test, value), key, value); } /** * Adds the first value that matches the specified predicate. * * @param The value types. * @param test The predicate to match against. * @param key The key. * @param values The values to test. * @return This object. */ @SafeVarargs public final JsonMap appendFirst(Predicate test, String key, T...values) { for (T v : values) if (test(test, v)) return append(key, v); return this; } /** * Adds a value in this map if the entry does not exist or the current value is null. * * @param key The map key. * @param value The value to set if the current value does not exist or is null. * @return This object. */ public JsonMap appendIfAbsent(String key, Object value) { return appendIfAbsentIf(x -> true, key, value); } /** * Adds a value in this map if the entry does not exist or the current value is null and the value matches the specified predicate. * * @param The value type. * @param predicate The predicate to test the value with. * @param key The map key. * @param value The value to set if the current value does not exist or is null. * @return This object. */ public JsonMap appendIfAbsentIf(Predicate predicate, String key, T value) { Object o = get(key); if (o == null && predicate.test(value)) put(key, value); return this; } /** * Enables filtering based on default values. * *

* Any of the following types will be ignored when set as values in this map: *

    *
  • null *
  • false *
  • -1 (any Number type) *
  • Empty arrays/collections/maps. *
* @return This object. */ public JsonMap filtered() { return filtered(x -> ! ( x == null || (x instanceof Boolean && x.equals(false)) || (x instanceof Number && ((Number)x).intValue() == -1) || (x.getClass().isArray() && Array.getLength(x) == 0) || (x instanceof Map && ((Map)x).isEmpty()) || (x instanceof Collection && ((Collection)x).isEmpty()) )); } /** * Enables filtering based on a predicate test. * *

* If the predicate evaluates to false on values added to this map, the entry will be skipped. * * @param value The value tester predicate. * @return This object. */ public JsonMap filtered(Predicate value) { valueFilter = value; return this; } //------------------------------------------------------------------------------------------------------------------ // Retrievers //------------------------------------------------------------------------------------------------------------------ /** * Same as {@link Map#get(Object) get()}, but casts or converts the value to the specified class type. * *

* This is the preferred get method for simple types. * *

Examples:
*

* JsonMap map = JsonMap.ofJson("..."); * * // Value converted to a string. * String string = map.get("key1", String.class); * * // Value converted to a bean. * MyBean bean = map.get("key2", MyBean.class); * * // Value converted to a bean array. * MyBean[] beanArray = map.get("key3", MyBean[].class); * * // Value converted to a linked-list of objects. * List list = map.get("key4", LinkedList.class); * * // Value converted to a map of object keys/values. * Map map2 = map.get("key5", TreeMap.class); *

* *

* See {@link BeanSession#convertToType(Object, ClassMeta)} for the list of valid data conversions. * * @param key The key. * @param The class type returned. * @param type The class type returned. * @return The value, or null if the entry doesn't exist. */ public T get(String key, Class type) { return getWithDefault(key, (T)null, type); } /** * Same as {@link #get(String,Class)}, but allows for complex data types consisting of collections or maps. * *

* The type can be a simple type (e.g. beans, strings, numbers) or parameterized type (collections/maps). * *

Examples:
*

* JsonMap map = JsonMap.ofJson("..."); * * // Value converted to a linked-list of strings. * List<String> list1 = map.get("key1", LinkedList.class, String.class); * * // Value converted to a linked-list of beans. * List<MyBean> list2 = map.get("key2", LinkedList.class, MyBean.class); * * // Value converted to a linked-list of linked-lists of strings. * List<List<String>> list3 = map.get("key3", LinkedList.class, LinkedList.class, String.class); * * // Value converted to a map of string keys/values. * Map<String,String> map1 = map.get("key4", TreeMap.class, String.class, String.class); * * // Value converted to a map containing string keys and values of lists containing beans. * Map<String,List<MyBean>> map2 = map.get("key5", TreeMap.class, String.class, List.class, MyBean.class); *

* *

* Collection classes are assumed to be followed by zero or one objects indicating the element type. * *

* Map classes are assumed to be followed by zero or two meta objects indicating the key and value types. * *

* The array can be arbitrarily long to indicate arbitrarily complex data structures. * *

* See {@link BeanSession#convertToType(Object, ClassMeta)} for the list of valid data conversions. * *

Notes:
    *
  • * Use the {@link #get(String, Class)} method instead if you don't need a parameterized map/collection. *
* * @param key The key. * @param The class type returned. * @param type The class type returned. * @param args The class type parameters. * @return The value, or null if the entry doesn't exist. */ public T get(String key, Type type, Type...args) { return getWithDefault(key, null, type, args); } /** * Same as {@link Map#get(Object) get()}, but returns the default value if the key could not be found. * * @param key The key. * @param def The default value if the entry doesn't exist. * @return The value, or the default value if the entry doesn't exist. */ public Object getWithDefault(String key, Object def) { Object o = get(key); return (o == null ? def : o); } /** * Same as {@link #get(String,Class)} but returns a default value if the value does not exist. * * @param key The key. * @param def The default value. Can be null. * @param The class type returned. * @param type The class type returned. * @return The value, or null if the entry doesn't exist. */ public T getWithDefault(String key, T def, Class type) { return getWithDefault(key, def, type, new Type[0]); } /** * Same as {@link #get(String,Type,Type...)} but returns a default value if the value does not exist. * * @param key The key. * @param def The default value. Can be null. * @param The class type returned. * @param type The class type returned. * @param args The class type parameters. * @return The value, or null if the entry doesn't exist. */ public T getWithDefault(String key, T def, Type type, Type...args) { Object o = get(key); if (o == null) return def; T t = bs().convertToType(o, type, args); return t == null ? def : t; } /** * Searches for the specified key in this map ignoring case. * * @param key * The key to search for. * For performance reasons, it's preferable that the key be all lowercase. * @return The key, or null if map does not contain this key. */ public String findKeyIgnoreCase(String key) { for (String k : keySet()) if (key.equalsIgnoreCase(k)) return k; return null; } /** * Same as {@link Map#get(Object) get()}, but converts the raw value to the specified class type using the specified * POJO swap. * * @param key The key. * @param objectSwap The swap class used to convert the raw type to a transformed type. * @param The transformed class type. * @return The value, or null if the entry doesn't exist. * @throws ParseException Malformed input encountered. */ @SuppressWarnings({ "rawtypes", "unchecked" }) public T getSwapped(String key, ObjectSwap objectSwap) throws ParseException { try { Object o = super.get(key); if (o == null) return null; ObjectSwap swap = objectSwap; return (T) swap.unswap(bs(), o, null); } catch (ParseException e) { throw e; } catch (Exception e) { throw new ParseException(e); } } /** * Returns the value for the first key in the list that has an entry in this map. * * @param keys The keys to look up in order. * @return The value of the first entry whose key exists, or null if none of the keys exist in this map. */ public Object find(String...keys) { for (String key : keys) if (containsKey(key)) return get(key); return null; } /** * Returns the value for the first key in the list that has an entry in this map. * *

* Casts or converts the value to the specified class type. * *

* See {@link BeanSession#convertToType(Object, ClassMeta)} for the list of valid data conversions. * * @param type The class type to convert the value to. * @param The class type to convert the value to. * @param keys The keys to look up in order. * @return The value of the first entry whose key exists, or null if none of the keys exist in this map. */ public T find(Class type, String...keys) { for (String key : keys) if (containsKey(key)) return get(key, type); return null; } /** * Returns the specified entry value converted to a {@link String}. * *

* Shortcut for get(key, String.class). * * @param key The key. * @return The converted value, or null if the map contains no mapping for this key. */ public String getString(String key) { return get(key, String.class); } /** * Returns the specified entry value converted to a {@link String}. * *

* Shortcut for get(key, String[].class). * * @param key The key. * @return The converted value, or null if the map contains no mapping for this key. */ public String[] getStringArray(String key) { return getStringArray(key, null); } /** * Same as {@link #getStringArray(String)} but returns a default value if the value cannot be found. * * @param key The map key. * @param def The default value if value is not found. * @return The value converted to a string array. */ public String[] getStringArray(String key, String[] def) { Object s = get(key, Object.class); if (s == null) return def; String[] r = null; if (s instanceof Collection) r = ArrayUtils.toStringArray((Collection)s); else if (s instanceof String[]) r = (String[])s; else if (s instanceof Object[]) r = ArrayUtils.toStringArray(alist((Object[])s)); else r = split(stringify(s)); return (r.length == 0 ? def : r); } /** * Returns the specified entry value converted to a {@link String}. * *

* Shortcut for getWithDefault(key, defVal, String.class). * * @param key The key. * @param defVal The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. */ public String getString(String key, String defVal) { return getWithDefault(key, defVal, String.class); } /** * Returns the specified entry value converted to an {@link Integer}. * *

* Shortcut for get(key, Integer.class). * * @param key The key. * @return The converted value, or null if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Integer getInt(String key) { return get(key, Integer.class); } /** * Returns the specified entry value converted to an {@link Integer}. * *

* Shortcut for getWithDefault(key, defVal, Integer.class). * * @param key The key. * @param defVal The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Integer getInt(String key, Integer defVal) { return getWithDefault(key, defVal, Integer.class); } /** * Returns the specified entry value converted to a {@link Long}. * *

* Shortcut for get(key, Long.class). * * @param key The key. * @return The converted value, or null if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Long getLong(String key) { return get(key, Long.class); } /** * Returns the specified entry value converted to a {@link Long}. * *

* Shortcut for getWithDefault(key, defVal, Long.class). * * @param key The key. * @param defVal The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Long getLong(String key, Long defVal) { return getWithDefault(key, defVal, Long.class); } /** * Returns the specified entry value converted to a {@link Boolean}. * *

* Shortcut for get(key, Boolean.class). * * @param key The key. * @return The converted value, or null if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Boolean getBoolean(String key) { return get(key, Boolean.class); } /** * Returns the specified entry value converted to a {@link Boolean}. * *

* Shortcut for getWithDefault(key, defVal, Boolean.class). * * @param key The key. * @param defVal The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Boolean getBoolean(String key, Boolean defVal) { return getWithDefault(key, defVal, Boolean.class); } /** * Returns the specified entry value converted to a {@link Map}. * *

* Shortcut for get(key, JsonMap.class). * * @param key The key. * @return The converted value, or null if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public JsonMap getMap(String key) { return get(key, JsonMap.class); } /** * Returns the specified entry value converted to a {@link JsonMap}. * *

* Shortcut for getWithDefault(key, defVal, JsonMap.class). * * @param key The key. * @param defVal The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public JsonMap getMap(String key, JsonMap defVal) { return getWithDefault(key, defVal, JsonMap.class); } /** * Same as {@link #getMap(String)} but creates a new empty {@link JsonMap} if it doesn't already exist. * * @param key The key. * @param createIfNotExists If mapping doesn't already exist, create one with an empty {@link JsonMap}. * @return The converted value, or an empty value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public JsonMap getMap(String key, boolean createIfNotExists) { JsonMap m = getWithDefault(key, null, JsonMap.class); if (m == null && createIfNotExists) { m = new JsonMap(); put(key, m); } return m; } /** * Same as {@link #getMap(String, JsonMap)} except converts the keys and values to the specified types. * * @param The key type. * @param The value type. * @param key The key. * @param keyType The key type class. * @param valType The value type class. * @param def The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Map getMap(String key, Class keyType, Class valType, Map def) { Object o = get(key); if (o == null) return def; return bs().convertToType(o, Map.class, keyType, valType); } /** * Returns the specified entry value converted to a {@link JsonList}. * *

* Shortcut for get(key, JsonList.class). * * @param key The key. * @return The converted value, or null if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public JsonList getList(String key) { return get(key, JsonList.class); } /** * Returns the specified entry value converted to a {@link JsonList}. * *

* Shortcut for getWithDefault(key, defVal, JsonList.class). * * @param key The key. * @param defVal The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public JsonList getList(String key, JsonList defVal) { return getWithDefault(key, defVal, JsonList.class); } /** * Same as {@link #getList(String)} but creates a new empty {@link JsonList} if it doesn't already exist. * * @param key The key. * @param createIfNotExists If mapping doesn't already exist, create one with an empty {@link JsonList}. * @return The converted value, or an empty value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public JsonList getList(String key, boolean createIfNotExists) { JsonList m = getWithDefault(key, null, JsonList.class); if (m == null && createIfNotExists) { m = new JsonList(); put(key, m); } return m; } /** * Same as {@link #getList(String, JsonList)} except converts the elements to the specified types. * * @param The element type. * @param key The key. * @param elementType The element type class. * @param def The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public List getList(String key, Class elementType, List def) { Object o = get(key); if (o == null) return def; return bs().convertToType(o, List.class, elementType); } /** * Returns the first entry that exists converted to a {@link String}. * *

* Shortcut for find(String.class, keys). * * @param keys The list of keys to look for. * @return * The converted value of the first key in the list that has an entry in this map, or null if the map * contains no mapping for any of the keys. */ public String findString(String... keys) { return find(String.class, keys); } /** * Returns the first entry that exists converted to an {@link Integer}. * *

* Shortcut for find(Integer.class, keys). * * @param keys The list of keys to look for. * @return * The converted value of the first key in the list that has an entry in this map, or null if the map * contains no mapping for any of the keys. * @throws InvalidDataConversionException If value cannot be converted. */ public Integer findInt(String... keys) { return find(Integer.class, keys); } /** * Returns the first entry that exists converted to a {@link Long}. * *

* Shortcut for find(Long.class, keys). * * @param keys The list of keys to look for. * @return * The converted value of the first key in the list that has an entry in this map, or null if the map * contains no mapping for any of the keys. * @throws InvalidDataConversionException If value cannot be converted. */ public Long findLong(String... keys) { return find(Long.class, keys); } /** * Returns the first entry that exists converted to a {@link Boolean}. * *

* Shortcut for find(Boolean.class, keys). * * @param keys The list of keys to look for. * @return * The converted value of the first key in the list that has an entry in this map, or null if the map * contains no mapping for any of the keys. * @throws InvalidDataConversionException If value cannot be converted. */ public Boolean findBoolean(String... keys) { return find(Boolean.class, keys); } /** * Returns the first entry that exists converted to a {@link JsonMap}. * *

* Shortcut for find(JsonMap.class, keys). * * @param keys The list of keys to look for. * @return * The converted value of the first key in the list that has an entry in this map, or null if the map * contains no mapping for any of the keys. * @throws InvalidDataConversionException If value cannot be converted. */ public JsonMap findMap(String... keys) { return find(JsonMap.class, keys); } /** * Returns the first entry that exists converted to a {@link JsonList}. * *

* Shortcut for find(JsonList.class, keys). * * @param keys The list of keys to look for. * @return * The converted value of the first key in the list that has an entry in this map, or null if the map * contains no mapping for any of the keys. * @throws InvalidDataConversionException If value cannot be converted. */ public JsonList findList(String... keys) { return find(JsonList.class, keys); } /** * Returns the first key in the map. * * @return The first key in the map, or null if the map is empty. */ public String getFirstKey() { return isEmpty() ? null : keySet().iterator().next(); } /** * Returns the class type of the object at the specified index. * * @param key The key into this map. * @return * The data type of the object at the specified key, or null if the value is null or does not exist. */ public ClassMeta getClassMeta(String key) { return bs().getClassMetaForObject(get(key)); } /** * Equivalent to calling get(class,key,def) followed by remove(key); * @param key The key. * @param defVal The default value if the map doesn't contain the specified mapping. * @param type The class type. * * @param The class type. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public T removeWithDefault(String key, T defVal, Class type) { T t = getWithDefault(key, defVal, type); remove(key); return t; } /** * Equivalent to calling removeWithDefault(key,null,String.class). * * @param key The key. * @return The converted value, or null if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public String removeString(String key) { return removeString(key, null); } /** * Equivalent to calling removeWithDefault(key,def,String.class). * * @param key The key. * @param def The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public String removeString(String key, String def) { return removeWithDefault(key, def, String.class); } /** * Equivalent to calling removeWithDefault(key,null,Integer.class). * * @param key The key. * @return The converted value, or null if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Integer removeInt(String key) { return removeInt(key, null); } /** * Equivalent to calling removeWithDefault(key,def,Integer.class). * * @param key The key. * @param def The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Integer removeInt(String key, Integer def) { return removeWithDefault(key, def, Integer.class); } /** * Equivalent to calling removeWithDefault(key,null,Boolean.class). * * @param key The key. * @return The converted value, or null if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Boolean removeBoolean(String key) { return removeBoolean(key, null); } /** * Equivalent to calling removeWithDefault(key,def,Boolean.class). * * @param key The key. * @param def The default value if the map doesn't contain the specified mapping. * @return The converted value, or the default value if the map contains no mapping for this key. * @throws InvalidDataConversionException If value cannot be converted. */ public Boolean removeBoolean(String key, Boolean def) { return removeWithDefault(key, def, Boolean.class); } /** * Convenience method for removing several keys at once. * * @param keys The list of keys to remove. */ public void removeAll(Collection keys) { keys.forEach(this::remove); } /** * Convenience method for removing several keys at once. * * @param keys The list of keys to remove. */ public void removeAll(String... keys) { for (String k : keys) remove(k); } /** * The opposite of {@link #removeAll(String...)}. * *

* Discards all keys from this map that aren't in the specified list. * * @param keys The keys to keep. * @return This map. */ public JsonMap keepAll(String...keys) { for (Iterator i = keySet().iterator(); i.hasNext();) { boolean remove = true; String key = i.next(); for (String k : keys) { if (k.equals(key)) { remove = false; break; } } if (remove) i.remove(); } return this; } /** * Returns true if the map contains the specified entry and the value is not null nor an empty string. * *

* Always returns false if the value is not a {@link CharSequence}. * * @param key The key. * @return true if the map contains the specified entry and the value is not null nor an empty string. */ public boolean containsKeyNotEmpty(String key) { Object val = get(key); if (val == null) return false; if (val instanceof CharSequence) return ! StringUtils.isEmpty((CharSequence)val); return false; } /** * Returns true if this map contains the specified key, ignoring the inner map if it exists. * * @param key The key to look up. * @return true if this map contains the specified key. */ public boolean containsOuterKey(Object key) { return super.containsKey(key); } /** * Returns a copy of this JsonMap with only the specified keys. * * @param keys The keys of the entries to copy. * @return A new map with just the keys and values from this map. */ public JsonMap include(String...keys) { JsonMap m2 = new JsonMap(); this.forEach((k,v) -> { for (String kk : keys) if (kk.equals(k)) m2.put(kk, v); }); return m2; } /** * Returns a copy of this JsonMap without the specified keys. * * @param keys The keys of the entries not to copy. * @return A new map without the keys and values from this map. */ public JsonMap exclude(String...keys) { JsonMap m2 = new JsonMap(); this.forEach((k,v) -> { boolean exclude = false; for (String kk : keys) if (kk.equals(k)) exclude = true; if (! exclude) m2.put(k, v); }); return m2; } /** * Converts this map into an object of the specified type. * *

* If this map contains a "_type" entry, it must be the same as or a subclass of the type. * * @param The class type to convert this map object to. * @param type The class type to convert this map object to. * @return The new object. * @throws ClassCastException * If the "_type" entry is present and not assignable from type */ @SuppressWarnings("unchecked") public T cast(Class type) { BeanSession bs = bs(); ClassMeta c2 = bs.getClassMeta(type); String typePropertyName = bs.getBeanTypePropertyName(c2); ClassMeta c1 = bs.getBeanRegistry().getClassMeta((String)get(typePropertyName)); ClassMeta c = c1 == null ? c2 : narrowClassMeta(c1, c2); if (c.isObject()) return (T)this; return (T)cast2(c); } /** * Same as {@link #cast(Class)}, except allows you to specify a {@link ClassMeta} parameter. * * @param The class type to convert this map object to. * @param cm The class type to convert this map object to. * @return The new object. * @throws ClassCastException * If the "_type" entry is present and not assignable from type */ @SuppressWarnings({"unchecked"}) public T cast(ClassMeta cm) { BeanSession bs = bs(); ClassMeta c1 = bs.getBeanRegistry().getClassMeta((String)get(bs.getBeanTypePropertyName(cm))); ClassMeta c = narrowClassMeta(c1, cm); return (T)cast2(c); } //------------------------------------------------------------------------------------------------------------------ // POJO REST methods. //------------------------------------------------------------------------------------------------------------------ /** * Same as {@link #get(String,Class) get(String,Class)}, but the key is a slash-delimited path used to traverse * entries in this POJO. * *

* For example, the following code is equivalent: *

*

* JsonMap map = JsonMap.ofJson("..."); * * // Long way * long _long = map.getMap("foo").getList("bar").getMap("0").getLong("baz"); * * // Using this method * long _long = map.getAt("foo/bar/0/baz", long.class); *

* *

* This method uses the {@link ObjectRest} class to perform the lookup, so the map can contain any of the various * class types that the {@link ObjectRest} class supports (e.g. beans, collections, arrays). * * @param path The path to the entry. * @param type The class type. * * @param The class type. * @return The value, or null if the entry doesn't exist. */ public T getAt(String path, Class type) { return getObjectRest().get(path, type); } /** * Same as {@link #getAt(String,Class)}, but allows for conversion to complex maps and collections. * *

* This method uses the {@link ObjectRest} class to perform the lookup, so the map can contain any of the various * class types that the {@link ObjectRest} class supports (e.g. beans, collections, arrays). * * @param path The path to the entry. * @param type The class type. * @param args The class parameter types. * * @param The class type. * @return The value, or null if the entry doesn't exist. */ public T getAt(String path, Type type, Type...args) { return getObjectRest().get(path, type, args); } /** * Same as put(String,Object), but the key is a slash-delimited path used to traverse entries in this * POJO. * *

* For example, the following code is equivalent: *

*

* JsonMap map = JsonMap.ofJson("..."); * * // Long way * map.getMap("foo").getList("bar").getMap("0").put("baz", 123); * * // Using this method * map.putAt("foo/bar/0/baz", 123); *

* *

* This method uses the {@link ObjectRest} class to perform the lookup, so the map can contain any of the various * class types that the {@link ObjectRest} class supports (e.g. beans, collections, arrays). * * @param path The path to the entry. * @param o The new value. * @return The previous value, or null if the entry doesn't exist. */ public Object putAt(String path, Object o) { return getObjectRest().put(path, o); } /** * Similar to {@link #putAt(String,Object) putAt(String,Object)}, but used to append to collections and arrays. * *

* For example, the following code is equivalent: *

*

* JsonMap map = JsonMap.ofJson("..."); * * // Long way * map.getMap("foo").getList("bar").append(123); * * // Using this method * map.postAt("foo/bar", 123); *

* *

* This method uses the {@link ObjectRest} class to perform the lookup, so the map can contain any of the various * class types that the {@link ObjectRest} class supports (e.g. beans, collections, arrays). * * @param path The path to the entry. * @param o The new value. * @return The previous value, or null if the entry doesn't exist. */ public Object postAt(String path, Object o) { return getObjectRest().post(path, o); } /** * Similar to {@link #remove(Object) remove(Object)}, but the key is a slash-delimited path used to traverse entries * in this POJO. * *

* For example, the following code is equivalent: *

*

* JsonMap map = JsonMap.ofJson("..."); * * // Long way * map.getMap("foo").getList("bar").getMap(0).remove("baz"); * * // Using this method * map.deleteAt("foo/bar/0/baz"); *

* *

* This method uses the {@link ObjectRest} class to perform the lookup, so the map can contain any of the various * class types that the {@link ObjectRest} class supports (e.g. beans, collections, arrays). * * @param path The path to the entry. * @return The previous value, or null if the entry doesn't exist. */ public Object deleteAt(String path) { return getObjectRest().delete(path); } //------------------------------------------------------------------------------------------------------------------ // Other methods //------------------------------------------------------------------------------------------------------------------ @Override public Object put(String key, Object value) { if (valueFilter.test(value)) super.put(key, value); return null; } /** * Returns the {@link BeanSession} currently associated with this map. * * @return The {@link BeanSession} currently associated with this map. */ public BeanSession getBeanSession() { return session; } /** * Sets the {@link BeanSession} currently associated with this map. * * @param value The {@link BeanSession} currently associated with this map. * @return This object. */ public JsonMap setBeanSession(BeanSession value) { this.session = value; return this; } /** * Convenience method for inserting JSON directly into an attribute on this object. * *

* The JSON text can be an object (i.e. "{...}") or an array (i.e. "[...]"). * * @param key The key. * @param json The JSON text that will be parsed into an Object and then inserted into this map. * @throws ParseException Malformed input encountered. */ public void putJson(String key, String json) throws ParseException { this.put(key, JsonParser.DEFAULT.parse(json, Object.class)); } /** * Serialize this object into a string using the specified serializer. * * @param serializer The serializer to use to convert this object to a string. * @return This object serialized as a string. */ public String asString(WriterSerializer serializer) { return serializer.toString(this); } /** * Serialize this object to Simplified JSON using {@link Json5Serializer#DEFAULT}. * * @return This object serialized as a string. */ public String asString() { if (Json5Serializer.DEFAULT == null) return stringify(this); return Json5Serializer.DEFAULT.toString(this); } /** * Serialize this object to Simplified JSON using {@link Json5Serializer#DEFAULT_READABLE}. * * @return This object serialized as a string. */ public String asReadableString() { if (Json5Serializer.DEFAULT_READABLE == null) return stringify(this); return Json5Serializer.DEFAULT_READABLE.toString(this); } /** * Convenience method for serializing this map to the specified Writer using the * {@link JsonSerializer#DEFAULT} serializer. * * @param w The writer to serialize this object to. * @return This object. * @throws IOException If a problem occurred trying to write to the writer. * @throws SerializeException If a problem occurred trying to convert the output. */ public JsonMap writeTo(Writer w) throws IOException, SerializeException { JsonSerializer.DEFAULT.serialize(this, w); return this; } /** * Returns true if this map is unmodifiable. * * @return true if this map is unmodifiable. */ public boolean isUnmodifiable() { return false; } /** * Returns a modifiable copy of this map if it's unmodifiable. * * @return A modifiable copy of this map if it's unmodifiable, or this map if it is already modifiable. */ public JsonMap modifiable() { if (isUnmodifiable()) return new JsonMap(this); return this; } /** * Returns an unmodifiable copy of this map if it's modifiable. * * @return An unmodifiable copy of this map if it's modifiable, or this map if it is already unmodifiable. */ public JsonMap unmodifiable() { if (this instanceof UnmodifiableJsonMap) return this; return new UnmodifiableJsonMap(this); } //------------------------------------------------------------------------------------------------------------------ // Utility methods //------------------------------------------------------------------------------------------------------------------ private BeanSession bs() { if (session == null) session = BeanContext.DEFAULT_SESSION; return session; } private ObjectRest getObjectRest() { if (objectRest == null) objectRest = new ObjectRest(this); return objectRest; } /* * Combines the class specified by a "_type" attribute with the ClassMeta * passed in through the cast(ClassMeta) method. * The rule is that child classes supersede parent classes, and c2 supersedes c1 * if one isn't the parent of another. */ private ClassMeta narrowClassMeta(ClassMeta c1, ClassMeta c2) { if (c1 == null) return c2; ClassMeta c = getNarrowedClassMeta(c1, c2); if (c1.isMap()) { ClassMeta k = getNarrowedClassMeta(c1.getKeyType(), c2.getKeyType()); ClassMeta v = getNarrowedClassMeta(c1.getValueType(), c2.getValueType()); return bs().getClassMeta(c.getInnerClass(), k, v); } if (c1.isCollection()) { ClassMeta e = getNarrowedClassMeta(c1.getElementType(), c2.getElementType()); return bs().getClassMeta(c.getInnerClass(), e); } return c; } /* * If c1 is a child of c2 or the same as c2, returns c1. * Otherwise, returns c2. */ private static ClassMeta getNarrowedClassMeta(ClassMeta c1, ClassMeta c2) { if (c2 == null || c2.getInfo().isParentOf(c1.getInnerClass())) return c1; return c2; } /* * Converts this map to the specified class type. */ @SuppressWarnings({"unchecked","rawtypes"}) private T cast2(ClassMeta cm) { BeanSession bs = bs(); try { Object value = get("value"); if (cm.isMap()) { Map m2 = (cm.canCreateNewInstance() ? (Map)cm.newInstance() : new JsonMap(bs)); ClassMeta kType = cm.getKeyType(), vType = cm.getValueType(); forEach((k,v) -> { if (! k.equals(bs.getBeanTypePropertyName(cm))) { // Attempt to recursively cast child maps. if (v instanceof JsonMap) v = ((JsonMap)v).cast(vType); Object k2 = (kType.isString() ? k : bs.convertToType(k, kType)); v = (vType.isObject() ? v : bs.convertToType(v, vType)); m2.put(k2, v); } }); return (T)m2; } else if (cm.isBean()) { BeanMap bm = bs.newBeanMap(cm.getInnerClass()); // Iterate through all the entries in the map and set the individual field values. forEach((k,v) -> { if (! k.equals(bs.getBeanTypePropertyName(cm))) { // Attempt to recursively cast child maps. if (v instanceof JsonMap) v = ((JsonMap)v).cast(bm.getProperty(k).getMeta().getClassMeta()); bm.put(k, v); } }); return bm.getBean(); } else if (cm.isCollectionOrArray()) { List items = (List)get("items"); return bs.convertToType(items, cm); } else if (value != null) { return bs.convertToType(value, cm); } } catch (Exception e) { throw new BeanRuntimeException(e, cm.getInnerClass(), "Error occurred attempting to cast to an object of type ''{0}''", cm.getInnerClass().getName()); } throw new BeanRuntimeException(cm.getInnerClass(), "Cannot convert to class type ''{0}''. Only beans and maps can be converted using this method.", cm.getInnerClass().getName()); } private void parse(Reader r, Parser p) throws ParseException { if (p == null) p = JsonParser.DEFAULT; p.parseIntoMap(r, this, bs().string(), bs().object()); } private static final class UnmodifiableJsonMap extends JsonMap { private static final long serialVersionUID = 1L; UnmodifiableJsonMap(JsonMap contents) { if (contents != null) contents.forEach(super::put); } @Override public Object put(String key, Object val) { throw new UnsupportedOperationException("Not supported on read-only object."); } @Override public Object remove(Object key) { throw new UnsupportedOperationException("Not supported on read-only object."); } @Override public boolean isUnmodifiable() { return true; } } //------------------------------------------------------------------------------------------------------------------ // Overridden methods. //------------------------------------------------------------------------------------------------------------------ @Override /* Map */ public Object get(Object key) { Object o = super.get(key); if (o == null && inner != null) o = inner.get(key); return o; } @Override /* Map */ public boolean containsKey(Object key) { if (super.containsKey(key)) return true; if (inner != null) return inner.containsKey(key); return false; } @Override /* Map */ public Set keySet() { if (inner == null) return super.keySet(); LinkedHashSet s = set(); s.addAll(inner.keySet()); s.addAll(super.keySet()); return s; } @Override /* Map */ public Set> entrySet() { if (inner == null) return super.entrySet(); final Set keySet = keySet(); final Iterator keys = keySet.iterator(); return new AbstractSet<>() { @Override /* Iterable */ public Iterator> iterator() { return new Iterator<>() { @Override /* Iterator */ public boolean hasNext() { return keys.hasNext(); } @Override /* Iterator */ public Map.Entry next() { return new Map.Entry<>() { String key = keys.next(); @Override /* Map.Entry */ public String getKey() { return key; } @Override /* Map.Entry */ public Object getValue() { return get(key); } @Override /* Map.Entry */ public Object setValue(Object object) { return put(key, object); } }; } @Override /* Iterator */ public void remove() { throw new UnsupportedOperationException("Not supported on read-only object."); } }; } @Override /* Set */ public int size() { return keySet.size(); } }; } /** * A synonym for {@link #toString()} * * @return This object as a JSON string. */ public String asJson() { return toString(); } @Override /* Object */ public String toString() { return Json5.of(this); } }