/*
 * This file is part of Nuts Framework.
 * Copyright(C) 2009-2012 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 *
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.core.json;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
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;

import net.sf.json.JSONFunction;
import net.sf.json.JSONNull;
import net.sf.json.JsonConfig;
import net.sf.json.processors.JsonBeanProcessor;
import net.sf.json.processors.JsonValueProcessor;
import net.sf.json.processors.JsonVerifier;
import net.sf.json.processors.PropertyNameProcessor;
import net.sf.json.regexp.RegexpUtils;
import net.sf.json.util.JSONUtils;
import net.sf.json.util.PropertyFilter;
import net.sf.json.util.PropertySetStrategy;

import nuts.core.beans.BeanHandler;
import nuts.core.beans.BeanHandlerFactory;
import nuts.core.beans.PropertyUtils;
import nuts.core.lang.StringUtils;

import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

public class JsonSerializer extends AbstractSerializer {
	private final Log log = LogFactory.getLog(JsonSerializer.class);

   /**
    * 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 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 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 != null && object.getClass()
            .isAnnotation()) ){
         throw new JSONException( "'object' is an Annotation." );
      }else if( object instanceof JSONObject ){
         return _fromJSONObject( (JSONObject) object, jsonConfig );
      }else if( object instanceof JSONTokener ){
         return _fromJSONTokener( (JSONTokener) object, jsonConfig );
      }else if( object instanceof Map ){
         return _fromMap( (Map) object, jsonConfig );
      }else if( object instanceof String ){
         return _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 _fromBean( object, jsonConfig );
      }
   }

   /**
    * Creates a bean from a JSONObject, with a specific target class.<br>
    */
   public 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 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.
    */
   @SuppressWarnings("unchecked")
   public Object toBean( JSONObject jsonObject, JsonConfig jsonConfig ) {
      if( jsonObject == null || jsonObject.equals(JSONObject.NULL)) {
         return null;
      }

      Class beanClass = jsonConfig.getRootClass();
      Map classMap = jsonConfig.getClassMap();

      if( beanClass == null ){
         beanClass = LinkedHashMap.class;
      }
      if( classMap == null ){
         classMap = Collections.EMPTY_MAP;
      }

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

      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{
            if( Map.class.isAssignableFrom( beanClass ) ){
               // no type info available for conversion
               if( JSONUtils.isNull( value ) ){
                  setProperty( bean, key, value, jsonConfig );
               }else if( value instanceof JSONArray ){
                  setProperty( bean, key, 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( jsonConfig.isHandleJettisonEmptyElement() && "".equals( value ) ){
                     setProperty( bean, key, null, jsonConfig );
                  }else{
                     setProperty( bean, key, value, jsonConfig );
                  }
               }else{
                  Class targetClass = resolveClass(classMap, key, name, type);
                  JsonConfig jsc = jsonConfig.copy();
                  jsc.setRootClass( targetClass );
                  jsc.setClassMap( classMap );
                  if( targetClass != null ){
                     setProperty( bean, key, toBean( (JSONObject) value, jsc ), jsonConfig );
                  }else{
                     setProperty( bean, key, toBean( (JSONObject) value ), jsonConfig );
                  }
               }
            }else{
            	BeanHandler bh = BeanHandlerFactory.getInstance().getBeanHandler(bean.getClass());
               if( bh != null && !bh.canReadProperty(bean, key)){
                  log.info( "Property '" + key + "' of "+ bean.getClass()+" has no write method. SKIPPED." );
                  continue;
               }

               if( bh != null ){
                  Class targetType = bh.getPropertyType(bean, key);
                  if( !JSONUtils.isNull( value ) ){
                     if( value instanceof JSONArray ){
                        if( List.class.isAssignableFrom(targetType) ){
                           setProperty( bean, key, convertPropertyValueToCollection( key, value,
                                 jsonConfig, name, classMap, targetType), jsonConfig );
                        }else if( Set.class.isAssignableFrom(targetType) ){
                           setProperty( bean, key, convertPropertyValueToCollection( key, value,
                                 jsonConfig, name, classMap, targetType), jsonConfig );
                        }else{
                           setProperty( bean, key, 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( bh != null ){
                           if( jsonConfig.isHandleJettisonEmptyElement() && "".equals( value ) ){
                              setProperty( bean, key, null, jsonConfig );
                           }else if( !targetType.isInstance( value ) ){
                              setProperty( bean, key, morphPropertyValue( key, value, type,
                                    targetType ), jsonConfig );
                           }else{
                              setProperty( bean, key, value, jsonConfig );
                           }
                        }else if( beanClass == null || bean instanceof Map ){
                           setProperty( bean, key, 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 = resolveClass(classMap, key, name, type);
                           JsonConfig jsc = jsonConfig.copy();
                           jsc.setRootClass( newTargetClass );
                           jsc.setClassMap( classMap );
                           if( targetType.isArray() ){
                              setProperty( bean, key, JSONArray.toArray( array, jsc ), jsonConfig );
                           }else if( JSONArray.class.isAssignableFrom( targetType ) ){
                              setProperty( bean, key, array, jsonConfig );
                           }else if( List.class.isAssignableFrom( targetType )
                                 || Set.class.isAssignableFrom( targetType ) ){
                              jsc.setCollectionType( targetType );
                              setProperty( bean, key, JSONArray.toCollection( array, jsc ),
                                    jsonConfig );
                           }else{
                              setProperty( bean, key, toBean( (JSONObject) value, jsc ), jsonConfig );
                           }
                        }else{
                           if( targetType == Object.class || targetType.isInterface() ) {
                              Class targetTypeCopy = targetType;
                              targetType = findTargetClass( key, classMap );
                              targetType = targetType == null ? findTargetClass( name, classMap )
                                    : targetType;
                              targetType = targetType == null && targetTypeCopy.isInterface() ? targetTypeCopy
                                    : targetType;
                           }
                           JsonConfig jsc = jsonConfig.copy();
                           jsc.setRootClass( targetType );
                           jsc.setClassMap( classMap );
                           setProperty( bean, key, toBean( (JSONObject) value, jsc ), jsonConfig );
                        }
                     }
                  }else{
                     if( type.isPrimitive() ){
                        // assume assigned default value
                        log.warn( "Tried to assign null value to " + key + ":" + type.getName() );
                        setProperty( bean, key, JSONUtils.getMorpherRegistry()
                              .morph( type, null ), jsonConfig );
                     }else{
                        setProperty( bean, key, null, jsonConfig );
                     }
                  }
               }else{
                  if( !JSONUtils.isNull( value ) ){
                     if( value instanceof JSONArray ){
                        setProperty( bean, key, 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() ){
                           setProperty( bean, key, 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 = resolveClass(classMap, key, name, type);
                           JsonConfig jsc = jsonConfig.copy();
                           jsc.setRootClass( newTargetClass );
                           jsc.setClassMap( classMap );
                           setProperty( bean, key, toBean( (JSONObject) value, jsc ), jsonConfig );
                        }else{
                           setProperty( bean, key, value, jsonConfig );
                        }
                     }
                  }else{
                     if( type.isPrimitive() ){
                        // assume assigned default value
                        log.warn( "Tried to assign null value to " + key + ":" + type.getName() );
                        setProperty( bean, key, JSONUtils.getMorpherRegistry()
                              .morph( type, null ), jsonConfig );
                     }else{
                        setProperty( bean, key, null, jsonConfig );
                     }
                  }
               }
            }
         }catch( JSONException jsone ){
            throw jsone;
         }catch( Exception e ){
            throw new JSONException( "Error while setting property=" + name + " type " + type, e );
         }
      }

      return bean;
   }

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

      Class rootClass = root.getClass();
      if( rootClass.isInterface() ){
         throw new JSONException( "Root bean is an interface. " + 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( root, name, value ) ){
            continue;
         }
         String key = JSONUtils.convertToJavaIdentifier( name, jsonConfig );
         try{
            PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor( root, key );
            if( pd != null && pd.getWriteMethod() == null ){
               log.info( "Property '" + key + "' of "+ root.getClass()+" has no write method. SKIPPED." );
               continue;
            }

            if( !JSONUtils.isNull( value ) ){
               if( value instanceof JSONArray ){
                  if( pd == null || List.class.isAssignableFrom( pd.getPropertyType() ) ){
                     Class targetClass = resolveClass(classMap, key, name, type);
                     Object newRoot = jsonConfig.getNewBeanInstanceStrategy()
                           .newInstance( targetClass, null );
                     List list = JSONArray.toList( (JSONArray) value, newRoot, jsonConfig );
                     setProperty( root, key, list, jsonConfig );
                  }else{
                     Class innerType = JSONUtils.getInnerComponentType( pd.getPropertyType() );
                     Class targetInnerType = findTargetClass( key, classMap );
                     if( innerType.equals( Object.class ) && targetInnerType != null
                           && !targetInnerType.equals( Object.class ) ){
                        innerType = targetInnerType;
                     }
                     Object newRoot = jsonConfig.getNewBeanInstanceStrategy()
                           .newInstance( innerType, null );
                     Object array = JSONArray.toArray( (JSONArray) value, newRoot, jsonConfig );
                     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( pd.getPropertyType() ) ){
                        if( !pd.getPropertyType()
                              .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 );
                        }
                     }
                     setProperty( root, key, array, 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 ) ){
                        setProperty( root, key, null, jsonConfig );
                     }else if( !pd.getPropertyType()
                           .isInstance( value ) ){
                        Morpher morpher = JSONUtils.getMorpherRegistry()
                              .getMorpherFor( pd.getPropertyType() );
                        if( IdentityObjectMorpher.getInstance()
                              .equals( morpher ) ){
                           log.warn( "Can't transform property '" + key + "' from "
                                 + type.getName() + " into " + pd.getPropertyType()
                                       .getName() + ". Will register a default BeanMorpher" );
                           JSONUtils.getMorpherRegistry()
                                 .registerMorpher(
                                       new BeanMorpher( pd.getPropertyType(),
                                             JSONUtils.getMorpherRegistry() ) );
                        }
                        setProperty( root, key, JSONUtils.getMorpherRegistry()
                              .morph( pd.getPropertyType(), value ), jsonConfig );
                     }else{
                        setProperty( root, key, value, jsonConfig );
                     }
                  }else if( root instanceof Map ){
                     setProperty( root, key, value, jsonConfig );
                  }else{
                     log.warn( "Tried to assign property " + key + ":" + type.getName()
                           + " to bean of class " + root.getClass()
                                 .getName() );
                  }
               }else{
                  if( pd != null ){
                     Class targetClass = pd.getPropertyType();
                     if( jsonConfig.isHandleJettisonSingleElementArray() ){
                        JSONArray array = new JSONArray().element( value, jsonConfig );
                        Class newTargetClass = resolveClass(classMap, key, name, type);
                        Object newRoot = jsonConfig.getNewBeanInstanceStrategy()
                              .newInstance( newTargetClass, (JSONObject) value );
                        if( targetClass.isArray() ){
                           setProperty( root, key, JSONArray.toArray( array, newRoot, jsonConfig ),
                                 jsonConfig );
                        }else if( Collection.class.isAssignableFrom( targetClass ) ){
                           setProperty( root, key, JSONArray.toList( array, newRoot, jsonConfig ),
                                 jsonConfig );
                        }else if( JSONArray.class.isAssignableFrom( targetClass ) ){
                           setProperty( root, key, array, jsonConfig );
                        }else{
                           setProperty( root, key,
                                 toBean( (JSONObject) value, newRoot, jsonConfig ), jsonConfig );
                        }
                     }else{
                        if( targetClass == Object.class ){
                            targetClass = resolveClass(classMap, key, name, type);
                            if(targetClass == null) {
                                targetClass = Object.class;
                            }
                        }
                        Object newRoot = jsonConfig.getNewBeanInstanceStrategy()
                              .newInstance( targetClass, (JSONObject) value );
                        setProperty( root, key, toBean( (JSONObject) value, newRoot, jsonConfig ),
                              jsonConfig );
                     }
                  }else if( root instanceof Map ){
                     Class targetClass = findTargetClass( key, classMap );
                     targetClass = targetClass == null ? findTargetClass( name, classMap )
                           : targetClass;
                     Object newRoot = jsonConfig.getNewBeanInstanceStrategy()
                           .newInstance( targetClass, null );
                     setProperty( root, key, toBean( (JSONObject) value, newRoot, jsonConfig ),
                           jsonConfig );
                  }else{
                     log.warn( "Tried to assign property " + key + ":" + type.getName()
                           + " to bean of class " + rootClass.getName() );
                  }
               }
            }else{
               if( type.isPrimitive() ){
                  // assume assigned default value
                  log.warn( "Tried to assign null value to " + key + ":" + type.getName() );
                  setProperty( root, key, JSONUtils.getMorpherRegistry()
                        .morph( type, null ), jsonConfig );
               }else{
                  setProperty( root, key, null, jsonConfig );
               }
            }
         }catch( JSONException jsone ){
            throw jsone;
         }catch( Exception e ){
            throw new JSONException( "Error while setting property=" + name + " type " + type, e );
         }
      }

      return root;
   }

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

                  if( jsonConfig.isIgnoreTransientFields() && isTransient( 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;
   }

   private 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;
   }

   /**
    * Get the next value. The value can be a Boolean, Double, Integer,
    * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object.
    *
    * @throws JSONException If syntax error.
    * @return An object.
    */
   private Object nextValue(JSONTokener tokener, JsonConfig jsonConfig ) {
      char c = tokener.nextClean();
      String s;

      switch( c ){
         case '"':
         case '\'':
            return tokener.nextString( c );
         case '{':
        	 tokener.back();
            return fromObject( tokener, jsonConfig );
         case '[':
        	 tokener.back();
            return fromObject( tokener, jsonConfig );
         default:
            // empty
      }

      /*
       * Handle unquoted text. This could be the values true, false, or null, or
       * it can be a number. An implementation (such as this one) is allowed to
       * also accept non-standard forms. Accumulate characters until we reach
       * the end of the text or a formatting character.
       */

      StringBuffer sb = new StringBuffer();
      char b = c;
      while( c >= ' ' && ",:]}/\\\"[{;=#".indexOf( c ) < 0 ){
         sb.append( c );
         c = tokener.next();
      }
      tokener.back();

      /*
       * If it is true, false, or null, return the proper value.
       */

      s = sb.toString()
            .trim();
      if( s.equals( "" ) ){
         throw tokener.syntaxError( "Missing value." );
      }
      if( s.equalsIgnoreCase( "true" ) ){
         return Boolean.TRUE;
      }
      if( s.equalsIgnoreCase( "false" ) ){
         return Boolean.FALSE;
      }
      if( s.equals( "null" ) || 
          (jsonConfig.isJavascriptCompliant() && s.equals("undefined"))){
         return JSONNull.getInstance();
      }

      /*
       * If it might be a number, try converting it. We support the 0- and 0x-
       * conventions. If a number cannot be produced, then the value will just
       * be a string. Note that the 0-, 0x-, plus, and implied string
       * conventions are non-standard. A JSON parser is free to accept non-JSON
       * forms as long as it accepts all correct JSON forms.
       */

      if( (b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+' ){
         if( b == '0' ){
            if( s.length() > 2 && (s.charAt( 1 ) == 'x' || s.charAt( 1 ) == 'X') ){
               try{
                  return new Integer( Integer.parseInt( s.substring( 2 ), 16 ) );
               }catch( Exception e ){
                  /* Ignore the error */
               }
            }else{
               try{
                  return new Integer( Integer.parseInt( s, 8 ) );
               }catch( Exception e ){
                  /* Ignore the error */
               }
            }
         }

         try{
            return NumberUtils.createNumber(s);
         }catch( Exception e ){
            return s;
         }
      }

      if( JSONUtils.isFunctionHeader( s ) || JSONUtils.isFunction( s ) ){
         return s;
      }
      switch( peek(tokener) ){
         case ',':
         case '}':
         case '{':
         case '[':
         case ']':
            throw new JSONException( "Unquotted string '" + s + "'" );
      }

      return s;
   }

   /**
    * Look at the next character in the source string.
    *
    * @return The next character, or 0 if past the end of the source string.
    */
   public char peek(JSONTokener tokener) {
	   char c = tokener.next();
       if (!tokener.end()) {
           tokener.back();
       }
       return c;
   }

   private JSONObject _fromJSONTokener( JSONTokener tokener, JsonConfig jsonConfig ) {

      try{
         char c;
         String key;
         Object value;

         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 '}':
                  fireObjectEndEvent( jsonConfig );
                  return jsonObject;
               default:
                  tokener.back();
                  key = nextValue(tokener, 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 = peek(tokener);
            boolean quoted = peek == '"' || peek == '\'';
            Object v = nextValue(tokener, 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( quoted && v instanceof String && (JSONUtils.mayBeJSON( (String) v ) || JSONUtils.isFunction( v ))){
                     v = JSONUtils.DOUBLE_QUOTE + v + JSONUtils.DOUBLE_QUOTE;
                  }
                  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;
      }
   }

   private 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;
   }

   private 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 );
   }

   private 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;
   }

   private 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;
   }

   private 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 );
   }

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

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

   private boolean isTransientField( String name, Class beanClass, JsonConfig jsonConfig ) {
      try{
         Field field = beanClass.getDeclaredField( name );
         if((field.getModifiers() & Modifier.TRANSIENT) == Modifier.TRANSIENT) return true;
         return isTransient(field, jsonConfig);
      }catch( Exception e ){
         log.info( "Error while inspecting field "+beanClass+"."+name+" for transient status." ,e );
      }
      return false;
   }

   private 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;
   }

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

   private 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 );
   }
}
