/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2006-2024 the original author or authors.
 *
 * Licensed 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
 *
 *     https://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.kordamp.json;

import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.DynaProperty;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections4.map.ListOrderedMap;
import org.apache.commons.lang3.StringUtils;
import org.kordamp.ezmorph.Morpher;
import org.kordamp.ezmorph.array.ObjectArrayMorpher;
import org.kordamp.ezmorph.bean.BeanMorpher;
import org.kordamp.ezmorph.object.IdentityObjectMorpher;
import org.kordamp.json.processors.JsonBeanProcessor;
import org.kordamp.json.processors.JsonValueProcessor;
import org.kordamp.json.processors.JsonVerifier;
import org.kordamp.json.processors.PropertyNameProcessor;
import org.kordamp.json.regexp.RegexpUtils;
import org.kordamp.json.util.CycleDetectionStrategy;
import org.kordamp.json.util.EnumMorpher;
import org.kordamp.json.util.JSONTokener;
import org.kordamp.json.util.JSONUtils;
import org.kordamp.json.util.PropertyFilter;
import org.kordamp.json.util.PropertySetStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A JSONObject is an unordered collection of name/value pairs. Its external
 * form is a string wrapped in curly braces with colons between the names and
 * values, and commas between the values and names. The internal form is an
 * object having <code>get</code> and <code>opt</code> methods for accessing
 * the values by name, and <code>put</code> methods for adding or replacing
 * values by name. The values can be any of these types: <code>Boolean</code>,
 * <code>JSONArray</code>, <code>JSONObject</code>, <code>Number</code>,
 * <code>String</code>, or the <code>JSONNull</code> object. A JSONObject
 * constructor can be used to convert an external form JSON text into an
 * internal form whose values can be retrieved with the <code>get</code> and
 * <code>opt</code> methods, or to convert values into a JSON text using the
 * <code>element</code> and <code>toString</code> methods. A
 * <code>get</code> method returns a value if one can be found, and throws an
 * exception if one cannot be found. An <code>opt</code> method returns a
 * default value instead of throwing an exception, and so is useful for
 * obtaining optional values.
 * <p/>
 * The generic <code>get()</code> and <code>opt()</code> methods return an
 * object, which you can cast or query for type. There are also typed
 * <code>get</code> and <code>opt</code> methods that do type checking and
 * type coercion for you.
 * <p/>
 * The <code>put</code> methods adds values to an object. For example,
 * <p/>
 * <pre>
 *     myString = new JSONObject().put("JSON", "Hello, World!").toString();</pre>
 * <p/>
 * produces the string <code>{"JSON": "Hello, World"}</code>.
 * <p/>
 * The texts produced by the <code>toString</code> methods strictly conform to
 * the JSON syntax rules. The constructors are more forgiving in the texts they
 * will accept:
 * <ul>
 * <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just
 * before the closing brace.</li>
 * <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single quote)</small>.</li>
 * <li>Strings do not need to be quoted at all if they do not begin with a
 * quote or single quote, and if they do not contain leading or trailing spaces,
 * and if they do not contain any of these characters:
 * <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers and
 * if they are not the reserved words <code>true</code>, <code>false</code>,
 * or <code>null</code>.</li>
 * <li>Keys can be followed by <code>=</code> or <code>=></code> as well as
 * by <code>:</code>.</li>
 * <li>Values can be followed by <code>;</code> <small>(semicolon)</small>
 * as well as by <code>,</code> <small>(comma)</small>.</li>
 * <li>Numbers may have the <code>0-</code> <small>(octal)</small> or
 * <code>0x-</code> <small>(hex)</small> prefix.</li>
 * <li>Comments written in the slashshlash, slashstar, and hash conventions
 * will be ignored.</li>
 * </ul>
 *
 * @author JSON.org
 */
public final class JSONObject extends AbstractJSON implements JSON, Map<String, Object>, Comparable {
    private static final long serialVersionUID = 1997944501504436316L;

    private static final Logger LOG = LoggerFactory.getLogger(JSONObject.class);
    /**
     * identifies this object as null
     */
    private boolean nullObject;
    /**
     * The Map where the JSONObject's properties are kept.
     */
    private Map properties;

    /**
     * Construct an empty JSONObject.
     */
    public JSONObject() {
        this.properties = new ListOrderedMap();
    }

    /**
     * Creates a JSONObject that is null.
     */
    public JSONObject(boolean isNull) {
        this();
        this.nullObject = isNull;
    }

    /**
     * Creates a JSONObject.<br>
     * Inspects the object type to call the correct JSONObject factory method.
     * Accepts JSON formatted strings, Maps, DynaBeans and JavaBeans.
     *
     * @param object
     *
     * @throws JSONException if the object can not be converted to a proper
     *                       JSONObject.
     */

    public static JSONObject fromObject(Object object) {
        return fromObject(object, new JsonConfig());
    }

    /**
     * Creates a JSONObject.<br>
     * Inspects the object type to call the correct JSONObject factory method.
     * Accepts JSON formatted strings, Maps, DynaBeans and JavaBeans.
     *
     * @param object
     *
     * @throws JSONException if the object can not be converted to a proper
     *                       JSONObject.
     */
    public static JSONObject fromObject(Object object, JsonConfig jsonConfig) {
        if (object == null || JSONUtils.isNull(object)) {
            return new JSONObject(true);
        } else if (object instanceof Enum) {
            throw new JSONException("'object' is an Enum. Use JSONArray instead");
        } else if (object instanceof Annotation || (object.getClass().isAnnotation())) {
            throw new JSONException("'object' is an Annotation.");
        } else if (object instanceof JSONObject) {
            return Impl.fromJSONObject((JSONObject) object, jsonConfig);
        } else if (object instanceof DynaBean) {
            return Impl.fromDynaBean((DynaBean) object, jsonConfig);
        } else if (object instanceof JSONTokener) {
            return Impl.fromJSONTokener((JSONTokener) object, jsonConfig);
        } else if (object instanceof JSONString) {
            return Impl.fromJSONString((JSONString) object, jsonConfig);
        } else if (object instanceof Map) {
            return Impl.fromMap((Map) object, jsonConfig);
        } else if (object instanceof String) {
            return Impl.fromString((String) object, jsonConfig);
        } else if (JSONUtils.isNumber(object) || JSONUtils.isBoolean(object)
            || JSONUtils.isString(object)) {
            return new JSONObject();
        } else if (JSONUtils.isArray(object)) {
            throw new JSONException("'object' is an array. Use JSONArray instead");
        } else {
            return Impl.fromBean(object, jsonConfig);
        }
    }

    public static Object toBean(JSONObject jsonObject) {
        if (jsonObject == null || jsonObject.isNullObject()) {
            return null;
        }

        DynaBean dynaBean = null;

        JsonConfig jsonConfig = new JsonConfig();
        Map props = JSONUtils.getProperties(jsonObject);
        dynaBean = JSONUtils.newDynaBean(jsonObject, jsonConfig);
        for (Iterator entries = jsonObject.names(jsonConfig)
            .iterator(); entries.hasNext(); ) {
            String name = (String) entries.next();
            String key = JSONUtils.convertToJavaIdentifier(name, jsonConfig);
            Class type = (Class) props.get(name);
            Object value = jsonObject.get(name);
            try {
                if (!JSONUtils.isNull(value)) {
                    if (value instanceof JSONArray) {
                        dynaBean.set(key, JSONArray.toCollection((JSONArray) value));
                    } else if (String.class.isAssignableFrom(type)
                        || Boolean.class.isAssignableFrom(type) || JSONUtils.isNumber(type)
                        || Character.class.isAssignableFrom(type)
                        || JSONFunction.class.isAssignableFrom(type)) {
                        dynaBean.set(key, value);
                    } else {
                        dynaBean.set(key, toBean((JSONObject) value));
                    }
                } else {
                    if (type.isPrimitive()) {
                        // assume assigned default value
                        LOG.warn("Tried to assign null value to " + key + ":" + type.getName());
                        dynaBean.set(key, JSONUtils.getMorpherRegistry()
                            .morph(type, null));
                    } else {
                        dynaBean.set(key, null);
                    }
                }
            } catch (JSONException jsone) {
                throw jsone;
            } catch (Exception e) {
                throw new JSONException("Error while setting property=" + name + " type" + type, e);
            }
        }

        return dynaBean;
    }

    /**
     * Creates a bean from a JSONObject, with a specific target class.<br>
     */
    public static Object toBean(JSONObject jsonObject, Class beanClass) {
        JsonConfig jsonConfig = new JsonConfig();
        jsonConfig.setRootClass(beanClass);
        return toBean(jsonObject, jsonConfig);
    }

    /**
     * Creates a bean from a JSONObject, with a specific target class.<br>
     * If beanClass is null, this method will return a graph of DynaBeans. Any
     * attribute that is a JSONObject and matches a key in the classMap will be
     * converted to that target class.<br>
     * The classMap has the following conventions:
     * <ul>
     * <li>Every key must be an String.</li>
     * <li>Every value must be a Class.</li>
     * <li>A key may be a regular expression.</li>
     * </ul>
     */
    public static Object toBean(JSONObject jsonObject, Class beanClass, Map classMap) {
        JsonConfig jsonConfig = new JsonConfig();
        jsonConfig.setRootClass(beanClass);
        jsonConfig.setClassMap(classMap);
        return toBean(jsonObject, jsonConfig);
    }

    /**
     * Creates a bean from a JSONObject, with the specific configuration.
     */
    public static Object toBean(JSONObject jsonObject, JsonConfig jsonConfig) {
        if (jsonObject == null || jsonObject.isNullObject()) {
            return null;
        }

        Class beanClass = jsonConfig.getRootClass();
        if (beanClass == null) {
            return toBean(jsonObject);
        }

        Object bean = null;
        try {
            if (beanClass.isInterface()) {
                if (!Map.class.isAssignableFrom(beanClass)) {
                    throw new JSONException("beanClass is an interface. " + beanClass);
                } else {
                    bean = new HashMap();
                }
            } else {
                bean = jsonConfig.getNewBeanInstanceStrategy()
                    .newInstance(beanClass, jsonObject);
            }
        } catch (JSONException jsone) {
            throw jsone;
        } catch (Exception e) {
            throw new JSONException(e);
        }

        return toBean(jsonObject, bean, jsonConfig);
    }

    /**
     * <p>This class is considered private API.</p>
     * It should only be used for developers extending the capabilities of the library and/or
     * those writing custom JSON Bean/Value processors.
     */
    public static class Impl {
        public static Property getProperty(Class beanClass, Object bean, String key, JsonConfig jsonConfig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
            key = jsonConfig.isSkipJavaIdentifierTransformationInMapKeys() ? key
                : JSONUtils.convertToJavaIdentifier(key, jsonConfig);

            try {
                if (!jsonConfig.isIgnorePublicFields()) { return new FieldProperty(bean.getClass().getField(key)); }
            } catch (NoSuchFieldException e) {
                // fall through
            }

            PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(bean, key);
            if (pd != null) { return new MethodProperty(pd); }

            if (Map.class.isAssignableFrom(beanClass)) { return new PropertyOnMap(key); }

            return null;
        }

