// ***************************************************************************************************************************
// * 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.
 *
 * <p>
 * An extension of {@link LinkedHashMap}, so all methods available in that class are also available to this class.
 *
 * <p>
 * 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.
 *
 * <h5 class='section'>Example:</h5>
 * <p class='bjava'>
 * 	<jc>// Construct an empty Map</jc>
 * 	JsonMap <jv>map</jv> = JsonMap.<jsm>of</jsm>();
 *
 * 	<jc>// Construct a Map from JSON</jc>
 * 	<jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"{a:'A',b:{c:'C',d:123}}"</js>);
 *
 * 	<jc>// Construct a Map using the append method</jc>
 * 	<jv>map</jv> = JsonMap.<jsm>of</jsm>().a(<js>"foo"</js>,<js>"x"</js>).a(<js>"bar"</js>,123).a(<js>"baz"</js>,<jk>true</jk>);
 *
 * 	<jc>// Construct a Map from XML generated by XmlSerializer</jc>
 * 	String <jv>xml</jv> = <js>"&lt;object&gt;&lt;a type='string'&gt;A&lt;/a&gt;&lt;b type='object'&gt;&lt;c type='string'&gt;C&lt;/c&gt;&lt;d type='number'&gt;123&lt;/d&gt;&lt;/b&gt;&lt;/object&gt;"</js>;
 * 	<jv>map</jv> = JsonMap.<jsm>of</jsm>(<jv>xml</jv>, XmlParser.<jsf>DEFAULT</jsf>);
 *
 * 	<jc>// Construct a Map from a URL GET parameter string generated by UrlEncodingParser</jc>
 * 	String <jv>urlParams</jv> = <js>"?a='A'&amp;b={c:'C',d:123}"</js>;
 * 	<jv>map</jv> = JsonMap.<jsm>of</jsm>(<jv>urlParams</jv>, UrlEncodingParser.<jsf>DEFAULT</jsf>);
 *
 * 	<jc>// Construct JSON from JsonMap</jc>
 * 	<jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"{foo:'bar'},{baz:[123,true]}"</js>);
 * 	String <jv>json</jv> = <jv>map</jv>.toString();  <jc>// Produces "{foo:'bar'},{baz:[123,true]}"</jc>
 * 	<jv>json</jv> = <jv>map</jv>.toString(JsonSerializer.<jsf>DEFAULT</jsf>);  <jc>// Equivalent</jc>
 * 	<jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>map</jv>);  <jc>// Equivalent</jc>
 *
 * 	<jc>// Get a map entry as an Integer</jc>
 * 	<jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"{foo:123}"</js>);
 * 	Integer <jv>integer</jv> = <jv>map</jv>.getInt(<js>"foo"</js>);
 * 	<jv>integer</jv> = <jv>map</jv>.get(Integer.<jk>class</jk>, <js>"foo"</js>);  <jc>// Equivalent</jc>
 *
 * 	<jc>// Get a map entry as a Float</jc>
 * 	<jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"{foo:123}"</js>);
 * 	Float <jv>_float</jv> = <jv>map</jv>.getFloat(<js>"foo"</js>);
 * 	<jv>_float</jv> = <jv>map</jv>.get(Float.<jk>class</jk>, <js>"foo"</js>);  <jc>// Equivalent</jc>
 *
 * 	<jc>// Same as above, except converted to a String</jc>
 * 	<jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"{foo:123}"</js>);
 * 	String <jv>string</jv> = <jv>map</jv>.getString(<js>"foo"</js>); <jc>// Returns "123"</jc>
 * 	<jv>string</jv> = <jv>map</jv>.get(String.<jk>class</jk>, <js>"foo"</js>);  <jc>// Equivalent</jc>
 *
 * 	<jc>// Get one of the entries in the list as a bean (converted to a bean if it isn't already one)</jc>
 * 	<jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"{person:{name:'John Smith',age:45}}"</js>);
 * 	Person <jv>person</jv> = <jv>map</jv>.get(Person.<jk>class</jk>, <js>"person"</js>);
 *
 * 	<jc>// Add an inner map</jc>
 * 	JsonMap <jv>map1</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"{a:1}"</js>);
 * 	JsonMap <jv>map2</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"{b:2}"</js>).setInner(<jv>map1</jv>);
 * 	<jk>int</jk> <jv>_int</jv> = <jv>map2</jv>.getInt(<js>"a"</js>);  <jc>// a == 1 </jc>
 * </p>
 *
 * <h5 class='section'>Notes:</h5><ul>
 * 	<li class='warn'>This class is not thread safe.
 * </ul>
 */