        public static JSONObject defaultBeanProcessing(Object bean, JsonConfig jsonConfig) {
            Class beanClass = bean.getClass();
            PropertyNameProcessor propertyNameProcessor = jsonConfig.findJsonPropertyNameProcessor(beanClass);
            Collection exclusions = jsonConfig.getMergedExcludes(beanClass);
            JSONObject jsonObject = new JSONObject();
            try {
                PropertyDescriptor[] pds = PropertyUtils.getPropertyDescriptors(bean);
                PropertyFilter jsonPropertyFilter = jsonConfig.getJsonPropertyFilter();
                for (int i = 0; i < pds.length; i++) {
                    boolean bypass = false;
                    String key = pds[i].getName();
                    if (exclusions.contains(key)) {
                        continue;
                    }

                    if (jsonConfig.isIgnoreTransientFields() && isTransientField(key, beanClass, jsonConfig)) {
                        continue;
                    }

                    Class type = pds[i].getPropertyType();
                    try {
                        pds[i].getReadMethod();
                    } catch (Exception e) {
                        // bug 2565295
                        String warning = "Property '" + key + "' of " + beanClass + " has no read method. SKIPPED";
                        fireWarnEvent(warning, jsonConfig);
                        LOG.info(warning);
                        continue;
                    }
                    if (pds[i].getReadMethod() != null) {
               /*
               if( jsonConfig.isIgnoreJPATransient() ){
                  try{
                     Class transientClass = Class.forName( "javax.persistence.Transient" );
                     if( pds[i].getReadMethod()
                           .getAnnotation( transientClass ) != null ){
                        continue;
                     }
                  }catch( ClassNotFoundException cnfe ){
                     // ignore
                  }
               }
               */
                        if (isTransient(pds[i].getReadMethod(), jsonConfig)) { continue; }

                        Object value = PropertyUtils.getProperty(bean, key);
                        if (jsonPropertyFilter != null && jsonPropertyFilter.apply(bean, key, value)) {
                            continue;
                        }
                        JsonValueProcessor jsonValueProcessor = jsonConfig.findJsonValueProcessor(
                            beanClass, type, key);
                        if (jsonValueProcessor != null) {
                            value = jsonValueProcessor.processObjectValue(key, value, jsonConfig);
                            bypass = true;
                            if (!JsonVerifier.isValidJsonValue(value)) {
                                throw new JSONException("Value is not a valid JSON value. " + value);
                            }
                        }
                        if (propertyNameProcessor != null) {
                            key = propertyNameProcessor.processPropertyName(beanClass, key);
                        }
                        setValue(jsonObject, key, value, type, jsonConfig, bypass);
                    } else {
                        String warning = "Property '" + key + "' of " + beanClass + " has no read method. SKIPPED";
                        fireWarnEvent(warning, jsonConfig);
                        LOG.info(warning);
                    }
                }
                // inspect public fields, this operation may fail under
                // a SecurityManager so we will eat all exceptions
                try {
                    if (!jsonConfig.isIgnorePublicFields()) {
                        Field[] fields = beanClass.getFields();
                        for (int i = 0; i < fields.length; i++) {
                            boolean bypass = false;
                            Field field = fields[i];
                            String key = field.getName();
                            if (exclusions.contains(key)) {
                                continue;
                            }

                            // 3.0.0 - ignore static properties
                            if (Modifier.isStatic(field.getModifiers())) {
                                continue;
                            }

                            if (jsonConfig.isIgnoreTransientFields() && isTransientField(field, jsonConfig)) {
                                continue;
                            }

                            Class type = field.getType();
                            Object value = field.get(bean);
                            if (jsonPropertyFilter != null && jsonPropertyFilter.apply(bean, key, value)) {
                                continue;
                            }
                            JsonValueProcessor jsonValueProcessor = jsonConfig.findJsonValueProcessor(beanClass, type, key);
                            if (jsonValueProcessor != null) {
                                value = jsonValueProcessor.processObjectValue(key, value, jsonConfig);
                                bypass = true;
                                if (!JsonVerifier.isValidJsonValue(value)) {
                                    throw new JSONException("Value is not a valid JSON value. " + value);
                                }
                            }
                            if (propertyNameProcessor != null) {
                                key = propertyNameProcessor.processPropertyName(beanClass, key);
                            }
                            setValue(jsonObject, key, value, type, jsonConfig, bypass);
                        }
                    }
                } catch (Exception e) {
                    LOG.trace("Couldn't read public fields.", e);
                }
            } catch (JSONException jsone) {
                removeInstance(bean);
                fireErrorEvent(jsone, jsonConfig);
                throw jsone;
            } catch (Exception e) {
                removeInstance(bean);
                JSONException jsone = new JSONException(e);
                fireErrorEvent(jsone, jsonConfig);
                throw jsone;
            }
            return jsonObject;
        }

        /**
         * Creates a JSONObject from a POJO.<br>
         * Supports nested maps, POJOs, and arrays/collections.
         *
         * @param bean An object with POJO conventions
         *
         * @throws JSONException if the bean can not be converted to a proper
         *                       JSONObject.
         */
        public static JSONObject fromBean(Object bean, JsonConfig jsonConfig) {
            if (!addInstance(bean)) {
                try {
                    return jsonConfig.getCycleDetectionStrategy()
                        .handleRepeatedReferenceAsObject(bean);
                } catch (JSONException jsone) {
                    removeInstance(bean);
                    fireErrorEvent(jsone, jsonConfig);
                    throw jsone;
                } catch (RuntimeException e) {
                    removeInstance(bean);
                    JSONException jsone = new JSONException(e);
                    fireErrorEvent(jsone, jsonConfig);
                    throw jsone;
                }
            }
            fireObjectStartEvent(jsonConfig);

            JsonBeanProcessor processor = jsonConfig.findJsonBeanProcessor(bean.getClass());
            if (processor != null) {
                JSONObject json = null;
                try {
                    json = processor.processBean(bean, jsonConfig);
                    if (json == null) {
                        json = (JSONObject) jsonConfig.findDefaultValueProcessor(bean.getClass())
                            .getDefaultValue(bean.getClass());
                        if (json == null) {
                            json = new JSONObject(true);
                        }
                    }
                    removeInstance(bean);
                    fireObjectEndEvent(jsonConfig);
                } catch (JSONException jsone) {
                    removeInstance(bean);
                    fireErrorEvent(jsone, jsonConfig);
                    throw jsone;
                } catch (RuntimeException e) {
                    removeInstance(bean);
                    JSONException jsone = new JSONException(e);
                    fireErrorEvent(jsone, jsonConfig);
                    throw jsone;
                }
                return json;
            }

            JSONObject jsonObject = defaultBeanProcessing(bean, jsonConfig);
            removeInstance(bean);
            fireObjectEndEvent(jsonConfig);
            return jsonObject;
        }

        public static JSONObject fromDynaBean(DynaBean bean, JsonConfig jsonConfig) {
            if (bean == null) {
                fireObjectStartEvent(jsonConfig);
                fireObjectEndEvent(jsonConfig);
                return new JSONObject(true);
            }

            if (!addInstance(bean)) {
                try {
                    return jsonConfig.getCycleDetectionStrategy()
                        .handleRepeatedReferenceAsObject(bean);
                } catch (JSONException jsone) {
                    removeInstance(bean);
                    fireErrorEvent(jsone, jsonConfig);
                    throw jsone;
                } catch (RuntimeException e) {
                    removeInstance(bean);
                    JSONException jsone = new JSONException(e);
                    fireErrorEvent(jsone, jsonConfig);
                    throw jsone;
                }
            }
            fireObjectStartEvent(jsonConfig);

            JSONObject jsonObject = new JSONObject();
            try {
                DynaProperty[] props = bean.getDynaClass()
                    .getDynaProperties();
                Collection exclusions = jsonConfig.getMergedExcludes();
                PropertyFilter jsonPropertyFilter = jsonConfig.getJsonPropertyFilter();
                for (int i = 0; i < props.length; i++) {
                    boolean bypass = false;
                    DynaProperty dynaProperty = props[i];
                    String key = dynaProperty.getName();
                    if (exclusions.contains(key)) {
                        continue;
                    }
                    Class type = dynaProperty.getType();
                    Object value = bean.get(dynaProperty.getName());
                    if (jsonPropertyFilter != null && jsonPropertyFilter.apply(bean, key, value)) {
                        continue;
                    }
                    JsonValueProcessor jsonValueProcessor = jsonConfig.findJsonValueProcessor(type, key);
                    if (jsonValueProcessor != null) {
                        value = jsonValueProcessor.processObjectValue(key, value, jsonConfig);
                        bypass = true;
                        if (!JsonVerifier.isValidJsonValue(value)) {
                            throw new JSONException("Value is not a valid JSON value. " + value);
                        }
                    }
                    setValue(jsonObject, key, value, type, jsonConfig, bypass);
                }
            } catch (JSONException jsone) {
                removeInstance(bean);
                fireErrorEvent(jsone, jsonConfig);
                throw jsone;
            } catch (RuntimeException e) {
                removeInstance(bean);
                JSONException jsone = new JSONException(e);
                fireErrorEvent(jsone, jsonConfig);
                throw jsone;
            }

            removeInstance(bean);
            fireObjectEndEvent(jsonConfig);
            return jsonObject;
        }

        public static JSONObject fromJSONObject(JSONObject object, JsonConfig jsonConfig) {
            if (object == null || object.isNullObject()) {
                fireObjectStartEvent(jsonConfig);
                fireObjectEndEvent(jsonConfig);
                return new JSONObject(true);
            }

            if (!addInstance(object)) {
                try {
                    return jsonConfig.getCycleDetectionStrategy()
                        .handleRepeatedReferenceAsObject(object);
                } catch (JSONException jsone) {
                    removeInstance(object);
                    fireErrorEvent(jsone, jsonConfig);
                    throw jsone;
                } catch (RuntimeException e) {
                    removeInstance(object);
                    JSONException jsone = new JSONException(e);
                    fireErrorEvent(jsone, jsonConfig);
                    throw jsone;
                }
            }
            fireObjectStartEvent(jsonConfig);

            JSONArray sa = object.names(jsonConfig);
            Collection exclusions = jsonConfig.getMergedExcludes();
            JSONObject jsonObject = new JSONObject();
            PropertyFilter jsonPropertyFilter = jsonConfig.getJsonPropertyFilter();
            for (Iterator i = sa.iterator(); i.hasNext(); ) {
                Object k = i.next();
                if (k == null) {
                    throw new JSONException("JSON keys cannot be null.");
                }
                if (!(k instanceof String) && !jsonConfig.isAllowNonStringKeys()) {
                    throw new ClassCastException("JSON keys must be strings.");
                }
                String key = String.valueOf(k);
                if ("null".equals(key)) {
                    throw new NullPointerException("JSON keys must not be null nor the 'null' string.");
                }
                if (exclusions.contains(key)) {
                    continue;
                }
                Object value = object.opt(key);
                if (jsonPropertyFilter != null && jsonPropertyFilter.apply(object, key, value)) {
                    continue;
                }
                if (jsonObject.properties.containsKey(key)) {
                    jsonObject.accumulate(key, value, jsonConfig);
                    firePropertySetEvent(key, value, true, jsonConfig);
                } else {
                    jsonObject.setInternal(key, value, jsonConfig);
                    firePropertySetEvent(key, value, false, jsonConfig);
                }
            }

            removeInstance(object);
            fireObjectEndEvent(jsonConfig);
            return jsonObject;
        }

        public static JSONObject fromJSONString(JSONString string, JsonConfig jsonConfig) {
            return fromJSONTokener(new JSONTokener(string.toJSONString()), jsonConfig);
        }

        public static JSONObject fromJSONTokener(JSONTokener tokener, JsonConfig jsonConfig) {

            try {
                char c;
                String key;
                Object value;

                if (tokener.matches("null.*")) {
                    fireObjectStartEvent(jsonConfig);
                    fireObjectEndEvent(jsonConfig);
                    return new JSONObject(true);
                }

                if (tokener.nextClean() != '{') {
                    throw tokener.syntaxError("A JSONObject text must begin with '{'");
                }
                fireObjectStartEvent(jsonConfig);

                Collection exclusions = jsonConfig.getMergedExcludes();
                PropertyFilter jsonPropertyFilter = jsonConfig.getJsonPropertyFilter();
                JSONObject jsonObject = new JSONObject();
                for (; ; ) {
                    c = tokener.nextClean();
                    switch (c) {
                        case 0:
                            throw tokener.syntaxError("A JSONObject text must end with '}'");
                        case '{':
                            throw tokener.syntaxError("Object found where key expected");
                        case '[':
                            throw tokener.syntaxError("Array found where key expected");
                        case '}':
                            fireObjectEndEvent(jsonConfig);
                            return jsonObject;
                        default:
                            tokener.back();
                            key = tokener.nextValue(jsonConfig)
                                .toString();
                    }

            /*
             * The key is followed by ':'. We will also tolerate '=' or '=>'.
             */

                    c = tokener.nextClean();
                    if (c == '=') {
                        if (tokener.next() != '>') {
                            tokener.back();
                        }
                    } else if (c != ':') {
                        throw tokener.syntaxError("Expected a ':' after a key");
                    }

                    char peek = tokener.peek();
                    boolean quoted = peek == '"' || peek == '\'';
                    Object v = tokener.nextValue(jsonConfig);
                    if (quoted || !JSONUtils.isFunctionHeader(v)) {
                        if (exclusions.contains(key)) {
                            switch (tokener.nextClean()) {
                                case ';':
                                case ',':
                                    if (tokener.nextClean() == '}') {
                                        fireObjectEndEvent(jsonConfig);
                                        return jsonObject;
                                    }
                                    tokener.back();
                                    break;
                                case '}':
                                    fireObjectEndEvent(jsonConfig);
                                    return jsonObject;
                                default:
                                    throw tokener.syntaxError("Expected a ',' or '}'");
                            }
                            continue;
                        }
                        if (jsonPropertyFilter == null || !jsonPropertyFilter.apply(tokener, key, v)) {
                            if (jsonObject.properties.containsKey(key)) {
                                jsonObject.accumulate(key, v, jsonConfig);
                                firePropertySetEvent(key, v, true, jsonConfig);
                            } else {
                                jsonObject.element(key, v, jsonConfig);
                                firePropertySetEvent(key, v, false, jsonConfig);
                            }
                        }
                    } else {
                        // read params if any
                        String params = JSONUtils.getFunctionParams((String) v);
                        // read function text
                        int i = 0;
                        StringBuffer sb = new StringBuffer();
                        for (; ; ) {
                            char ch = tokener.next();
                            if (ch == 0) {
                                break;
                            }
                            if (ch == '{') {
                                i++;
                            }
                            if (ch == '}') {
                                i--;
                            }
                            sb.append(ch);
                            if (i == 0) {
                                break;
                            }
                        }
                        if (i != 0) {
                            throw tokener.syntaxError("Unbalanced '{' or '}' on prop: " + v);
                        }
                        // trim '{' at start and '}' at end
                        String text = sb.toString();
                        text = text.substring(1, text.length() - 1)
                            .trim();
                        value = new JSONFunction(
                            (params != null) ? StringUtils.split(params, ",") : null, text);
                        if (jsonPropertyFilter == null || !jsonPropertyFilter.apply(tokener, key, value)) {
                            if (jsonObject.properties.containsKey(key)) {
                                jsonObject.accumulate(key, value, jsonConfig);
                                firePropertySetEvent(key, value, true, jsonConfig);
                            } else {
                                jsonObject.element(key, value, jsonConfig);
                                firePropertySetEvent(key, value, false, jsonConfig);
                            }
                        }
                    }

            /*
             * Pairs are separated by ','. We will also tolerate ';'.
             */

                    switch (tokener.nextClean()) {
                        case ';':
                        case ',':
                            if (tokener.nextClean() == '}') {
                                fireObjectEndEvent(jsonConfig);
                                return jsonObject;
                            }
                            tokener.back();
                            break;
                        case '}':
                            fireObjectEndEvent(jsonConfig);
                            return jsonObject;
                        default:
                            throw tokener.syntaxError("Expected a ',' or '}'");
                    }
                }
            } catch (JSONException jsone) {
                fireErrorEvent(jsone, jsonConfig);
                throw jsone;
            }
        }

        public static JSONObject fromMap(Map map, JsonConfig jsonConfig) {
            if (map == null) {
                fireObjectStartEvent(jsonConfig);
                fireObjectEndEvent(jsonConfig);
                return new JSONObject(true);
            }

            if (!addInstance(map)) {
                try {
                    return jsonConfig.getCycleDetectionStrategy()
                        .handleRepeatedReferenceAsObject(map);
                } catch (JSONException jsone) {
                    removeInstance(map);
                    fireErrorEvent(jsone, jsonConfig);
                    throw jsone;
                } catch (RuntimeException e) {
                    removeInstance(map);
                    JSONException jsone = new JSONException(e);
                    fireErrorEvent(jsone, jsonConfig);
                    throw jsone;
                }
            }
            fireObjectStartEvent(jsonConfig);

            Collection exclusions = jsonConfig.getMergedExcludes();
            JSONObject jsonObject = new JSONObject();
            PropertyFilter jsonPropertyFilter = jsonConfig.getJsonPropertyFilter();
            try {
                for (Iterator entries = map.entrySet()
                    .iterator(); entries.hasNext(); ) {
                    boolean bypass = false;
                    Map.Entry entry = (Map.Entry) entries.next();
                    Object k = entry.getKey();
                    if (k == null) {
                        throw new JSONException("JSON keys cannot be null.");
                    }
                    if (!(k instanceof String) && !jsonConfig.isAllowNonStringKeys()) {
                        throw new ClassCastException("JSON keys must be strings.");
                    }
                    String key = String.valueOf(k);
                    if ("null".equals(key)) {
                        throw new NullPointerException("JSON keys must not be null nor the 'null' string.");
                    }
                    if (exclusions.contains(key)) {
                        continue;
                    }
                    Object value = entry.getValue();
                    if (jsonPropertyFilter != null && jsonPropertyFilter.apply(map, key, value)) {
                        continue;
                    }
                    if (value != null) {
                        JsonValueProcessor jsonValueProcessor = jsonConfig.findJsonValueProcessor(
                            value.getClass(), key);
                        if (jsonValueProcessor != null) {
                            value = jsonValueProcessor.processObjectValue(key, value, jsonConfig);
                            bypass = true;
                            if (!JsonVerifier.isValidJsonValue(value)) {
                                throw new JSONException("Value is not a valid JSON value. " + value);
                            }
                        }
                        setValue(jsonObject, key, value, value.getClass(), jsonConfig, bypass);
                    } else {
                        if (jsonObject.properties.containsKey(key)) {
                            jsonObject.accumulate(key, JSONNull.getInstance());
                            firePropertySetEvent(key, JSONNull.getInstance(), true, jsonConfig);
                        } else {
                            jsonObject.element(key, JSONNull.getInstance());
                            firePropertySetEvent(key, JSONNull.getInstance(), false, jsonConfig);
                        }
                    }
                }
            } catch (JSONException jsone) {
                removeInstance(map);
                fireErrorEvent(jsone, jsonConfig);
                throw jsone;
            } catch (RuntimeException e) {
                removeInstance(map);
                JSONException jsone = new JSONException(e);
                fireErrorEvent(jsone, jsonConfig);
                throw jsone;
            }

            removeInstance(map);
            fireObjectEndEvent(jsonConfig);
            return jsonObject;
        }

        public static JSONObject fromString(String str, JsonConfig jsonConfig) {
            if (str == null || "null".equals(str)) {
                fireObjectStartEvent(jsonConfig);
                fireObjectEndEvent(jsonConfig);
                return new JSONObject(true);
            }
            return fromJSONTokener(new JSONTokener(str), jsonConfig);
        }

        public static Object convertPropertyValueToArray(String key, Object value, Class targetType,
                                                         JsonConfig jsonConfig, Map classMap) {
            Class innerType = JSONUtils.getInnerComponentType(targetType);
            Class targetInnerType = findTargetClass(key, classMap);
            if (innerType.equals(Object.class) && targetInnerType != null
                && !targetInnerType.equals(Object.class)) {
                innerType = targetInnerType;
            }
            JsonConfig jsc = jsonConfig.copy();
            jsc.setRootClass(innerType);
            jsc.setClassMap(classMap);
            Object array = JSONArray.toArray((JSONArray) value, jsc);
            if (innerType.isPrimitive() || JSONUtils.isNumber(innerType)
                || Boolean.class.isAssignableFrom(innerType) || JSONUtils.isString(innerType)) {
                array = JSONUtils.getMorpherRegistry()
                    .morph(Array.newInstance(innerType, 0)
                        .getClass(), array);
            } else if (!array.getClass()
                .equals(targetType)) {
                if (!targetType.equals(Object.class)) {
                    Morpher morpher = JSONUtils.getMorpherRegistry()
                        .getMorpherFor(Array.newInstance(innerType, 0)
                            .getClass());
                    if (IdentityObjectMorpher.getInstance()
                        .equals(morpher)) {
                        ObjectArrayMorpher beanMorpher = new ObjectArrayMorpher(new BeanMorpher(innerType,
                            JSONUtils.getMorpherRegistry()));
                        JSONUtils.getMorpherRegistry()
                            .registerMorpher(beanMorpher);
                    }
                    array = JSONUtils.getMorpherRegistry()
                        .morph(Array.newInstance(innerType, 0)
                            .getClass(), array);
                }
            }
            return array;
        }