public class JsonMap extends LinkedHashMap<String,Object> {

	//------------------------------------------------------------------------------------------------------------------
	// 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<Map.Entry<String,Object>> entrySet() {
			return Collections.<String,Object>emptyMap().entrySet();
		}

		@Override /* Map */
		public Set<String> keySet() {
			return Collections.<String,Object>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<Object> 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.
	 * 	<br>Can be <jk>null</jk>.
	 * 	<br>Keys will be converted to strings using {@link Object#toString()}.
	 * @return A new map or <jk>null</jk> if the map was <jk>null</jk>.
	 */
	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.
	 * 	<br>Can be normal or simplified JSON.
	 * @return A new map or <jk>null</jk> 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.
	 * 	<br>Can be <jk>null</jk>.
	 * @param p
	 * 	The parser to use to parse the input.
	 * 	<br>If <jk>null</jk>, uses {@link JsonParser}.
	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
	 * @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.
	 * 	<br>Can contain normal or simplified JSON.
	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
	 * @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.
	 * 	<br>Can contain normal or simplified JSON.
	 * @param p
	 * 	The parser to use to parse the input.
	 * 	<br>If <jk>null</jk>, uses {@link JsonParser}.
	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
	 * @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.
	 *
	 * <h5 class='section'>Examples:</h5>
	 * <p class='bjava'>
	 * 	JsonMap <jv>map</jv> = <jk>new</jk> JsonMap(<js>"key1"</js>,<js>"val1"</js>,<js>"key2"</js>,<js>"val2"</js>);
	 * </p>
	 *
	 * @param keyValuePairs A list of key/value pairs to add to this map.
	 * @return A new map, never <jk>null</jk>.
	 */
	public static JsonMap of(Object... keyValuePairs) {
		return new JsonMap(keyValuePairs);
	}

	/**
	 * Construct a map initialized with the specified key/value pairs.
	 *
	 * <p>
	 * Same as {@link #of(Object...)} but calls {@link #filtered()} on the created map.
	 *
	 * <h5 class='section'>Examples:</h5>
	 * <p class='bjava'>
	 * 	JsonMap <jv>map</jv> = <jk>new</jk> JsonMap(<js>"key1"</js>,<js>"val1"</js>,<js>"key2"</js>,<js>"val2"</js>);
	 * </p>
	 *
	 * @param keyValuePairs A list of key/value pairs to add to this map.
	 * @return A new map, never <jk>null</jk>.
	 */
	public static JsonMap filteredMap(Object... keyValuePairs) {
		return new JsonMap(keyValuePairs).filtered();
	}

	//------------------------------------------------------------------------------------------------------------------
	// Instance
	//------------------------------------------------------------------------------------------------------------------

	private transient BeanSession session;
	private Map<String,Object> inner;
	private transient ObjectRest objectRest;
	private transient Predicate<Object> 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.
	 * 	<br>Can be <jk>null</jk>.
	 * 	<br>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.
	 * 	<br>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.
	 * 	<br>Can be <jk>null</jk>.
	 * @param p
	 * 	The parser to use to parse the input.
	 * 	<br>If <jk>null</jk>, 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.
	 * 	<br>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.
	 * 	<br>Can contain normal or simplified JSON.
	 * @param p
	 * 	The parser to use to parse the input.
	 * 	<br>If <jk>null</jk>, 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.
	 *
	 * <h5 class='section'>Examples:</h5>
	 * <p class='bjava'>
	 * 	JsonMap <jv>map</jv> = <jk>new</jk> JsonMap(<js>"key1"</js>,<js>"val1"</js>,<js>"key2"</js>,<js>"val2"</js>);
	 * </p>
	 *
	 * @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.
	 *
	 * <p>
	 * If {@link #get(Object)} returns <jk>null</jk>, then {@link #get(Object)} will be called on the inner map.
	 *
	 * <p>
	 * 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.
	 *
	 * <p class='bjava'>
	 * 	JsonMap <jv>map1</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"{foo:1}"</js>);
	 * 	JsonMap <jv>map2</jv> = JsonMap.<jsm>of</jsm>().setInner(<jv>map1</jv>);
	 * 	<jv>map2</jv>.put(<js>"foo"</js>, 2);                      <jc>// Overwrite the entry</jc>
	 * 	<jk>int</jk> <jv>foo1</jv> = <jv>map1</jv>.getInt(<js>"foo"</js>);           <jc>// foo1 == 1 </jc>
	 * 	<jk>int</jk> <jv>foo2</jv> = <jv>map2</jv>.getInt(<js>"foo"</js>);           <jc>// foo2 == 2 </jc>
	 * </p>
	 *
	 * @param inner
	 * 	The inner map.
	 * 	Can be <jk>null</jk> to remove the inner map from an existing map.
	 * @return This object.
	 */
	public JsonMap inner(Map<String,Object> inner) {
		this.inner = inner;
		return this;
	}

	/**
	 * Override the default bean session used for converting POJOs.
	 *
	 * <p>
	 * Default is {@link BeanContext#DEFAULT}, which is sufficient in most cases.
	 *
	 * <p>
	 * 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 <jk>null</jk>.
	 * @return This object.
	 */
	public JsonMap append(Map<String,Object> values) {
		if (values != null)
			super.putAll(values);
		return this;
	}

	/**
	 * Add if flag is <jk>true</jk>.
	 *
	 * @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 <T> The value type.
	 * @param test The predicate to match against.
	 * @param key The key.
	 * @param value The value.
	 * @return This object.
	 */
	public <T> JsonMap appendIf(Predicate<T> test, String key, T value) {
		return appendIf(test(test, value), key, value);
	}

	/**
	 * Adds the first value that matches the specified predicate.
	 *
	 * @param <T> 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 <T> JsonMap appendFirst(Predicate<T> 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 <jk>null</jk>.
	 *
	 * @param key The map key.
	 * @param value The value to set if the current value does not exist or is <jk>null</jk>.
	 * @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 <jk>null</jk> and the value matches the specified predicate.
	 *
	 * @param <T> 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 <jk>null</jk>.
	 * @return This object.
	 */
	public <T> JsonMap appendIfAbsentIf(Predicate<T> 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.
	 *
	 * <p>
	 * Any of the following types will be ignored when set as values in this map:
	 * <ul>
	 * 	<li><jk>null</jk>
	 * 	<li><jk>false</jk>
	 * 	<li><c>-1</c> (any Number type)
	 * 	<li>Empty arrays/collections/maps.
	 * </ul>
	 * @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.
	 *
	 * <p>
	 * If the predicate evaluates to <jk>false</jk> on values added to this map, the entry will be skipped.
	 *
	 * @param value The value tester predicate.
	 * @return This object.
	 */
	public JsonMap filtered(Predicate<Object> value) {
		valueFilter = value;
		return this;
	}

	//------------------------------------------------------------------------------------------------------------------
	// Retrievers
	//------------------------------------------------------------------------------------------------------------------

	/**
	 * Same as {@link Map#get(Object) get()}, but casts or converts the value to the specified class type.
	 *
	 * <p>
	 * This is the preferred get method for simple types.
	 *
	 * <h5 class='section'>Examples:</h5>
	 * <p class='bjava'>
	 * 	JsonMap <jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"..."</js>);
	 *
	 * 	<jc>// Value converted to a string.</jc>
	 * 	String <jv>string</jv> = <jv>map</jv>.get(<js>"key1"</js>, String.<jk>class</jk>);
	 *
	 * 	<jc>// Value converted to a bean.</jc>
	 * 	MyBean <jv>bean</jv> = <jv>map</jv>.get(<js>"key2"</js>, MyBean.<jk>class</jk>);
	 *
	 * 	<jc>// Value converted to a bean array.</jc>
	 * 	MyBean[] <jv>beanArray</jv> = <jv>map</jv>.get(<js>"key3"</js>, MyBean[].<jk>class</jk>);
	 *
	 * 	<jc>// Value converted to a linked-list of objects.</jc>
	 * 	List <jv>list</jv> = <jv>map</jv>.get(<js>"key4"</js>, LinkedList.<jk>class</jk>);
	 *
	 * 	<jc>// Value converted to a map of object keys/values.</jc>
	 * 	Map <jv>map2</jv> = <jv>map</jv>.get(<js>"key5"</js>, TreeMap.<jk>class</jk>);
	 * </p>
	 *
	 * <p>
	 * See {@link BeanSession#convertToType(Object, ClassMeta)} for the list of valid data conversions.
	 *
	 * @param key The key.
	 * @param <T> The class type returned.
	 * @param type The class type returned.
	 * @return The value, or <jk>null</jk> if the entry doesn't exist.
	 */
	public <T> T get(String key, Class<T> type) {
		return getWithDefault(key, (T)null, type);
	}

	/**
	 * Same as {@link #get(String,Class)}, but allows for complex data types consisting of collections or maps.
	 *
	 * <p>
	 * The type can be a simple type (e.g. beans, strings, numbers) or parameterized type (collections/maps).
	 *
	 * <h5 class='section'>Examples:</h5>
	 * <p class='bjava'>
	 * 	JsonMap <jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"..."</js>);
	 *
	 * 	<jc>// Value converted to a linked-list of strings.</jc>
	 * 	List&lt;String&gt; <jv>list1</jv> = <jv>map</jv>.get(<js>"key1"</js>, LinkedList.<jk>class</jk>, String.<jk>class</jk>);
	 *
	 * 	<jc>// Value converted to a linked-list of beans.</jc>
	 * 	List&lt;MyBean&gt; <jv>list2</jv> = <jv>map</jv>.get(<js>"key2"</js>, LinkedList.<jk>class</jk>, MyBean.<jk>class</jk>);
	 *
	 * 	<jc>// Value converted to a linked-list of linked-lists of strings.</jc>
	 * 	List&lt;List&lt;String&gt;&gt; <jv>list3</jv> = <jv>map</jv>.get(<js>"key3"</js>, LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>);
	 *
	 * 	<jc>// Value converted to a map of string keys/values.</jc>
	 * 	Map&lt;String,String&gt; <jv>map1</jv> = <jv>map</jv>.get(<js>"key4"</js>, TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>);
	 *
	 * 	<jc>// Value converted to a map containing string keys and values of lists containing beans.</jc>
	 * 	Map&lt;String,List&lt;MyBean&gt;&gt; <jv>map2</jv> = <jv>map</jv>.get(<js>"key5"</js>, TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
	 * </p>
	 *
	 * <p>
	 * <c>Collection</c> classes are assumed to be followed by zero or one objects indicating the element type.
	 *
	 * <p>
	 * <c>Map</c> classes are assumed to be followed by zero or two meta objects indicating the key and value types.
	 *
	 * <p>
	 * The array can be arbitrarily long to indicate arbitrarily complex data structures.
	 *
	 * <p>
	 * See {@link BeanSession#convertToType(Object, ClassMeta)} for the list of valid data conversions.
	 *
	 * <h5 class='section'>Notes:</h5><ul>
	 * 	<li class='note'>
	 * 		Use the {@link #get(String, Class)} method instead if you don't need a parameterized map/collection.
	 * </ul>
	 *
	 * @param key The key.
	 * @param <T> The class type returned.
	 * @param type The class type returned.
	 * @param args The class type parameters.
	 * @return The value, or <jk>null</jk> if the entry doesn't exist.
	 */
	public <T> 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 <jk>null</jk>.
	 * @param <T> The class type returned.
	 * @param type The class type returned.
	 * @return The value, or <jk>null</jk> if the entry doesn't exist.
	 */
	public <T> T getWithDefault(String key, T def, Class<T> 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 <jk>null</jk>.
	 * @param <T> The class type returned.
	 * @param type The class type returned.
	 * @param args The class type parameters.
	 * @return The value, or <jk>null</jk> if the entry doesn't exist.
	 */
	public <T> 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 <jk>null</jk> 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 <T> The transformed class type.
	 * @return The value, or <jk>null</jk> if the entry doesn't exist.
	 * @throws ParseException Malformed input encountered.
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public <T> T getSwapped(String key, ObjectSwap<T,?> 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 <jk>null</jk> 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.
	 *
	 * <p>
	 * Casts or converts the value to the specified class type.
	 *
	 * <p>
	 * See {@link BeanSession#convertToType(Object, ClassMeta)} for the list of valid data conversions.
	 *
	 * @param type The class type to convert the value to.
	 * @param <T> 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 <jk>null</jk> if none of the keys exist in this map.
	 */
	public <T> T find(Class<T> 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}.
	 *
	 * <p>
	 * Shortcut for <code>get(key, String.<jk>class</jk>)</code>.
	 *
	 * @param key The key.
	 * @return The converted value, or <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>get(key, String[].<jk>class</jk>)</code>.
	 *
	 * @param key The key.
	 * @return The converted value, or <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>getWithDefault(key, defVal, String.<jk>class</jk>)</code>.
	 *
	 * @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}.
	 *
	 * <p>
	 * Shortcut for <code>get(key, Integer.<jk>class</jk>)</code>.
	 *
	 * @param key The key.
	 * @return The converted value, or <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>getWithDefault(key, defVal, Integer.<jk>class</jk>)</code>.
	 *
	 * @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}.
	 *
	 * <p>
	 * Shortcut for <code>get(key, Long.<jk>class</jk>)</code>.
	 *
	 * @param key The key.
	 * @return The converted value, or <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>getWithDefault(key, defVal, Long.<jk>class</jk>)</code>.
	 *
	 * @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}.
	 *
	 * <p>
	 * Shortcut for <code>get(key, Boolean.<jk>class</jk>)</code>.
	 *
	 * @param key The key.
	 * @return The converted value, or <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>getWithDefault(key, defVal, Boolean.<jk>class</jk>)</code>.
	 *
	 * @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}.
	 *
	 * <p>
	 * Shortcut for <code>get(key, JsonMap.<jk>class</jk>)</code>.
	 *
	 * @param key The key.
	 * @return The converted value, or <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>getWithDefault(key, defVal, JsonMap.<jk>class</jk>)</code>.
	 *
	 * @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 <K> The key type.
	 * @param <V> 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 <K,V> Map<K,V> getMap(String key, Class<K> keyType, Class<V> valType, Map<K,V> 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}.
	 *
	 * <p>
	 * Shortcut for <code>get(key, JsonList.<jk>class</jk>)</code>.
	 *
	 * @param key The key.
	 * @return The converted value, or <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>getWithDefault(key, defVal, JsonList.<jk>class</jk>)</code>.
	 *
	 * @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 <E> 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 <E> List<E> getList(String key, Class<E> elementType, List<E> 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}.
	 *
	 * <p>
	 * Shortcut for <code>find(String.<jk>class</jk>, keys)</code>.
	 *
	 * @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 <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>find(Integer.<jk>class</jk>, keys)</code>.
	 *
	 * @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 <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>find(Long.<jk>class</jk>, keys)</code>.
	 *
	 * @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 <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>find(Boolean.<jk>class</jk>, keys)</code>.
	 *
	 * @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 <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>find(JsonMap.<jk>class</jk>, keys)</code>.
	 *
	 * @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 <jk>null</jk> 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}.
	 *
	 * <p>
	 * Shortcut for <code>find(JsonList.<jk>class</jk>, keys)</code>.
	 *
	 * @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 <jk>null</jk> 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 <jk>null</jk> 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 <jk>null</jk> if the value is null or does not exist.
	 */
	public ClassMeta<?> getClassMeta(String key) {
		return bs().getClassMetaForObject(get(key));
	}

	/**
	 * Equivalent to calling <c>get(class,key,def)</c> followed by <c>remove(key);</c>
	 * @param key The key.
	 * @param defVal The default value if the map doesn't contain the specified mapping.
	 * @param type The class type.
	 *
	 * @param <T> 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> T removeWithDefault(String key, T defVal, Class<T> type) {
		T t = getWithDefault(key, defVal, type);
		remove(key);
		return t;
	}

	/**
	 * Equivalent to calling <code>removeWithDefault(key,<jk>null</jk>,String.<jk>class</jk>)</code>.
	 *
	 * @param key The key.
	 * @return The converted value, or <jk>null</jk> 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 <code>removeWithDefault(key,def,String.<jk>class</jk>)</code>.
	 *
	 * @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 <code>removeWithDefault(key,<jk>null</jk>,Integer.<jk>class</jk>)</code>.
	 *
	 * @param key The key.
	 * @return The converted value, or <jk>null</jk> 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 <code>removeWithDefault(key,def,Integer.<jk>class</jk>)</code>.
	 *
	 * @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 <code>removeWithDefault(key,<jk>null</jk>,Boolean.<jk>class</jk>)</code>.
	 *
	 * @param key The key.
	 * @return The converted value, or <jk>null</jk> 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 <code>removeWithDefault(key,def,Boolean.<jk>class</jk>)</code>.
	 *
	 * @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<String> 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...)}.
	 *
	 * <p>
	 * 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<String> 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 <jk>true</jk> if the map contains the specified entry and the value is not null nor an empty string.
	 *
	 * <p>
	 * Always returns <jk>false</jk> if the value is not a {@link CharSequence}.
	 *
	 * @param key The key.
	 * @return <jk>true</jk> 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 <jk>true</jk> if this map contains the specified key, ignoring the inner map if it exists.
	 *
	 * @param key The key to look up.
	 * @return <jk>true</jk> if this map contains the specified key.
	 */
	public boolean containsOuterKey(Object key) {
		return super.containsKey(key);
	}

	/**
	 * Returns a copy of this <c>JsonMap</c> 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 <c>JsonMap</c> 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.
	 *
	 * <p>
	 * If this map contains a <js>"_type"</js> entry, it must be the same as or a subclass of the <c>type</c>.
	 *
	 * @param <T> 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 <js>"_type"</js> entry is present and not assignable from <c>type</c>
	 */
	@SuppressWarnings("unchecked")
	public <T> T cast(Class<T> 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 <T> 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 <js>"_type"</js> entry is present and not assignable from <c>type</c>
	 */
	@SuppressWarnings({"unchecked"})
	public <T> T cast(ClassMeta<T> 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.
	 *
	 * <p>
	 * For example, the following code is equivalent:
	 * </p>
	 * <p class='bjava'>
	 * 	JsonMap <jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"..."</js>);
	 *
	 * 	<jc>// Long way</jc>
	 * 	<jk>long</jk> <jv>_long</jv> = <jv>map</jv>.getMap(<js>"foo"</js>).getList(<js>"bar"</js>).getMap(<js>"0"</js>).getLong(<js>"baz"</js>);
	 *
	 * 	<jc>// Using this method</jc>
	 * 	<jk>long</jk> <jv>_long</jv> = <jv>map</jv>.getAt(<js>"foo/bar/0/baz"</js>, <jk>long</jk>.<jk>class</jk>);
	 * </p>
	 *
	 * <p>
	 * 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 <T> The class type.
	 * @return The value, or <jk>null</jk> if the entry doesn't exist.
	 */
	public <T> T getAt(String path, Class<T> type) {
		return getObjectRest().get(path, type);
	}

	/**
	 * Same as {@link #getAt(String,Class)}, but allows for conversion to complex maps and collections.
	 *
	 * <p>
	 * 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 <T> The class type.
	 * @return The value, or <jk>null</jk> if the entry doesn't exist.
	 */
	public <T> T getAt(String path, Type type, Type...args) {
		return getObjectRest().get(path, type, args);
	}

	/**
	 * Same as <c>put(String,Object)</c>, but the key is a slash-delimited path used to traverse entries in this
	 * POJO.
	 *
	 * <p>
	 * For example, the following code is equivalent:
	 * </p>
	 * <p class='bjava'>
	 * 	JsonMap <jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"..."</js>);
	 *
	 * 	<jc>// Long way</jc>
	 * 	<jv>map</jv>.getMap(<js>"foo"</js>).getList(<js>"bar"</js>).getMap(<js>"0"</js>).put(<js>"baz"</js>, 123);
	 *
	 * 	<jc>// Using this method</jc>
	 * 	<jv>map</jv>.putAt(<js>"foo/bar/0/baz"</js>, 123);
	 * </p>
	 *
	 * <p>
	 * 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 <jk>null</jk> 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.
	 *
	 * <p>
	 * For example, the following code is equivalent:
	 * </p>
	 * <p class='bjava'>
	 * 	JsonMap <jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"..."</js>);
	 *
	 * 	<jc>// Long way</jc>
	 * 	<jv>map</jv>.getMap(<js>"foo"</js>).getList(<js>"bar"</js>).append(123);
	 *
	 * 	<jc>// Using this method</jc>
	 * 	<jv>map</jv>.postAt(<js>"foo/bar"</js>, 123);
	 * </p>
	 *
	 * <p>
	 * 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 <jk>null</jk> 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.
	 *
	 * <p>
	 * For example, the following code is equivalent:
	 * </p>
	 * <p class='bjava'>
	 * 	JsonMap <jv>map</jv> = JsonMap.<jsm>ofJson</jsm>(<js>"..."</js>);
	 *
	 * 	<jc>// Long way</jc>
	 * 	<jv>map</jv>.getMap(<js>"foo"</js>).getList(<js>"bar"</js>).getMap(0).remove(<js>"baz"</js>);
	 *
	 * 	<jc>// Using this method</jc>
	 * 	<jv>map</jv>.deleteAt(<js>"foo/bar/0/baz"</js>);
	 * </p>
	 *
	 * <p>
	 * 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 <jk>null</jk> 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.
	 *
	 * <p>
	 * The JSON text can be an object (i.e. <js>"{...}"</js>) or an array (i.e. <js>"[...]"</js>).
	 *
	 * @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 <c>Writer</c> 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 <jk>true</jk> if this map is unmodifiable.
	 *
	 * @return <jk>true</jk> 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> T cast2(ClassMeta<T> 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<? extends T> 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;

		@SuppressWarnings("synthetic-access")
		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<String> keySet() {
		if (inner == null)
			return super.keySet();
		LinkedHashSet<String> s = set();
		s.addAll(inner.keySet());
		s.addAll(super.keySet());
		return s;
	}

	@Override /* Map */
	public Set<Map.Entry<String,Object>> entrySet() {
		if (inner == null)
			return super.entrySet();

		final Set<String> keySet = keySet();
		final Iterator<String> keys = keySet.iterator();

		return new AbstractSet<>() {

			@Override /* Iterable */
			public Iterator<Map.Entry<String,Object>> iterator() {

				return new Iterator<>() {

					@Override /* Iterator */
					public boolean hasNext() {
						return keys.hasNext();
					}

					@Override /* Iterator */
					public Map.Entry<String,Object> 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);
	}
}