        public static List convertPropertyValueToList(String key, Object value, JsonConfig jsonConfig,
                                                      String name, Map classMap) {
            Class targetClass = findTargetClass(key, classMap);
            targetClass = targetClass == null ? findTargetClass(name, classMap) : targetClass;
            JsonConfig jsc = jsonConfig.copy();
            jsc.setRootClass(targetClass);
            jsc.setClassMap(classMap);
            List list = (List) JSONArray.toCollection((JSONArray) value, jsc);
            return list;
        }

        public static Collection convertPropertyValueToCollection(String key, Object value, JsonConfig jsonConfig,
                                                                  String name, Map classMap, Class collectionType) {
            Class targetClass = findTargetClass(key, classMap);
            targetClass = targetClass == null ? findTargetClass(name, classMap) : targetClass;
            JsonConfig jsc = jsonConfig.copy();
            jsc.setRootClass(targetClass);
            jsc.setClassMap(classMap);
            jsc.setCollectionType(collectionType);
            return JSONArray.toCollection((JSONArray) value, jsc);
        }

        public static Class resolveClass(Map classMap, String key, String name, Class type) {
            Class targetClass = findTargetClass(key, classMap);
            if (targetClass == null) {
                targetClass = findTargetClass(name, classMap);
            }
            if (targetClass == null && type != null) {
                if (List.class.equals(type)) {
                    targetClass = ArrayList.class;
                } else if (Map.class.equals(type)) {
                    targetClass = LinkedHashMap.class;
                } else if (Set.class.equals(type)) {
                    targetClass = LinkedHashSet.class;
                } else if (!type.isInterface() && !Object.class.equals(type)) {
                    targetClass = type;
                }
            }
            return targetClass;
        }

        /**
         * Locates a Class associated to a specifi key.<br>
         * The key may be a regexp.
         */
        public static Class findTargetClass(String key, Map classMap) {
            // try get first
            Class targetClass = (Class) classMap.get(key);
            if (targetClass == null) {
                // try with regexp
                // this will hit performance as it must iterate over all the keys
                // and create a RegexpMatcher for each key
                for (Iterator i = classMap.entrySet()
                    .iterator(); i.hasNext(); ) {
                    Map.Entry entry = (Map.Entry) i.next();
                    if (RegexpUtils.getMatcher((String) entry.getKey())
                        .matches(key)) {
                        targetClass = (Class) entry.getValue();
                        break;
                    }
                }
            }

            return targetClass;
        }

        public static boolean isTransientField(String name, Class beanClass, JsonConfig jsonConfig) {
            try {
                return isTransientField(beanClass.getDeclaredField(name), jsonConfig);
            } catch (Exception e) {
                LOG.info("Error while inspecting field " + beanClass + "." + name + " for transient status.", e);
            }
            return false;
        }

        public static boolean isTransientField(Field field, JsonConfig jsonConfig) {
            try {
                if ((field.getModifiers() & Modifier.TRANSIENT) == Modifier.TRANSIENT) { return true; }
                return isTransient(field, jsonConfig);
            } catch (Exception e) {
                LOG.info("Error while inspecting field " + field + " for transient status.", e);
            }
            return false;
        }

        public static boolean isTransient(AnnotatedElement element, JsonConfig jsonConfig) {
            for (Iterator annotations = jsonConfig.getIgnoreFieldAnnotations().iterator(); annotations.hasNext(); ) {
                try {
                    String annotationClassName = (String) annotations.next();
                    if (element.getAnnotation((Class) Class.forName(annotationClassName)) != null) { return true; }
                } catch (Exception e) {
                    LOG.info("Error while inspecting " + element + " for transient status.", e);
                }
            }
            return false;
        }

        public static Object morphPropertyValue(String key, Object value, Class type, Class targetType) {
            Morpher morpher = JSONUtils.getMorpherRegistry()
                .getMorpherFor(targetType);
            if (IdentityObjectMorpher.getInstance()
                .equals(morpher)) {
                LOG.warn("Can't transform property '" + key + "' from " + type.getName() + " into "
                    + targetType.getName() + ". Will register a default Morpher");
                if (Enum.class.isAssignableFrom(targetType)) {
                    JSONUtils.getMorpherRegistry()
                        .registerMorpher(new EnumMorpher(targetType));
                } else {
                    JSONUtils.getMorpherRegistry()
                        .registerMorpher(new BeanMorpher(targetType, JSONUtils.getMorpherRegistry()));
                }
            }

            value = JSONUtils.getMorpherRegistry()
                .morph(targetType, value);
            return value;
        }

        /**
         * Sets a property on the target bean.<br>
         * Bean may be a Map or a POJO.
         */
        public static void setProperty(Object bean, String key, Object value, JsonConfig jsonConfig)
            throws Exception {
            PropertySetStrategy propertySetStrategy = jsonConfig.getPropertySetStrategy() != null ? jsonConfig.getPropertySetStrategy()
                : PropertySetStrategy.DEFAULT;
            propertySetStrategy.setProperty(bean, key, value, jsonConfig);
        }

        public static void setValue(JSONObject jsonObject, String key, Object value, Class type,
                                    JsonConfig jsonConfig, boolean bypass) {
            boolean accumulated = false;
            if (value == null) {
                value = jsonConfig.findDefaultValueProcessor(type)
                    .getDefaultValue(type);
                if (!JsonVerifier.isValidJsonValue(value)) {
                    throw new JSONException("Value is not a valid JSON value. " + value);
                }
            }
            if (jsonObject.properties.containsKey(key)) {
                if (String.class.isAssignableFrom(type)) {
                    Object o = jsonObject.opt(key);
                    if (o instanceof JSONArray) {
                        ((JSONArray) o).addString((String) value);
                    } else {
                        jsonObject.properties.put(key, new JSONArray().element(o)
                            .addString((String) value));
                    }
                } else {
                    jsonObject.accumulate(key, value, jsonConfig);
                }
                accumulated = true;
            } else {
                if (bypass || String.class.isAssignableFrom(type)) {
                    jsonObject.properties.put(key, value);
                } else {
                    jsonObject.setInternal(key, value, jsonConfig);
                }
            }

            value = jsonObject.opt(key);
            if (accumulated) {
                JSONArray array = (JSONArray) value;
                value = array.get(array.size() - 1);
            }
            firePropertySetEvent(key, value, accumulated, jsonConfig);
        }
    }

    /**
     * Creates a bean from a JSONObject, with the specific configuration.
     */
    public static Object toBean(JSONObject jsonObject, Object bean, JsonConfig jsonConfig) {
        if (jsonObject == null || jsonObject.isNullObject() || bean == null) {
            return bean;
        }

        Class rootClass = bean.getClass();
        Class beanClass = rootClass;
        Map classMap = jsonConfig.getClassMap();
        if (classMap == null) {
            classMap = Collections.EMPTY_MAP;
        }

        Map props = JSONUtils.getProperties(jsonObject);
        PropertyFilter javaPropertyFilter = jsonConfig.getJavaPropertyFilter();
        for (Iterator entries = jsonObject.names(jsonConfig)
            .iterator(); entries.hasNext(); ) {
            String name = (String) entries.next();
            Class type = (Class) props.get(name);
            Object value = jsonObject.get(name);
            if (javaPropertyFilter != null && javaPropertyFilter.apply(bean, name, value)) {
                continue;
            }
            String key = Map.class.isAssignableFrom(beanClass)
                && jsonConfig.isSkipJavaIdentifierTransformationInMapKeys() ? name
                : JSONUtils.convertToJavaIdentifier(name, jsonConfig);
            PropertyNameProcessor propertyNameProcessor = jsonConfig.findJavaPropertyNameProcessor(beanClass);
            if (propertyNameProcessor != null) {
                key = propertyNameProcessor.processPropertyName(beanClass, key);
            }
            try {
                Property pd = Impl.getProperty(rootClass, bean, key, jsonConfig);
                if (pd != null) {
                    if (!pd.isWritable()) {
                        LOG.info("Property '" + key + "' of " + bean.getClass() + " has no write method. SKIPPED.");
                        continue;
                    }
                    if (jsonConfig.getPropertySetStrategy() != null) {
                        pd = new PropertySetterStrategyDecorator(pd, jsonConfig.getPropertySetStrategy());
                    }

                    Class targetType = pd.getPropertyType();
                    if (!JSONUtils.isNull(value)) {
                        if (value instanceof JSONArray) {
                            if (List.class.isAssignableFrom(pd.getPropertyType())) {
                                pd.set(bean, Impl.convertPropertyValueToCollection(key, value,
                                    jsonConfig, name, classMap, pd.getPropertyType()), jsonConfig);
                            } else if (Set.class.isAssignableFrom(pd.getPropertyType())) {
                                pd.set(bean, Impl.convertPropertyValueToCollection(key, value,
                                    jsonConfig, name, classMap, pd.getPropertyType()), jsonConfig);
                            } else {
                                pd.set(bean, Impl.convertPropertyValueToArray(key, value,
                                    targetType, jsonConfig, classMap), jsonConfig);
                            }
                        } else if (String.class.isAssignableFrom(type) || JSONUtils.isBoolean(type)
                            || JSONUtils.isNumber(type) || JSONUtils.isString(type)
                            || JSONFunction.class.isAssignableFrom(type)) {
                            if (pd != null) {
                                if (jsonConfig.isHandleJettisonEmptyElement() && "".equals(value)) {
                                    pd.set(bean, null, jsonConfig);
                                } else if (!targetType.isInstance(value)) {
                                    pd.set(bean, Impl.morphPropertyValue(key, value, type,
                                        targetType), jsonConfig);
                                } else {
                                    pd.set(bean, value, jsonConfig);
                                }
                            } else if (beanClass == null || bean instanceof Map) {
                                pd.set(bean, value, jsonConfig);
                            } else {
                                LOG.warn("Tried to assign property " + key + ":" + type.getName()
                                    + " to bean of class " + bean.getClass()
                                    .getName());
                            }
                        } else {
                            if (jsonConfig.isHandleJettisonSingleElementArray()) {
                                JSONArray array = new JSONArray().element(value, jsonConfig);
                                Class newTargetClass = Impl.resolveClass(classMap, key, name, type);
                                JsonConfig jsc = jsonConfig.copy();
                                jsc.setRootClass(newTargetClass);
                                jsc.setClassMap(classMap);
                                if (targetType.isArray()) {
                                    pd.set(bean, JSONArray.toArray(array, jsc), jsonConfig);
                                } else if (JSONArray.class.isAssignableFrom(targetType)) {
                                    pd.set(bean, array, jsonConfig);
                                } else if (List.class.isAssignableFrom(targetType)
                                    || Set.class.isAssignableFrom(targetType)) {
                                    jsc.setCollectionType(targetType);
                                    pd.set(bean, JSONArray.toCollection(array, jsc),
                                        jsonConfig);
                                } else {
                                    pd.set(bean, toBean((JSONObject) value, jsc), jsonConfig);
                                }
                            } else {
                                if (targetType == Object.class || targetType.isInterface()) {
                                    Class targetTypeCopy = targetType;
                                    targetType = Impl.findTargetClass(key, classMap);
                                    targetType = targetType == null ? Impl.findTargetClass(name, classMap)
                                        : targetType;
                                    targetType = targetType == null && targetTypeCopy.isInterface() ? targetTypeCopy
                                        : targetType;
                                }
                                JsonConfig jsc = jsonConfig.copy();
                                jsc.setRootClass(targetType);
                                jsc.setClassMap(classMap);
                                pd.set(bean, toBean((JSONObject) value, jsc), jsonConfig);
                            }
                        }
                    } else {
                        if (type.isPrimitive()) {
                            // assume assigned default value
                            LOG.warn("Tried to assign null value to " + key + ":" + type.getName());
                            pd.set(bean, JSONUtils.getMorpherRegistry()
                                .morph(type, null), jsonConfig);
                        } else {
                            pd.set(bean, null, jsonConfig);
                        }
                    }
                } else {
                    if (!JSONUtils.isNull(value)) {
                        if (value instanceof JSONArray) {
                            Impl.setProperty(bean, name, Impl.convertPropertyValueToCollection(key, value,
                                jsonConfig, name, classMap, List.class), jsonConfig);
                        } else if (String.class.isAssignableFrom(type) || JSONUtils.isBoolean(type)
                            || JSONUtils.isNumber(type) || JSONUtils.isString(type)
                            || JSONFunction.class.isAssignableFrom(type)) {
                            if (beanClass == null || bean instanceof Map || jsonConfig.getPropertySetStrategy() != null ||
                                !jsonConfig.isIgnorePublicFields()) {
                                Impl.setProperty(bean, name, value, jsonConfig);
                            } else {
                                LOG.warn("Tried to assign property " + key + ":" + type.getName()
                                    + " to bean of class " + bean.getClass()
                                    .getName());
                            }
                        } else {
                            if (jsonConfig.isHandleJettisonSingleElementArray()) {
                                Class newTargetClass = Impl.resolveClass(classMap, key, name, type);
                                JsonConfig jsc = jsonConfig.copy();
                                jsc.setRootClass(newTargetClass);
                                jsc.setClassMap(classMap);
                                Impl.setProperty(bean, name, toBean((JSONObject) value, jsc), jsonConfig);
                            } else {
                                Impl.setProperty(bean, name, value, jsonConfig);
                            }
                        }
                    } else {
                        if (type.isPrimitive()) {
                            // assume assigned default value
                            LOG.warn("Tried to assign null value to " + key + ":" + type.getName());
                            Impl.setProperty(bean, name, JSONUtils.getMorpherRegistry()
                                .morph(type, null), jsonConfig);
                        } else {
                            Impl.setProperty(bean, name, null, jsonConfig);
                        }
                    }
                }
            } catch (JSONException jsone) {
                throw jsone;
            } catch (Exception e) {
                throw new JSONException("Error while setting property=" + name + " type " + type, e);
            }
        }

        return bean;
    }

   /*
   private static Collection convertPropertyValueToCollection( String key, Object value, JsonConfig jsonConfig,
         String name, Map classMap, Object bean ) {
      Class targetClass = findTargetClass( key, classMap );
      targetClass = targetClass == null ? findTargetClass( name, classMap ) : targetClass;

      PropertyDescriptor pd;
      try{
         pd = PropertyUtils.getPropertyDescriptor( bean, key );
      }catch( IllegalAccessException e ){
         throw new JSONException( e );
      }catch( InvocationTargetException e ){
         throw new JSONException( e );
      }catch( NoSuchMethodException e ){
         throw new JSONException( e );
      }

      if( null == targetClass ){
         Class[] cType = JSONArray.getCollectionType( pd, false );
         if( null != cType && cType.length == 1 ){
            targetClass = cType[0];
         }
      }

      JsonConfig jsc = jsonConfig.copy();
      jsc.setRootClass( targetClass );
      jsc.setClassMap( classMap );
      jsc.setCollectionType( pd.getPropertyType() );
      jsc.setEnclosedType( targetClass );
      Collection collection = JSONArray.toCollection( (JSONArray) value, jsonConfig );
      return collection;
   }
   */

    /**
     * Creates a JSONDynaBean from a JSONObject.
     */
    public Object toBean() {
        return toBean(this);
    }

    public Object toBean(Class beanClass) {
        return toBean(this, beanClass);
    }

    /**
     * Accumulate values under a key. It is similar to the element method except
     * that if there is already an object stored under the key then a JSONArray
     * is stored under the key to hold all of the accumulated values. If there is
     * already a JSONArray, then the new value is appended to it. In contrast,
     * the replace method replaces the previous value.
     *
     * @param key   A key string.
     * @param value An object to be accumulated under the key.
     *
     * @return this.
     *
     * @throws JSONException If the value is an invalid number or if the key is
     *                       null.
     */
    public JSONObject accumulate(String key, boolean value) {
        return _accumulate(key, value ? Boolean.TRUE : Boolean.FALSE, new JsonConfig());
    }

    // ------------------------------------------------------

    /**
     * Accumulate values under a key. It is similar to the element method except
     * that if there is already an object stored under the key then a JSONArray
     * is stored under the key to hold all of the accumulated values. If there is
     * already a JSONArray, then the new value is appended to it. In contrast,
     * the replace method replaces the previous value.
     *
     * @param key   A key string.
     * @param value An object to be accumulated under the key.
     *
     * @return this.
     *
     * @throws JSONException If the value is an invalid number or if the key is
     *                       null.
     */
    public JSONObject accumulate(String key, double value) {
        return _accumulate(key, Double.valueOf(value), new JsonConfig());
    }

    /**
     * Accumulate values under a key. It is similar to the element method except
     * that if there is already an object stored under the key then a JSONArray
     * is stored under the key to hold all of the accumulated values. If there is
     * already a JSONArray, then the new value is appended to it. In contrast,
     * the replace method replaces the previous value.
     *
     * @param key   A key string.
     * @param value An object to be accumulated under the key.
     *
     * @return this.
     *
     * @throws JSONException If the value is an invalid number or if the key is
     *                       null.
     */
    public JSONObject accumulate(String key, int value) {
        return _accumulate(key, Integer.valueOf(value), new JsonConfig());
    }

    /**
     * Accumulate values under a key. It is similar to the element method except
     * that if there is already an object stored under the key then a JSONArray
     * is stored under the key to hold all of the accumulated values. If there is
     * already a JSONArray, then the new value is appended to it. In contrast,
     * the replace method replaces the previous value.
     *
     * @param key   A key string.
     * @param value An object to be accumulated under the key.
     *
     * @return this.
     *
     * @throws JSONException If the value is an invalid number or if the key is
     *                       null.
     */
    public JSONObject accumulate(String key, long value) {
        return _accumulate(key, Long.valueOf(value), new JsonConfig());
    }

    /**
     * Accumulate values under a key. It is similar to the element method except
     * that if there is already an object stored under the key then a JSONArray
     * is stored under the key to hold all of the accumulated values. If there is
     * already a JSONArray, then the new value is appended to it. In contrast,
     * the replace method replaces the previous value.
     *
     * @param key   A key string.
     * @param value An object to be accumulated under the key.
     *
     * @return this.
     *
     * @throws JSONException If the value is an invalid number or if the key is
     *                       null.
     */
    public JSONObject accumulate(String key, Object value) {
        return _accumulate(key, value, new JsonConfig());
    }

    /**
     * Accumulate values under a key. It is similar to the element method except
     * that if there is already an object stored under the key then a JSONArray
     * is stored under the key to hold all of the accumulated values. If there is
     * already a JSONArray, then the new value is appended to it. In contrast,
     * the replace method replaces the previous value.
     *
     * @param key   A key string.
     * @param value An object to be accumulated under the key.
     *
     * @return this.
     *
     * @throws JSONException If the value is an invalid number or if the key is
     *                       null.
     */
    public JSONObject accumulate(String key, Object value, JsonConfig jsonConfig) {
        return _accumulate(key, value, jsonConfig);
    }

    public void accumulateAll(Map map) {
        accumulateAll(map, new JsonConfig());
    }

    public void accumulateAll(Map map, JsonConfig jsonConfig) {
        if (map instanceof JSONObject) {
            for (Iterator entries = map.entrySet()
                .iterator(); entries.hasNext(); ) {
                Map.Entry entry = (Map.Entry) entries.next();
                String key = (String) entry.getKey();
                Object value = entry.getValue();
                accumulate(key, value, jsonConfig);
            }
        } else {
            for (Iterator entries = map.entrySet()
                .iterator(); entries.hasNext(); ) {
                Map.Entry entry = (Map.Entry) entries.next();
                String key = String.valueOf(entry.getKey());
                Object value = entry.getValue();
                accumulate(key, value, jsonConfig);
            }
        }
    }

    public int compareTo(Object obj) {
        if (obj != null && (obj instanceof JSONObject)) {
            JSONObject other = (JSONObject) obj;
            int size1 = size();
            int size2 = other.size();
            if (size1 < size2) {
                return -1;
            } else if (size1 > size2) {
                return 1;
            } else if (this.equals(other)) {
                return 0;
            }
        }
        return -1;
    }

    public boolean containsKey(Object key) {
        return properties.containsKey(key);
    }

    public boolean containsValue(Object value) {
        return containsValue(value, new JsonConfig());
    }

    public Object get(Object key) {
        if (key instanceof String) {
            return get((String) key);
        }
        return null;
    }

    public Object put(String key, Object value) {
        if (key == null) {
            throw new IllegalArgumentException("key is null.");
        }
        Object previous = properties.get(key);
        element(String.valueOf(key), value);
        return previous;
    }

    public Object remove(Object key) {
        return properties.remove(key);
    }

    public void putAll(Map map) {
        putAll(map, new JsonConfig());
    }

    public void clear() {
        properties.clear();
    }

    public Set keySet() {
        return Collections.unmodifiableSet(properties.keySet());
    }

    public Collection values() {
        return Collections.unmodifiableCollection(properties.values());
    }

    public Set entrySet() {
        return Collections.unmodifiableSet(properties.entrySet());
    }

    public boolean containsValue(Object value, JsonConfig jsonConfig) {
        try {
            value = processValue(value, jsonConfig);
        } catch (JSONException e) {
            return false;
        }
        return properties.containsValue(value);
    }

    /**
     * Remove a name and its value, if present.
     *
     * @param key A key string.
     *
     * @return this.
     */
    public JSONObject discard(String key) {
        verifyIsNull();
        this.properties.remove(key);
        return this;
    }

    /**
     * Put a key/boolean pair in the JSONObject.
     *
     * @param key   A key string.
     * @param value A boolean which is the value.
     *
     * @return this.
     *
     * @throws JSONException If the key is null.
     */
    public JSONObject element(String key, boolean value) {
        verifyIsNull();
        return element(key, value ? Boolean.TRUE : Boolean.FALSE);
    }

    /**
     * Put a key/value pair in the JSONObject, where the value will be a
     * JSONArray which is produced from a Collection.
     *
     * @param key   A key string.
     * @param value A Collection value.
     *
     * @return this.
     *
     * @throws JSONException
     */
    public JSONObject element(String key, Collection value) {
        return element(key, value, new JsonConfig());
    }

    /**
     * Put a key/value pair in the JSONObject, where the value will be a
     * JSONArray which is produced from a Collection.
     *
     * @param key   A key string.
     * @param value A Collection value.
     *
     * @return this.
     *
     * @throws JSONException
     */
    public JSONObject element(String key, Collection value, JsonConfig jsonConfig) {
        if (!(value instanceof JSONArray)) {
            value = JSONArray.fromObject(value, jsonConfig);
        }
        return setInternal(key, value, jsonConfig);
    }

    /**
     * Put a key/double pair in the JSONObject.
     *
     * @param key   A key string.
     * @param value A double which is the value.
     *
     * @return this.
     *
     * @throws JSONException If the key is null or if the number is invalid.
     */
    public JSONObject element(String key, double value) {
        verifyIsNull();
        Double d = new Double(value);
        JSONUtils.testValidity(d);
        return element(key, d);
    }

    /**
     * Put a key/int pair in the JSONObject.
     *
     * @param key   A key string.
     * @param value An int which is the value.
     *
     * @return this.
     *
     * @throws JSONException If the key is null.
     */
    public JSONObject element(String key, int value) {
        verifyIsNull();
        return element(key, new Integer(value));
    }

    /**
     * Put a key/long pair in the JSONObject.
     *
     * @param key   A key string.
     * @param value A long which is the value.
     *
     * @return this.
     *
     * @throws JSONException If the key is null.
     */
    public JSONObject element(String key, long value) {
        verifyIsNull();
        return element(key, new Long(value));
    }

    /**
     * Put a key/value pair in the JSONObject, where the value will be a
     * JSONObject which is produced from a Map.
     *
     * @param key   A key string.
     * @param value A Map value.
     *
     * @return this.
     *
     * @throws JSONException
     */
    public JSONObject element(String key, Map value) {
        return element(key, value, new JsonConfig());
    }

    /**
     * Put a key/value pair in the JSONObject, where the value will be a
     * JSONObject which is produced from a Map.
     *
     * @param key   A key string.
     * @param value A Map value.
     *
     * @return this.
     *
     * @throws JSONException
     */
    public JSONObject element(String key, Map value, JsonConfig jsonConfig) {
        verifyIsNull();
        if (value instanceof JSONObject) {
            return setInternal(key, value, jsonConfig);
        } else {
            return element(key, JSONObject.fromObject(value, jsonConfig), jsonConfig);
        }
    }

    /**
     * Put a key/value pair in the JSONObject. If the value is null, then the key
     * will be removed from the JSONObject if it is present.<br>
     * If there is a previous value assigned to the key, it will call accumulate.
     *
     * @param key   A key string.
     * @param value An object which is the value. It should be of one of these
     *              types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
     *              String, or the JSONNull object.
     *
     * @return this.
     *
     * @throws JSONException If the value is non-finite number or if the key is
     *                       null.
     */
    public JSONObject element(String key, Object value) {
        return element(key, value, new JsonConfig());
    }

    /**
     * Put a key/value pair in the JSONObject. If the value is null, then the key
     * will be removed from the JSONObject if it is present.<br>
     * If there is a previous value assigned to the key, it will call accumulate.
     *
     * @param key   A key string.
     * @param value An object which is the value. It should be of one of these
     *              types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
     *              String, or the JSONNull object.
     *
     * @return this.
     *
     * @throws JSONException If the value is non-finite number or if the key is
     *                       null.
     */
    public JSONObject element(String key, Object value, JsonConfig jsonConfig) {
        verifyIsNull();
        if (key == null) {
            throw new JSONException("Null key.");
        }
        if (value != null) {
            value = processValue(key, value, jsonConfig);
            _setInternal(key, value, jsonConfig);
        } else {
            remove(key);
        }
        return this;
    }

    /**
     * Put a key/value pair in the JSONObject, but only if the key and the value
     * are both non-null.
     *
     * @param key   A key string.
     * @param value An object which is the value. It should be of one of these
     *              types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
     *              String, or the JSONNull object.
     *
     * @return this.
     *
     * @throws JSONException If the value is a non-finite number.
     */
    public JSONObject elementOpt(String key, Object value) {
        return elementOpt(key, value, new JsonConfig());
    }

    /**
     * Put a key/value pair in the JSONObject, but only if the key and the value
     * are both non-null.
     *
     * @param key   A key string.
     * @param value An object which is the value. It should be of one of these
     *              types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
     *              String, or the JSONNull object.
     *
     * @return this.
     *
     * @throws JSONException If the value is a non-finite number.
     */
    public JSONObject elementOpt(String key, Object value, JsonConfig jsonConfig) {
        verifyIsNull();
        if (key != null && value != null) {
            element(key, value, jsonConfig);
        }
        return this;
    }

    /**
     * Get the value object associated with a key.
     *
     * @param key A key string.
     *
     * @return The object associated with the key.
     *
     * @throws JSONException if this.isNull() returns true.
     */
    public Object get(String key) {
        verifyIsNull();
        return this.properties.get(key);
    }

    /**
     * Get the boolean value associated with a key.
     *
     * @param key A key string.
     *
     * @return The truth.
     *
     * @throws JSONException if the value is not a Boolean or the String "true"
     *                       or "false".
     */
    public boolean getBoolean(String key) {
        verifyIsNull();
        Object o = get(key);
        if (o != null) {
            if (o.equals(Boolean.FALSE)
                || (o instanceof String && ((String) o).equalsIgnoreCase("false"))) {
                return false;
            } else if (o.equals(Boolean.TRUE)
                || (o instanceof String && ((String) o).equalsIgnoreCase("true"))) {
                return true;
            }
        }
        throw new JSONException("JSONObject[" + JSONUtils.quote(key) + "] is not a Boolean.");
    }

    /**
     * Get the double value associated with a key.
     *
     * @param key A key string.
     *
     * @return The numeric value.
     *
     * @throws JSONException if the key is not found or if the value is not a
     *                       Number object and cannot be converted to a number.
     */
    public double getDouble(String key) {
        verifyIsNull();
        Object o = get(key);
        if (o != null) {
            try {
                return o instanceof Number ? ((Number) o).doubleValue()
                    : Double.parseDouble((String) o);
            } catch (Exception e) {
                throw new JSONException("JSONObject[" + JSONUtils.quote(key) + "] is not a number.");
            }
        }
        throw new JSONException("JSONObject[" + JSONUtils.quote(key) + "] is not a number.");
    }

    /**
     * Get the int value associated with a key. If the number value is too large
     * for an int, it will be clipped.
     *
     * @param key A key string.
     *
     * @return The integer value.
     *
     * @throws JSONException if the key is not found or if the value cannot be
     *                       converted to an integer.
     */
    public int getInt(String key) {
        verifyIsNull();
        Object o = get(key);
        if (o != null) {
            return o instanceof Number ? ((Number) o).intValue() : (int) getDouble(key);
        }
        throw new JSONException("JSONObject[" + JSONUtils.quote(key) + "] is not a number.");
    }

    /**
     * Get the JSONArray value associated with a key.
     *
     * @param key A key string.
     *
     * @return A JSONArray which is the value.
     *
     * @throws JSONException if the key is not found or if the value is not a
     *                       JSONArray.
     */
    public JSONArray getJSONArray(String key) {
        verifyIsNull();
        Object o = get(key);
        if (o != null && o instanceof JSONArray) {
            return (JSONArray) o;
        }
        throw new JSONException("JSONObject[" + JSONUtils.quote(key) + "] is not a JSONArray.");
    }

    /**
     * Get the JSONObject value associated with a key.
     *
     * @param key A key string.
     *
     * @return A JSONObject which is the value.
     *
     * @throws JSONException if the key is not found or if the value is not a
     *                       JSONObject.
     */
    public JSONObject getJSONObject(String key) {
        verifyIsNull();
        Object o = get(key);
        if (JSONNull.getInstance()
            .equals(o)) {
            return new JSONObject(true);
        } else if (o instanceof JSONObject) {
            return (JSONObject) o;
        }
        throw new JSONException("JSONObject[" + JSONUtils.quote(key) + "] is not a JSONObject.");
    }

    /**
     * Get the long value associated with a key. If the number value is too long
     * for a long, it will be clipped.
     *
     * @param key A key string.
     *
     * @return The long value.
     *
     * @throws JSONException if the key is not found or if the value cannot be
     *                       converted to a long.
     */
    public long getLong(String key) {
        verifyIsNull();
        Object o = get(key);
        if (o != null) {
            return o instanceof Number ? ((Number) o).longValue() : (long) getDouble(key);
        }
        throw new JSONException("JSONObject[" + JSONUtils.quote(key) + "] is not a number.");
    }

    /**
     * Get the string associated with a key.
     *
     * @param key A key string.
     *
     * @return A string which is the value.
     *
     * @throws JSONException if the key is not found.
     */
    public String getString(String key) {
        verifyIsNull();
        Object o = get(key);
        if (o != null) {
            return o.toString();
        }
        throw new JSONException("JSONObject[" + JSONUtils.quote(key) + "] not found.");
    }

    /**
     * Determine if the JSONObject contains a specific key.
     *
     * @param key A key string.
     *
     * @return true if the key exists in the JSONObject.
     */
    public boolean has(String key) {
        verifyIsNull();
        return this.properties.containsKey(key);
    }

    public int hashCode() {
        int hashcode = 19;
        if (isNullObject()) {
            return hashcode + JSONNull.getInstance()
                .hashCode();
        }
        for (Iterator entries = properties.entrySet()
            .iterator(); entries.hasNext(); ) {
            Map.Entry entry = (Map.Entry) entries.next();
            Object key = entry.getKey();
            Object value = entry.getValue();
            hashcode += key.hashCode() + JSONUtils.hashCode(value);
        }
        return hashcode;
    }

    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj == null) {
            return false;
        }

        if (!(obj instanceof JSONObject)) {
            return false;
        }

        JSONObject other = (JSONObject) obj;

        if (isNullObject()) {
            return other.isNullObject();
        } else {
            if (other.isNullObject()) {
                return false;
            }
        }

        if (other.size() != size()) {
            return false;
        }

        for (Iterator keys = properties.keySet()
            .iterator(); keys.hasNext(); ) {
            String key = (String) keys.next();
            if (!other.properties.containsKey(key)) {
                return false;
            }
            Object o1 = properties.get(key);
            Object o2 = other.properties.get(key);

            if (JSONNull.getInstance()
                .equals(o1)) {
                if (JSONNull.getInstance()
                    .equals(o2)) {
                    continue;
                } else {
                    return false;
                }
            } else {
                if (JSONNull.getInstance()
                    .equals(o2)) {
                    return false;
                }
            }

            if (o1 instanceof String && o2 instanceof JSONFunction) {
                if (!o1.equals(String.valueOf(o2))) {
                    return false;
                }
            } else if (o1 instanceof JSONFunction && o2 instanceof String) {
                if (!o2.equals(String.valueOf(o1))) {
                    return false;
                }
            } else if (o1 instanceof JSONObject && o2 instanceof JSONObject) {
                if (!o1.equals(o2)) {
                    return false;
                }
            } else if (o1 instanceof JSONArray && o2 instanceof JSONArray) {
                if (!o1.equals(o2)) {
                    return false;
                }
            } else if (o1 instanceof JSONFunction && o2 instanceof JSONFunction) {
                if (!o1.equals(o2)) {
                    return false;
                }
            } else {
                if (o1 instanceof String) {
                    if (!o1.equals(String.valueOf(o2))) {
                        return false;
                    }
                } else if (o2 instanceof String) {
                    if (!o2.equals(String.valueOf(o1))) {
                        return false;
                    }
                } else {
                    Morpher m1 = JSONUtils.getMorpherRegistry()
                        .getMorpherFor(o1.getClass());
                    Morpher m2 = JSONUtils.getMorpherRegistry()
                        .getMorpherFor(o2.getClass());
                    if (m1 != null && m1 != IdentityObjectMorpher.getInstance()) {
                        if (!o1.equals(JSONUtils.getMorpherRegistry()
                            .morph(o1.getClass(), o2))) {
                            return false;
                        }
                    } else if (m2 != null && m2 != IdentityObjectMorpher.getInstance()) {
                        if (!JSONUtils.getMorpherRegistry()
                            .morph(o1.getClass(), o1)
                            .equals(o2)) {
                            return false;
                        }
                    } else {
                        if (!o1.equals(o2)) {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }

    /**
     * Make a JSON text of this JSONObject. For compactness, no whitespace is
     * added. If this would not result in a syntactically correct JSON text, then
     * null will be returned instead.
     * <p/>
     * Warning: This method assumes that the data structure is acyclical.
     *
     * @return a printable, displayable, portable, transmittable representation
     * of the object, beginning with <code>{</code>&nbsp;<small>(left
     * brace)</small> and ending with <code>}</code>&nbsp;<small>(right
     * brace)</small>.
     */
    public String toString() {
        if (isNullObject()) {
            return JSONNull.getInstance()
                .toString();
        }
        try {
            Iterator keys = keys();
            StringBuffer sb = new StringBuffer("{");

            while (keys.hasNext()) {
                if (sb.length() > 1) {
                    sb.append(',');
                }
                Object o = keys.next();
                sb.append(JSONUtils.quote(o.toString()));
                sb.append(':');
                sb.append(JSONUtils.valueToString(this.properties.get(o)));
            }
            sb.append('}');
            return sb.toString();
        } catch (Exception e) {
            return null;
        }
    }

    public boolean isArray() {
        return false;
    }

    public boolean isEmpty() {
        // verifyIsNull();
        return this.properties.isEmpty();
    }

    /**
     * Get the number of keys stored in the JSONObject.
     *
     * @return The number of keys in the JSONObject.
     */
    public int size() {
        // verifyIsNull();
        return this.properties.size();
    }

    /**
     * Make a prettyprinted JSON text of this JSONObject.
     * <p/>
     * Warning: This method assumes that the data structure is acyclical.
     *
     * @param indentFactor The number of spaces to add to each level of
     *                     indentation.
     *
     * @return a printable, displayable, portable, transmittable representation
     * of the object, beginning with <code>{</code>&nbsp;<small>(left
     * brace)</small> and ending with <code>}</code>&nbsp;<small>(right
     * brace)</small>.
     *
     * @throws JSONException If the object contains an invalid number.
     */
    public String toString(int indentFactor) {
        if (isNullObject()) {
            return JSONNull.getInstance()
                .toString();
        }
        if (indentFactor == 0) {
            return this.toString();
        }
        return toString(indentFactor, 0);
    }

    /**
     * Make a prettyprinted JSON text of this JSONObject.
     * <p/>
     * Warning: This method assumes that the data structure is acyclical.
     *
     * @param indentFactor The number of spaces to add to each level of
     *                     indentation.
     * @param indent       The indentation of the top level.
     *
     * @return a printable, displayable, transmittable representation of the
     * object, beginning with <code>{</code>&nbsp;<small>(left brace)</small>
     * and ending with <code>}</code>&nbsp;<small>(right brace)</small>.
     *
     * @throws JSONException If the object contains an invalid number.
     */
    public String toString(int indentFactor, int indent) {
        if (isNullObject()) {
            return JSONNull.getInstance()
                .toString();
        }
        int i;
        int n = size();
        if (n == 0) {
            return "{}";
        }
        if (indentFactor == 0) {
            return this.toString();
        }
        Iterator keys = keys();
        StringBuffer sb = new StringBuffer("{");
        int newindent = indent + indentFactor;
        Object o;
        if (n == 1) {
            o = keys.next();
            sb.append(JSONUtils.quote(o.toString()));
            sb.append(": ");
            sb.append(JSONUtils.valueToString(this.properties.get(o), indentFactor, indent));
        } else {
            while (keys.hasNext()) {
                o = keys.next();
                if (sb.length() > 1) {
                    sb.append(",\n");
                } else {
                    sb.append('\n');
                }
                for (i = 0; i < newindent; i += 1) {
                    sb.append(' ');
                }
                sb.append(JSONUtils.quote(o.toString()));
                sb.append(": ");
                sb.append(JSONUtils.valueToString(this.properties.get(o), indentFactor, newindent));
            }
            if (sb.length() > 1) {
                sb.append('\n');
                for (i = 0; i < indent; i += 1) {
                    sb.append(' ');
                }
            }
            for (i = 0; i < indent; i += 1) {
                sb.insert(0, ' ');
            }
        }
        sb.append('}');
        return sb.toString();
    }

    /**
     * Returs if this object is a null JSONObject.
     */
    public boolean isNullObject() {
        return nullObject;
    }

    /**
     * Get an enumeration of the keys of the JSONObject.
     *
     * @return An iterator of the keys.
     */
    public Iterator keys() {
        verifyIsNull();
        return keySet().iterator();
    }

    /**
     * Produce a JSONArray containing the names of the elements of this
     * JSONObject.
     *
     * @return A JSONArray containing the key strings, or null if the JSONObject
     * is empty.
     */
    public JSONArray names() {
        verifyIsNull();
        JSONArray ja = new JSONArray();
        Iterator keys = keys();
        while (keys.hasNext()) {
            ja.element(keys.next());
        }
        return ja;
    }

    /**
     * Produce a JSONArray containing the names of the elements of this
     * JSONObject.
     *
     * @return A JSONArray containing the key strings, or null if the JSONObject
     * is empty.
     */
    public JSONArray names(JsonConfig jsonConfig) {
        verifyIsNull();
        JSONArray ja = new JSONArray();
        Iterator keys = keys();
        while (keys.hasNext()) {
            ja.element(keys.next(), jsonConfig);
        }
        return ja;
    }

    /**
     * Get an optional value associated with a key.
     *
     * @param key A key string.
     *
     * @return An object which is the value, or null if there is no value.
     */
    public Object opt(String key) {
        verifyIsNull();
        return key == null ? null : this.properties.get(key);
    }

    /**
     * Get an optional boolean associated with a key. It returns false if there
     * is no such key, or if the value is not Boolean.TRUE or the String "true".
     *
     * @param key A key string.
     *
     * @return The truth.
     */
    public boolean optBoolean(String key) {
        verifyIsNull();
        return optBoolean(key, false);
    }

    /**
     * Get an optional boolean associated with a key. It returns the defaultValue
     * if there is no such key, or if it is not a Boolean or the String "true" or
     * "false" (case insensitive).
     *
     * @param key          A key string.
     * @param defaultValue The default.
     *
     * @return The truth.
     */
    public boolean optBoolean(String key, boolean defaultValue) {
        verifyIsNull();
        try {
            return getBoolean(key);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /**
     * Get an optional double associated with a key, or NaN if there is no such
     * key or if its value is not a number. If the value is a string, an attempt
     * will be made to evaluate it as a number.
     *
     * @param key A string which is the key.
     *
     * @return An object which is the value.
     */
    public double optDouble(String key) {
        verifyIsNull();
        return optDouble(key, Double.NaN);
    }

    /**
     * Get an optional double associated with a key, or the defaultValue if there
     * is no such key or if its value is not a number. If the value is a string,
     * an attempt will be made to evaluate it as a number.
     *
     * @param key          A key string.
     * @param defaultValue The default.
     *
     * @return An object which is the value.
     */
    public double optDouble(String key, double defaultValue) {
        verifyIsNull();
        try {
            Object o = opt(key);
            return o instanceof Number ? ((Number) o).doubleValue()
                : Double.parseDouble((String) o);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /**
     * Get an optional int value associated with a key, or zero if there is no
     * such key or if the value is not a number. If the value is a string, an
     * attempt will be made to evaluate it as a number.
     *
     * @param key A key string.
     *
     * @return An object which is the value.
     */
    public int optInt(String key) {
        verifyIsNull();
        return optInt(key, 0);
    }

    /**
     * Get an optional int value associated with a key, or the default if there
     * is no such key or if the value is not a number. If the value is a string,
     * an attempt will be made to evaluate it as a number.
     *
     * @param key          A key string.
     * @param defaultValue The default.
     *
     * @return An object which is the value.
     */
    public int optInt(String key, int defaultValue) {
        verifyIsNull();
        try {
            return getInt(key);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /**
     * Get an optional JSONArray associated with a key. It returns null if there
     * is no such key, or if its value is not a JSONArray.
     *
     * @param key A key string.
     *
     * @return A JSONArray which is the value.
     */
    public JSONArray optJSONArray(String key) {
        verifyIsNull();
        Object o = opt(key);
        return o instanceof JSONArray ? (JSONArray) o : null;
    }

    /**
     * Get an optional JSONObject associated with a key. It returns null if there
     * is no such key, or if its value is not a JSONObject.
     *
     * @param key A key string.
     *
     * @return A JSONObject which is the value.
     */
    public JSONObject optJSONObject(String key) {
        verifyIsNull();
        Object o = opt(key);
        return o instanceof JSONObject ? (JSONObject) o : null;
    }

    /**
     * Get an optional long value associated with a key, or zero if there is no
     * such key or if the value is not a number. If the value is a string, an
     * attempt will be made to evaluate it as a number.
     *
     * @param key A key string.
     *
     * @return An object which is the value.
     */
    public long optLong(String key) {
        verifyIsNull();
        return optLong(key, 0);
    }

    /**
     * Get an optional long value associated with a key, or the default if there
     * is no such key or if the value is not a number. If the value is a string,
     * an attempt will be made to evaluate it as a number.
     *
     * @param key          A key string.
     * @param defaultValue The default.
     *
     * @return An object which is the value.
     */
    public long optLong(String key, long defaultValue) {
        verifyIsNull();
        try {
            return getLong(key);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /**
     * Get an optional string associated with a key. It returns an empty string
     * if there is no such key. If the value is not a string and is not null,
     * then it is coverted to a string.
     *
     * @param key A key string.
     *
     * @return A string which is the value.
     */
    public String optString(String key) {
        verifyIsNull();
        return optString(key, "");
    }

    /**
     * Get an optional string associated with a key. It returns the defaultValue
     * if there is no such key.
     *
     * @param key          A key string.
     * @param defaultValue The default.
     *
     * @return A string which is the value.
     */
    public String optString(String key, String defaultValue) {
        verifyIsNull();
        Object o = opt(key);
        return o != null ? o.toString() : defaultValue;
    }

    public void putAll(Map map, JsonConfig jsonConfig) {
        if (map instanceof JSONObject) {
            for (Iterator entries = map.entrySet()
                .iterator(); entries.hasNext(); ) {
                Map.Entry entry = (Map.Entry) entries.next();
                String key = (String) entry.getKey();
                Object value = entry.getValue();
                this.properties.put(key, value);
            }
        } else {
            for (Iterator entries = map.entrySet()
                .iterator(); entries.hasNext(); ) {
                Map.Entry entry = (Map.Entry) entries.next();
                String key = String.valueOf(entry.getKey());
                Object value = entry.getValue();
                element(key, value, jsonConfig);
            }
        }
    }

    /**
     * Remove a name and its value, if present.
     *
     * @param key The name to be removed.
     *
     * @return The value that was associated with the name, or null if there was
     * no value.
     */
    public Object remove(String key) {
        verifyIsNull();
        return this.properties.remove(key);
    }

    /**
     * Produce a JSONArray containing the values of the members of this
     * JSONObject.
     *
     * @param names A JSONArray containing a list of key strings. This determines
     *              the sequence of the values in the result.
     *
     * @return A JSONArray of values.
     *
     * @throws JSONException If any of the values are non-finite numbers.
     */
    public JSONArray toJSONArray(JSONArray names) {
        verifyIsNull();
        if (names == null || names.size() == 0) {
            return null;
        }
        JSONArray ja = new JSONArray();
        for (int i = 0; i < names.size(); i += 1) {
            ja.element(this.opt(names.getString(i)));
        }
        return ja;
    }

    private JSONObject _accumulate(String key, Object value, JsonConfig jsonConfig) {
        if (isNullObject()) {
            throw new JSONException("Can't accumulate on null object");
        }

        if (!has(key)) {
            setInternal(key, value, jsonConfig);
        } else {
            Object o = opt(key);
            if (o instanceof JSONArray) {
                ((JSONArray) o).element(value, jsonConfig);
            } else {
                setInternal(key, new JSONArray().element(o)
                    .element(value, jsonConfig), jsonConfig);
            }
        }

        return this;
    }

    @Override
    protected Object _processValue(Object value, JsonConfig jsonConfig) {
        if (value instanceof JSONTokener) {
            return Impl.fromJSONTokener((JSONTokener) value, jsonConfig);
        } else if (value != null && Enum.class.isAssignableFrom(value.getClass())) {
            return ((Enum) value).name();
        }
        return super._processValue(value, jsonConfig);
    }

    /**
     * Write the contents of the JSONObject as JSON text to a writer. For
     * compactness, no whitespace is added.
     * <p/>
     * Warning: This method assumes that the data structure is acyclical.
     *
     * @throws JSONException
     */
    @Override
    protected void write(Writer writer, WritingVisitor visitor) throws IOException {
        try {
            if (isNullObject()) {
                writer.write(JSONNull.getInstance()
                    .toString());
            }

            boolean b = false;
            Iterator keys = visitor.keySet(this).iterator();
            writer.write('{');

            while (keys.hasNext()) {
                if (b) {
                    writer.write(',');
                }
                Object k = keys.next();
                writer.write(JSONUtils.quote(k.toString()));
                writer.write(':');
                Object v = this.properties.get(k);
                if (v instanceof JSON) {
                    visitor.on((JSON) v, writer);
                } else {
                    visitor.on(v, writer);
                }
                b = true;
            }
            writer.write('}');
        } catch (IOException e) {
            throw new JSONException(e);
        }
    }

    /**
     * Put a key/value pair in the JSONObject.
     *
     * @param key   A key string.
     * @param value An object which is the value. It should be of one of these
     *              types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
     *              String, or the JSONNull object.
     *
     * @return this.
     *
     * @throws JSONException If the value is non-finite number or if the key is
     *                       null.
     */
    private JSONObject _setInternal(String key, Object value, JsonConfig jsonConfig) {
        verifyIsNull();
        if (key == null) {
            throw new JSONException("Null key.");
        }

        if (JSONUtils.isString(value) && JSONUtils.mayBeJSON(String.valueOf(value))) {
            this.properties.put(key, value);
        } else {
         /*
         Object jo = _processValue( value, jsonConfig );
         if( CycleDetectionStrategy.IGNORE_PROPERTY_OBJ == jo
               || CycleDetectionStrategy.IGNORE_PROPERTY_ARR == jo ){
            // do nothing
         }else{
            this.properties.put( key, jo );
         }
         */
            if (CycleDetectionStrategy.IGNORE_PROPERTY_OBJ != value
                && CycleDetectionStrategy.IGNORE_PROPERTY_ARR != value) {
                this.properties.put(key, value);
            }
        }

        return this;
    }

    private Object processValue(Object value, JsonConfig jsonConfig) {
        if (value != null) {
            JsonValueProcessor processor = jsonConfig.findJsonValueProcessor(value.getClass());
            if (processor != null) {
                value = processor.processObjectValue(null, value, jsonConfig);
                if (!JsonVerifier.isValidJsonValue(value)) {
                    throw new JSONException("Value is not a valid JSON value. " + value);
                }
            }
        }
        return _processValue(value, jsonConfig);
    }

    private Object processValue(String key, Object value, JsonConfig jsonConfig) {
        if (value != null) {
            JsonValueProcessor processor = jsonConfig.findJsonValueProcessor(value.getClass(), key);
            if (processor != null) {
                value = processor.processObjectValue(null, value, jsonConfig);
                if (!JsonVerifier.isValidJsonValue(value)) {
                    throw new JSONException("Value is not a valid JSON value. " + value);
                }
            }
        }
        return _processValue(value, jsonConfig);
    }

    /**
     * Put a key/value pair in the JSONObject.
     *
     * @param key   A key string.
     * @param value An object which is the value. It should be of one of these
     *              types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
     *              String, or the JSONNull object.
     *
     * @return this.
     *
     * @throws JSONException If the value is non-finite number or if the key is
     *                       null.
     */
    private JSONObject setInternal(String key, Object value, JsonConfig jsonConfig) {
        return _setInternal(key, processValue(key, value, jsonConfig), jsonConfig);
    }

    /**
     * Checks if this object is a "null" object.
     */
    private void verifyIsNull() {
        if (isNullObject()) {
            throw new JSONException("null object");
        }
    }

    private interface Property {
        boolean isWritable();

        Class getPropertyType();

        String name();

        void set(Object bean, Object value, JsonConfig jsonConfig) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException;
    }

    private static class PropertyOnMap implements Property {
        private final String name;

        private PropertyOnMap(String name) {
            this.name = name;
        }

        public boolean isWritable() {
            return true;
        }

        public String name() {
            return name;
        }

        public Class getPropertyType() {
            return Object.class;
        }

        public void set(Object bean, Object value, JsonConfig jsonConfig) {
            ((Map) bean).put(name, value);
        }
    }

    private static class MethodProperty implements Property {
        private final PropertyDescriptor pd;

        private MethodProperty(PropertyDescriptor pd) {
            this.pd = pd;
        }

        public boolean isWritable() {
            return pd.getWriteMethod() != null;
        }

        public String name() {
            return pd.getName();
        }

        public Class getPropertyType() {
            return pd.getPropertyType();
        }

        public void set(Object bean, Object value, JsonConfig jsonConfig) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
            PropertyUtils.setSimpleProperty(bean, pd.getName(), value);
        }
    }

    private static class FieldProperty implements Property {
        private final Field f;

        private FieldProperty(Field f) {
            this.f = f;
        }

        public boolean isWritable() {
            return true;
        }

        public Class getPropertyType() {
            return f.getType();
        }

        public String name() {
            return f.getName();
        }

        public void set(Object bean, Object value, JsonConfig jsonConfig) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
            f.set(bean, value);
        }
    }

    /**
     * Uses {@link PropertySetStrategy} instead of the normal set method.
     */
    private static class PropertySetterStrategyDecorator implements Property {
        private final Property inner;
        private final PropertySetStrategy strategy;

        private PropertySetterStrategyDecorator(Property inner, PropertySetStrategy strategy) {
            this.inner = inner;
            this.strategy = strategy;
        }

        public boolean isWritable() {
            return inner.isWritable();
        }

        public Class getPropertyType() {
            return inner.getPropertyType();
        }

        public String name() {
            return inner.name();
        }

        public void set(Object bean, Object value, JsonConfig jsonConfig) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
            strategy.setProperty(bean, name(), value, jsonConfig);
        }
    }
}