com.p5solutions.core.json.JsonDeserializer.java Source code

Java tutorial

Introduction

Here is the source code for com.p5solutions.core.json.JsonDeserializer.java

Source

/* Pivotal 5 Solutions Inc. - Core Java library for all other Pivotal Java Modules.
 *
 * Copyright (C) 2011  KASRA RASAEE
 *
 * This program 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, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.p5solutions.core.json;

import java.beans.PropertyEditor;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;

import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.context.request.WebRequest;

import com.p5solutions.core.aop.Targetable;
import com.p5solutions.core.utils.Comparison;
import com.p5solutions.core.utils.NumberUtils;
import com.p5solutions.core.utils.ReflectionUtility;

/**
 * The Class JsonDeserializer: Standard JSON deserializer. Does not need to be
 * prototype based, this class is completely thread-safe and can be used in a
 * singleton fashion.
 * 
 * @author Kasra Rasaee
 * @since 2010-04-10
 * 
 * @see JsonConverterConfigurer
 * @see JsonHttpMessageConverter
 * @see JsonSerializer
 * @see ConversionService
 * @see GenericConverter
 */
public class JsonDeserializer {

    /** The logger. */
    private static Log logger = LogFactory.getLog(JsonDeserializer.class);

    /** The Constant DEFAULT_BUFFER_SIZE. */
    private static final int DEFAULT_BUFFER_SIZE = 4;

    /** The conversion service. */
    private ConversionService conversionService;

    /**
     * Throw input stream reader exception.
     */
    protected void throwInputStreamReaderException(Reader reader) {
        if (reader == null) {
            throw new NullPointerException(getClass() + " cannot deserialize a null input stream. "
                    + "You must instantiate with a vaild JSON input stream.");
        }

        try {
            if (!reader.ready()) {
                logger.error("Input stream reader is not ready to be read. Check instance of " + getClass());
            }
        } catch (Exception e) {
            logger.error(
                    e + " : " + "Input stream reader is not ready to be read. Check instance of " + getClass());
        }

    }

    /**
     * Copy.
     * 
     * @param source
     *          the source
     * @param destination
     *          the destination
     */
    protected void copy(char[] source, char[] destination) {
        copy(source, destination, 0);
    }

    /**
     * Copy array from source to destination, starting from index zero, up to the
     * maximum length of destination array.
     * 
     * @param source
     *          the source
     * @param destination
     *          the destination
     * @param start
     *          the start
     */
    protected void copy(char[] source, char[] destination, int start) {
        // if (source.length >= destination.length) {
        int j = start;
        for (int i = 0; i < destination.length; i++) {
            if (source.length == j) {
                break;
            }
            destination[i] = source[j++];
        }
    }

    /**
     * Copy.
     * 
     * @param a
     *          the a
     * @param pos
     *          the pos
     * @return the char[]
     */
    public static char[] copy(char[] a, int pos) {
        // char[] { 1, 2, 3, 4 };
        // count = 3
        // result { 4, , , }
        char[] dest = new char[a.length - pos];
        int j = 0;
        for (int i = pos; i < a.length; i++) {
            dest[j++] = a[i];
        }
        return dest;
    }

    /**
     * Checks if is start array tag.
     * 
     * @param c
     *          the c
     * @return true, if is start array tag
     */
    protected boolean isStartArrayTag(char c) {
        return c == '[';
    }

    /**
     * Checks if is end array tag.
     * 
     * @param c
     *          the c
     * @return true, if is end array tag
     */
    protected boolean isEndArrayTag(char c) {
        return c == ']';
    }

    /**
     * Checks if is start tag.
     * 
     * @param c
     *          the c
     * @return true, if is start tag
     */
    protected boolean isStartTag(char c) {
        return c == '{';
    }

    /**
     * Checks if is colon tag.
     * 
     * @param c
     *          the c
     * @return true, if is colon tag
     */
    protected boolean isColonTag(char c) {
        return c == ':';
    }

    /**
     * Checks if is end tag.
     * 
     * @param c
     *          the c
     * @return true, if is end tag
     */
    protected boolean isEndTag(char c) {
        return c == '}';
    }

    /**
     * Checks if is comma tag.
     * 
     * @param c
     *          the c
     * @return true, if is comma tag
     */
    protected boolean isCommaTag(char c) {
        return c == ',';
    }

    /**
     * Checks if is quotes.
     * 
     * @param c
     *          the c
     * @return true, if is quotes
     */
    protected boolean isQuotes(char c) {
        return c == '"';
    }

    /**
     * The Class Read.
     */
    protected class Read {

        /** The count. */
        int count;

        /** The buffer. */
        char[] buffer;
    }

    /**
     * Read.
     * 
     * @param previous
     *          the previous
     * @return the read
     * @throws IOException
     *           Signals that an I/O exception has occurred.
     */
    protected Read read(Reader reader, char[] previous, int bufferSize) throws IOException {
        Read read = new Read();
        if (previous != null && previous.length > 0) {
            read.buffer = previous;
            read.count = previous.length;
        } else {
            read.buffer = new char[bufferSize];
            read.count = reader.read(read.buffer);
        }
        return read;
    }

    // TODO comments.
    protected boolean attemptMapMapping(String fieldName, Object target, Object nvpValue, Class<?> returnType,
            JsonProperty jdt, WebRequest webRequest, WebDataBinder binder) {

        // quick return if the target field "method" is not of type List<?>
        // e.g. public List<?> pojo.getNames<String>() then continue
        if (!ReflectionUtility.isMapClass(returnType)) {
            return false;
        }

        if (isListWithinList(nvpValue)) {
            // cast the list of list
            List<List<?>> listOfObjects = (List<List<?>>) nvpValue;

            // is array
            if (ReflectionUtility.isMapClass(returnType) && jdt != null) {
                // TODO figure this out
                Map<Object, Object> put = new HashMap<Object, Object>();

                // iterate each element, and add it to the list
                for (List<?> list : listOfObjects) {
                    if (isNameValueList(list)) {

                        // recurisvely walk each list element
                        List<NameValuePair> nvl = (List<NameValuePair>) list;
                        Object next = ReflectionUtility.newInstance(jdt.value());
                        mapJsonDataToPOJO(next, nvl, webRequest, binder);
                        // add the walked element to the arraylist
                        // put.put(next);
                    }
                }
            }
            return true;
        } else if (nvpValue instanceof List<?>) {
            List<NameValuePair> listOfObjects = (List<NameValuePair>) nvpValue;
            // TODO cannot assume its a hashmap, maybe a treemap?
            Map<Object, Object> putMap = new HashMap<Object, Object>();
            if (listOfObjects.size() > 0) {
                for (NameValuePair nvp : listOfObjects) {
                    if (jdt != null && ReflectionUtility.isBasicClass(jdt.value())) {
                        putMap.put(nvp.name, nvp.value);
                    } else {
                        // TODO
                        // mapJsonDataToPOJO(target, pairs, webRequest, binder)
                    }
                }
            }
            ReflectionUtility.setValue(fieldName, target, putMap);
            return true;
        }

        return false;
    }

    /**
     * Attempt list mapping.
     * 
     * @param fieldName
     *          the field name
     * @param target
     *          the target
     * @param nvpValue
     *          the nvp value
     * @param returnType
     *          the return type
     * @param jdt
     *          the jdt
     * @return true, if successful
     */
    @SuppressWarnings("unchecked")
    protected boolean attemptListMapping(String fieldName, Object target, Object nvpValue, Class<?> returnType,
            JsonProperty jdt, WebRequest webRequest, WebDataBinder binder) {

        // quick return if the target field "method" is not of type List<?>
        // e.g. public List<?> pojo.getNames<String>() then continue
        if (!ReflectionUtility.isCollectionClass(returnType)) {
            return false;
        }

        if (isListWithinList(nvpValue)) {
            // cast the list of list
            List<List<?>> listOfObjects = (List<List<?>>) nvpValue;

            // is array
            if (ReflectionUtility.isCollectionClass(returnType) && jdt != null) {
                List<Object> put = new ArrayList<Object>();

                // iterate each element, and add it to the list
                for (List<?> list : listOfObjects) {
                    if (isNameValueList(list)) {

                        // recurisvely walk each list element
                        List<NameValuePair> nvl = (List<NameValuePair>) list;
                        Object next = ReflectionUtility.newInstance(jdt.value());
                        mapJsonDataToPOJO(next, nvl, webRequest, binder);

                        // add the walked element to the arraylist
                        put.add(next);
                    }
                }

                // set the list to the main object
                ReflectionUtility.setValue(fieldName, target, put);
            }
            return true;
        } else if (nvpValue instanceof List<?>) {
            List<NameValuePair> listOfObjects = (List<NameValuePair>) nvpValue;
            List<Object> putList = new ArrayList<Object>();
            for (NameValuePair nvp : listOfObjects) {
                if (jdt != null && ReflectionUtility.isBasicClass(jdt.value())) {
                    putList.add(nvp.value);
                } else {
                    // TODO woohoo
                    // mapJsonDataToPOJO(target, pairs, webRequest, binder)
                }
            }
            ReflectionUtility.setValue(fieldName, target, putList);
            return true;
        }

        return false;
    }

    /**
     * Attempt simple mapping.
     * 
     * @param fieldName
     *          the field name
     * @param target
     *          the target
     * @param nvpValue
     *          the nvp value
     * @param returnType
     *          the return type
     * @param webRequest
     *          the web request
     * @param binder
     *          the binder
     * @return true, if successful
     */
    @SuppressWarnings("unchecked")
    protected boolean attemptSimpleMapping(String fieldName, Object target, Object nvpValue, Class<?> returnType,
            WebRequest webRequest, WebDataBinder binder) {
        // recursively walk the next elements and set their properties
        if (nvpValue instanceof List<?>) {
            List<NameValuePair> list = (List<NameValuePair>) nvpValue;
            Object next = ReflectionUtility.newInstance(returnType);
            mapJsonDataToPOJO(next, list, webRequest, binder);
            ReflectionUtility.setValue(fieldName, target, next);
            return true;
        }

        return false;
    }

    /**
     * Sets the value. May also use the {@link ConversionService} to convert
     * types, such as {@link Date} using the {@link DateTimeFormat}. @see
     * 
     * @param method
     *          the method
     * @param fieldName
     *          the field name
     * @param target
     *          the target
     * @param value
     *          the value
     * @param binder
     *          the binder {@link ConversionService} and implementation of custom
     *          converters by implementing {@link GenericConverter}
     * @throws Exception
     */
    protected void setValue(Method method, String fieldName, Object target, Object value, WebRequest webRequest,
            WebDataBinder binder) {

        // Expose the real method, if proxied, since annotations need to be found.
        Method realMethod = method;
        if (target instanceof Targetable) {
            Targetable proxy = (Targetable) target;
            Class<?> clazz = proxy.getTarget().getClass();

            realMethod = ReflectionUtility.findMethod(clazz, realMethod.getName());
        }
        // TODO expose TrackStateUtility as part of Core??
        // Method realMethod = TrackStateUtility.exposeRealMethod(method, target);

        if (realMethod == null && method == null) {
            // if there are any binding, or formatting issues, put an error
            // in the model state.

            Object tempState = webRequest.getAttribute(ModelState.MODEL_STATE, WebRequest.SCOPE_REQUEST);
            if (tempState == null) {
                tempState = new ModelState();
            }
            ModelState modelState = (ModelState) tempState;
            modelState.add(fieldName,
                    "Cannot bind value " + value + " to target object "
                            + (target != null ? target.getClass() : "<null>"),
                    new RuntimeException(
                            "Field " + fieldName + " does not exist for " + target.getClass().getName()));
            webRequest.setAttribute(ModelState.MODEL_STATE, modelState, WebRequest.SCOPE_REQUEST);
        }
        // get the nullable property annotation, if any
        JsonNotNullProperty jnullpt = ReflectionUtility.findAnnotation(realMethod, JsonNotNullProperty.class);

        // get the json property, if any
        JsonProperty jpt = ReflectionUtility.findAnnotation(realMethod, JsonProperty.class);

        Class<?> returnType = method.getReturnType();
        if (ReflectionUtility.isNumberClass(returnType)) {

            try {
                Object numeric = NumberUtils.valueOf(value.toString(), returnType);
                ReflectionUtility.setValue(fieldName, target, numeric);
            } catch (NumberFormatException nfe) {
                // if there are any binding, or formatting issues, put an error
                // in the model state.

                Object tempState = webRequest.getAttribute(ModelState.MODEL_STATE, WebRequest.SCOPE_REQUEST);
                if (tempState == null) {
                    tempState = new ModelState();
                }
                ModelState modelState = (ModelState) tempState;
                modelState.add(fieldName, "Cannot bind value " + value + " to target object "
                        + (target != null ? target.getClass() : "<null>"), nfe);
                webRequest.setAttribute(ModelState.MODEL_STATE, modelState, WebRequest.SCOPE_REQUEST);
            }

        } else if (ReflectionUtility.isStringClass(returnType)) {

            // set empty values to null
            String sv = (String) value;
            sv = Comparison.isEmpty(sv) ? null : sv;

            // if the Nullable property is et with emptyNull to false
            // then actually set the value, even if its empty.
            if (jnullpt != null) {
                sv = (String) value;
            }

            // unescape the sting character.
            sv = unescape(sv);
            sv = unnull(sv);

            ReflectionUtility.setValue(fieldName, target, sv);
        } else if (!attemptListMapping(fieldName, target, value, returnType, jpt, webRequest, binder)) {

            // / attempt to map of Map<?,?>
            if (!attemptMapMapping(fieldName, target, value, returnType, jpt, webRequest, binder)) {

                // attempt to simple map the object
                if (!attemptSimpleMapping(fieldName, target, value, returnType, webRequest, binder)) {

                    // Use the Spring Conversion service and try to map the
                    // values
                    TypeDescriptor sourceType = TypeDescriptor.forObject(value);

                    // specify the method, -1 such that it uses the return value
                    // type
                    MethodParameter mp = new MethodParameter(realMethod, -1);

                    // setup the type descriptor and pass it to the converter
                    TypeDescriptor targetType = new TypeDescriptor(mp);

                    // PropertyValue pv = new PropertyValue(fieldName, value);

                    Object converted = null;
                    PropertyEditor editor = null;
                    if (binder != null) {
                        editor = binder.findCustomEditor(returnType, null);
                    }

                    if (editor != null) {
                        editor.setAsText(value.toString());
                        converted = editor.getValue();
                    } else if (conversionService != null) {
                        // use the conversion service to translate the value
                        converted = this.conversionService.convert(value, sourceType, targetType);
                    }

                    // set the converted value, if any
                    ReflectionUtility.setValue(fieldName, target, converted);
                }
            }
        }
    }

    /**
     * Escaping new lines and quotes for json.
     * 
     * @param value
     * @return
     */
    protected String unescape(String value) {
        if (value == null) {
            return null;
        }

        // TODO should we really be stripping these out? depends on the format we
        // are trying to save the content?
        // HTML doesn't support these types anyway, for example, what is a \t even
        // represented as? spaces <div> spacing. what?
        // since Json is usually used in web applications, this probably makes some
        // sort of sense...
        value = value.replace("\\\\", "\\");
        value = value.replace("\\t", "\t");
        value = value.replace("\\n", "\n");
        value = value.replace("\\r", "\r");
        value = value.replace("\\\"", "\"");

        // String result = value.replace("\\\\", "\\").replace("\\n",
        // "\n").replace("\\r", "\r").replace("\\\"", "\"").replace("\t", "");
        return value;
    }

    /**
     * Unnull.
     * 
     * @param value
     *          the value
     * @return the string
     */
    protected String unnull(String value) {
        if (value == null) {
            return null;
        }

        if (Comparison.isEqualCaseInsentiveTrim("null", value)) {
            return null;
        }

        return value;
    }

    /**
     * Builds the map.
     * 
     * @param target
     *          the target
     * @param pairs
     *          the pairs
     */
    @SuppressWarnings("unchecked")
    private void buildMap(Object target, List<NameValuePair> pairs) {
        if (pairs != null) {
            for (NameValuePair pair : pairs) {
                String name = pair.name;
                Object value = pair.value;
                if (isNameValueList(value)) {
                    HashMap<Object, Object> newMap = new HashMap<Object, Object>();
                    buildMap(newMap, (List<NameValuePair>) value);
                    ((HashMap) target).put(name, newMap);
                } else {
                    ((HashMap) target).put(name, value);
                }
            }
        }
    }

    private String tempfixandlogger(String value) {
        if (Comparison.isNotEmpty(value)) {
            StringBuilder sb = new StringBuilder();
            byte[] buf = value.getBytes();
            for (int i = 0; i < buf.length; i++) {
                if (buf[i] == '\0') {
                    logger.fatal(
                            "Found \0 in string buffer, something is not correct in the handling of the message, so far we've received on the buffer: "
                                    + sb.toString());
                    continue;
                }

                sb.append(buf[i]);
            }
            return sb.toString();
        }
        return value;
    }

    /**
     * Map json data to a plain old java object, all relevant data should be
     * mapped, as long as the name matches a getter/setter. For example
     * {address:{city: "Toronto"}} should match target object target.address.city.
     * It will also map values via dot path notation, such as json
     * {"address.city": "Toronto"}
     * 
     * @param target
     *          the object
     * @param pairs
     *          the pairs
     * @param binder
     *          the binder
     */
    protected void mapJsonDataToPOJO(Object target, List<NameValuePair> pairs, WebRequest webRequest,
            WebDataBinder binder) {

        Class<?> clazz = target.getClass();
        // if the target parameter is of type map.
        if (ReflectionUtility.isMapClass(clazz)) {
            buildMap(target, pairs);
            return;
        }

        // otherwise not map but some sort of POJO
        if (pairs != null) {
            for (NameValuePair pair : pairs) {
                //String name = tempfixandlogger(pair.name);
                String name = pair.name;
                Object value = pair.value;
                Method method = ReflectionUtility.findGetterMethod(clazz, name);

                if (method != null) {
                    setValue(method, name, target, value, webRequest, binder);
                } else {
                    if (name != null && name.indexOf(".") > 0) {
                        mapByPath(value, target, name, webRequest, binder);
                    } else {
                        logger.error("No method found under clazz: " + clazz + " when searching for field name of "
                                + name);
                    }
                }
            }
        }
    }

    protected Object isArrayOrMap(String fieldName) {
        if (Comparison.isNotEmpty(fieldName)) {
            int start = fieldName.indexOf('[');
            int end = fieldName.indexOf(']', start);
            if (start != -1 && end > start) {
                String indexer = fieldName.substring(start + 1, end);
                if (Comparison.isNumeric(indexer)) {
                    Integer i = NumberUtils.parseNumber(indexer, Integer.class);
                    return i >= 0 ? i : null;
                } else {
                    return indexer;
                }
            }
        }
        return null;
    }

    protected void mapByPath(Object value, Object target, String path, WebRequest webRequest,
            WebDataBinder binder) {
        mapByPath(value, null, target, null, path, webRequest, binder);
    }

    /**
     * Map value to target via path, usually a nested object, such as
     * target.address.city instead of using the standard JSON nesting.
     * 
     * @param value
     *          the value to set against the target object
     * @param lastIndexer
     *          the indexer, null if the value is not part of an Collection, Set
     *          or Map
     * @param target
     *          the target object that the value will be set against
     * @param property
     *          the json property of the given value
     * @param path
     *          the path, usually a single level path, but this JsonDeserializer
     *          supports "spring" type bind path variables.
     * @param webRequest
     *          the web request
     * @param binder
     *          the binder
     */
    protected void mapByPath(Object value, Object lastIndexer, Object target, JsonProperty property, String path,
            WebRequest webRequest, WebDataBinder binder) {

        int pos = path.indexOf('.', 1);
        Class<?> clazz = target.getClass();
        Object pushInstance = null;
        String field = path;

        if (pos > 0 && value != null) {
            field = path.substring(0, pos);
            String next = path.substring(pos + 1);

            boolean isArray = false;
            Object currentIndexer = isArrayOrMap(field);
            if (currentIndexer != null) {
                isArray = true;
                int start = field.indexOf('[');
                field = field.substring(0, start);
            }

            Method method = ReflectionUtility.findGetterMethod(clazz, field);
            if (method != null) {
                Class<?> pushType = method.getReturnType();
                pushInstance = ReflectionUtility.getValue(field, target);

                if (pushInstance == null) {
                    // its an array
                    // TODO .. need to create map?? based on pushType
                    if (isArray) {
                        pushInstance = new ArrayList<Object>();
                    } else {
                        pushInstance = ReflectionUtility.newInstance(pushType);
                    }
                }
                // is there a json property attribute?
                JsonProperty jsonProperty = ReflectionUtility.findAnnotation(method, JsonProperty.class);

                // recursion until we set the appropriate value
                mapByPath(value, currentIndexer, pushInstance, jsonProperty, next, webRequest, binder);

                // set teh instance to the current target
                ReflectionUtility.setValue(field, target, pushInstance);
            }
        } else {
            if (ReflectionUtility.isCollectionClass(clazz)) {
                if (Comparison.isNotNull(property) && Comparison.isNotNull(property.value())) {
                    Object addTarget = ReflectionUtility.newInstance(property.value());
                    if (Comparison.isNotNull(addTarget)) {
                        Method method = ReflectionUtility.findGetterMethod(property.value(), field);

                        setValue(method, field, addTarget, value, webRequest, binder);

                        ((Collection<Object>) target).add(addTarget);
                    }
                } else {
                    // TODO do basic mapping.. add values to array
                    ((Collection<Object>) target).add(value);
                }
                // attemptListMapping(field, pushInstance, nvpValue, returnType,
                // jdt,
                // binder)
            } else if (ReflectionUtility.isMap(clazz)) {
                Map<Object, Object> map = ((Map<Object, Object>) target);

                if (map != null) {
                    Object mapValue = map.get(lastIndexer);
                    if (mapValue == null) {
                        // TODO need to create an object, but the JsonProperty
                        // must be set??
                        throw new NotImplementedException(
                                "Cannot create a new instance of object type ? please define "
                                        + JsonProperty.class);
                    }

                    Class<?> mapClass = mapValue.getClass();
                    // TODO probably should check against real class, not some
                    // proxied class

                    Method method = ReflectionUtility.findGetterMethod(mapClass, field);
                    if (method == null) {
                        throw new NullPointerException(
                                "Setter must be defined for path " + field + " on class type " + mapClass);
                    }

                    setValue(method, field, mapValue, value, webRequest, binder);

                    // System.out.println("Last Indexer " + lastIndexer);
                }
            } else {
                Method method = ReflectionUtility.findGetterMethod(clazz, field);
                setValue(method, field, target, value, webRequest, binder);
            }
        }
    }

    /**
     * Checks if is name value list.
     * 
     * @param value
     *          the value
     * @return true, if is name value list
     */
    protected boolean isNameValueList(Object value) {
        if (value instanceof List<?>) {
            List<?> list = (List<?>) value;
            if (list.size() > 0) {
                Object v = list.get(0);
                return v instanceof NameValuePair;
            }
        }
        return false;
    }

    /**
     * Checks if is list within list.
     * 
     * @param value
     *          the value
     * @return true, if is list within list
     */
    protected boolean isListWithinList(Object value) {
        if (value instanceof List<?>) {
            List<?> list = (List<?>) value;
            if (list.size() > 0) {
                Object v = list.get(0);
                return v instanceof List<?>;
            }
        }
        return false;
    }

    /**
     * Deserialize the JSON data and return a populated POJO, however use UTF-8 as
     * the default reader encoding.
     * 
     * @param <T>
     *          the generic type
     * @param clazz
     *          the clazz
     * @param input
     *          the input
     * @return the t
     * @throws IOException
     *           Signals that an I/O exception has occurred.
     */
    public <T> T deserialize(Class<T> clazz, InputStream input) throws IOException {
        return deserialize(clazz, input, Charset.forName("UTF-8"));
    }

    /**
     * Deserialize.
     * 
     * @param <T>
     *          the generic type
     * @param clazz
     *          the clazz
     * @param target
     *          the target
     * @param binder
     *          the binder
     * @param input
     *          the input
     * @param charset
     *          the charset
     * @return the t
     * @throws IOException
     *           Signals that an I/O exception has occurred.
     */
    public <T> T deserialize(Class<T> clazz, T target, WebRequest webRequest, WebDataBinder binder,
            InputStream input, Charset charset) throws IOException {

        Reader reader = new InputStreamReader(input, charset);
        return deserialize(clazz, target, webRequest, binder, reader, null);
    }

    /**
     * Deserialize the JSON data and return a populated POJO.
     * 
     * @param <T>
     *          the generic type
     * @param clazz
     *          the clazz
     * @param input
     *          the input
     * @param charset
     *          the charset
     * @return the object
     * @throws IOException
     *           Signals that an I/O exception has occurred.
     */
    public <T> T deserialize(Class<T> clazz, InputStream input, Charset charset) throws IOException {
        Reader reader = new InputStreamReader(input, charset);
        return deserialize(clazz, null, null, null, reader, null);
    }

    /**
     * Deserialize the JSON data and return a populated POJO.
     * 
     * @param <T>
     *          the generic type
     * @param clazz
     *          the clazz
     * @param target
     *          the target
     * @param binder
     *          the binder
     * @param reader
     *          the reader
     * @param previous
     *          the previous
     * @return the object
     * @throws IOException
     *           Signals that an I/O exception has occurred.
     */
    @SuppressWarnings("unchecked")
    protected <T> T deserialize(Class<T> clazz, T target, WebRequest webRequest, WebDataBinder binder,
            Reader reader, char[] previous) throws IOException {
        char[] buffer;

        T root = null;
        if (target == null) {
            root = ReflectionUtility.newInstance(clazz);
        } else {
            root = target;
        }

        for (;;) {
            Read read = read(reader, previous, DEFAULT_BUFFER_SIZE);
            buffer = read.buffer;
            int count = read.count;
            previous = null; // reset, so that next time we don't use the same
            // buffer
            read = null; // read variable should not be used after this point
            if (count == -1) {
                break;
            }

            for (int i = 0; i < count; i++) {
                char c = buffer[i];

                // if its a start tag, then it must be the start of a
                // new object, or start of the actual json content,
                // either way, it needs to be walked to the next element
                if (isStartTag(c)) {
                    // cut the buffer and return remainder
                    char[] buffernext = cutBuffer(buffer, i, count);

                    // walk to the next element
                    ValueBufferPair vbp = walkNames(reader, buffernext, DEFAULT_BUFFER_SIZE);
                    List<NameValuePair> nvps = (List<NameValuePair>) vbp.value;
                    mapJsonDataToPOJO(root, nvps, webRequest, binder);
                }
            }
        }
        return root;
    }

    /**
     * The Class NameValuePair.
     */
    protected class NameValuePair {

        /** The name. */
        String name;

        /** The value. */
        Object value;

        /** The is array. */
        Boolean isArray;
    }

    /**
     * The Class ValueBufferPair.
     */
    protected class ValueBufferPair {

        /** The value. */
        Object value;

        /**
         * The buffer. This is the remainder of the buffer, whatever that was
         * leftover, and not necessarily part of the name/value pair
         */
        char[] buffer;

        /** The is end tag. Was the ValuBufferPair returned due to a } END TAG */
        boolean isEndTag;

        /** The is array. */
        boolean isArray;

        /**
         * The adjusted buffer size. This is different to the above buffer and its
         * current size.
         */
        int adjustedBufferSize;
    }

    /**
     * Name value pair.
     * 
     * @param name
     *          the name
     * @param vbp
     *          the vbp
     * @return the name value pair
     */
    protected NameValuePair createNameValuePair(String name, ValueBufferPair vbp) {
        // create a new name value pair for the returned value of the stream
        // walk
        NameValuePair nvp = new NameValuePair();

        // strip out any quotes from the BEGINNING and its END
        nvp.name = stripQuotes(name);
        nvp.value = vbp.value;
        return nvp;
    }

    /**
     * Creates the value buffer.
     * 
     * @param value
     *          the value
     * @return the value buffer pair
     */
    protected ValueBufferPair createValueBuffer(Object value, int adjustedBufferSize) {
        return createValueBuffer(value, null, -1, -1, false, adjustedBufferSize);
    }

    /**
     * Creates the value buffer.
     * 
     * @param value
     *          the value
     * @param buffer
     *          the buffer
     * @return the value buffer pair
     */
    protected ValueBufferPair createValueBuffer(Object value, char[] buffer, int adjustedBufferSize) {
        return createValueBuffer(value, buffer, -1, -1, false, adjustedBufferSize);
    }

    /**
     * Creates the value buffer.
     * 
     * @param value
     *          the value
     * @param buffer
     *          the buffer
     * @param count
     *          the count
     * @param index
     *          the index
     * @param isEndTag
     *          the is end tag
     * @return the value buffer pair
     */
    protected ValueBufferPair createValueBuffer(Object value, char[] buffer, int count, int index, boolean isEndTag,
            int adjustedBufferSize) {
        ValueBufferPair vbp = new ValueBufferPair();
        vbp.value = value;
        vbp.isEndTag = isEndTag;

        // cut the buffer from this position forward
        if (buffer != null) {
            char[] buffernext = null;

            if (count == -2 && index == -2) {
                // DO NOT DO ANYTHING, SIMPLY CREATE THE ValueBuffer and return it.
                buffernext = buffer;
            } else if (count != -1 && index != -1) {
                // copy only from the index position forward
                int nlen = count - index - 1;
                if (nlen > 0) {
                    buffernext = new char[nlen];
                    copy(buffer, buffernext, index + 1);
                }
            } else {
                // TODO check this logic..., OK SO....
                // previously in the walk
                int size = buffer.length - 1;
                if (size > 0) {
                    // copy the entire content of the buffer
                    // buffernext = new char[buffer.length];
                    buffernext = new char[size];
                    copy(buffer, buffernext, 1); // start at next position
                } else {
                    buffernext = null;
                }
            }
            vbp.buffer = buffernext;
            vbp.adjustedBufferSize = adjustedBufferSize;
        }
        return vbp;
    }

    /**
     * Strip quotes.
     * 
     * @param value
     *          the value
     * @return the string
     */
    protected String stripQuotes(String value) {
        // trim the string from any whitespaces
        value = value.trim();

        int x1 = 0;
        int x2 = value.length();
        if ('"' == value.charAt(0)) {
            x1 = 1;
        }
        if ('"' == value.charAt(x2 - 1)) {
            x2 -= 1;
        }
        value = value.substring(x1, x2);

        return value;
    }

    /**
     * Adjust buffer size, if the amount of read bytes is equal to the size of the
     * buffer, then double the buffer, otherwise shrink it.
     * 
     * @param read
     *          the read
     * @param size
     *          the size
     * @return the char[]
     */
    protected int adjustBufferSize(int read, int size) {
        // increase the buffer size * 2
        if (read == size) {
            return read * 2;
        } else if (read < size) {
            // shrink it. make sure to not go lower than 4 characters
            int split = read / 2;
            if (split < 4) {
                return 4;
            } else {
                return split;
            }
        }
        return size;
    }

    /**
     * Cut the buffer from the cut position, and return the remainder.
     * 
     * @param buffer
     *          the buffer
     * @param cut
     *          the cut
     * @return the char[], return <code>null</code> if there is nothing remaining
     */
    protected char[] cutBuffer(char[] buffer, int cut, int read) {
        int nlen = buffer.length - cut - 1;
        char[] buffernext = null;
        if (nlen > 0) {
            int actualbufferlen = read - cut - 1;
            if (actualbufferlen > 0) {
                buffernext = new char[actualbufferlen];
                copy(buffer, buffernext, cut + 1);
            }
        }
        return buffernext;
    }

    /**
     * Walk names.
     * 
     * @param previous
     *          the previous
     * @return the value buffer pair
     * @throws IOException
     *           Signals that an I/O exception has occurred.
     */
    protected ValueBufferPair walkNames(Reader reader, char[] previous, int bufferSize) throws IOException {
        char[] buffer = null;
        String name = new String();
        List<NameValuePair> nvps = new ArrayList<NameValuePair>();

        // this is populated when processig the value,
        // in some cases, the value may hit an endtag '}'
        // for example, in the case of EOF or end of a nested
        // object for a given parameter name, as such, we need
        // to identify this, and exit the recursion properly.
        boolean isEndTagFlag = false;
        // boolean inBetweenQuotes = false;

        for (;;) {
            Read read = read(reader, previous, bufferSize);
            buffer = read.buffer;
            int count = read.count;
            if (count == -1) {
                // TODO: do we need to add the last entry ??
                return createValueBuffer(nvps, bufferSize);
            }

            previous = null; // reset, so that next time we don't use the same
            // buffer
            read = null; // read variable should not be used after this point
            int cut = count;
            boolean reset = false;
            for (int i = 0; i < count; i++) {
                char c = buffer[i];

                if (isColonTag(c)) {
                    cut = i;
                    break;
                } else if (isEndArrayTag(c)) {
                    // SO, THIS IS THE CATCH, if this method is called from the walkArray,
                    // then it expects
                    // an array termination character "]", meaning we cannot move the
                    // array forward +1, otherwise
                    // the previous call "walkArray" will think there are more elements to
                    // be inserted into the array list.
                    // as such, we need to return the valuebuffer but not move the array
                    // forward.
                    return createValueBuffer(nvps, buffer, -2, -2, false, bufferSize);
                } else if (isEndTagFlag || isEndTag(c)) {
                    // make sure to return the buffer to the previous call
                    return createValueBuffer(nvps, buffer, bufferSize);
                }

                if (isCommaTag(c)) {
                    previous = copy(buffer, i + 1);
                    name = new String();
                    reset = true;
                    break;
                }
            }

            if (reset) {
                // go back up and try again, usually in the case of skipping a
                // character
                // like ','
                continue;
            }

            if (cut != count) {
                name = append(name, buffer, cut, count);

                // cut the buffer if necessary
                char[] buffernext = cutBuffer(buffer, cut, count);

                // walk the stream, and return its value
                ValueBufferPair vbp = walkValue(reader, buffernext, bufferSize);
                if (vbp == null) {
                    throw new NullPointerException("Value can back as null on JSON parameter name: " + name);
                }

                // add name value pair
                nvps.add(createNameValuePair(name, vbp));

                // make sure we use the buffer remaining from above call
                previous = vbp.buffer;

                // reset to new name, next token
                name = new String();

                // setup the end tag from value call
                isEndTagFlag = vbp.isEndTag;
            } else {
                name += new String(buffer, 0, count);
                bufferSize = adjustBufferSize(count, bufferSize);
            }
        }
    }

    /**
     * Walk array.
     * 
     * @param previous
     *          the previous
     * @return the object
     * @throws IOException
     *           Signals that an I/O exception has occurred.
     */
    protected Object walkArray(Reader reader, char[] previous, int bufferSize) throws IOException {
        char[] buffer = null;
        String value = new String();
        boolean inBetweenQuotes = false;
        List<Object> list = new ArrayList<Object>();

        for (;;) {
            Read read = read(reader, previous, bufferSize);
            buffer = read.buffer;
            int count = read.count;
            if (count == -1) {
                return createValueBuffer(value, bufferSize);
            }
            previous = null; // reset, so that next time we don't use the same
            // buffer
            read = null; // read variable should not be used after this point

            for (int i = 0; i < count; i++) {
                char c = buffer[i];

                // new element
                if (!inBetweenQuotes && isStartTag(c)) {

                    // cut the buffer
                    char[] buffernext = cutBuffer(buffer, i, count);

                    // walk to the next element
                    ValueBufferPair vbpReturn = walkNames(reader, buffernext, bufferSize);
                    list.add(vbpReturn.value);

                    // the buffer probably moved forward, as such,
                    // we must use the one passed back from walkNames(..)
                    buffernext = vbpReturn.buffer;
                    ValueBufferPair vbpNew = createValueBuffer(vbpReturn.value, buffernext, bufferSize);
                    vbpNew.isArray = true;
                    previous = buffernext;
                    break;
                }

                if (!inBetweenQuotes && isEndTag(c)) {
                    // return valuebuffer(value, buffer, count, i, true);
                }

                if (!inBetweenQuotes && isEndArrayTag(c)) {
                    return createValueBuffer(list, buffer, count, i, false, bufferSize);
                }

            }
        }
    }

    /**
     * Walk value.
     * 
     * @param previous
     *          the previous
     * @return the value buffer pair
     * @throws IOException
     *           Signals that an I/O exception has occurred.
     */
    protected ValueBufferPair walkValue(Reader reader, char[] previous, int bufferSize) throws IOException {
        char[] buffer = null;
        String value = new String();
        boolean inBetweenQuotes = false;
        boolean isArray = false;

        int pc = '\0';
        for (;;) {
            Read read = read(reader, previous, bufferSize);
            buffer = read.buffer;

            int count = read.count;
            if (count == -1) {
                return createValueBuffer(value, bufferSize);
            }
            previous = null; // reset, so that next time we don't use the same
            // buffer
            read = null; // read variable should not be used after this point

            int cut = count;
            for (int i = 0; i < count; i++) {
                char c = buffer[i];

                if (isQuotes(c) && pc != '\\') {
                    inBetweenQuotes = !inBetweenQuotes;
                    pc = c;
                    continue;
                }

                pc = c;

                if (!inBetweenQuotes && isCommaTag(c)) {
                    cut = i;
                    break;
                }

                if (!inBetweenQuotes && isColonTag(c)) {
                    // it could also be a property name (nested), not a value
                }

                if (!inBetweenQuotes && isStartArrayTag(c)) {
                    isArray = true;

                    // cut the buffer before walking the array
                    char[] buffernext = cutBuffer(buffer, i, count);

                    Object array = walkArray(reader, buffernext, bufferSize);
                    return (ValueBufferPair) array;
                }

                if (!inBetweenQuotes && isStartTag(c)) {
                    // cut the buffer before walking recursively the next names
                    char[] buffernext = cutBuffer(buffer, i, count);

                    // walk to the next element
                    ValueBufferPair vbpReturn = walkNames(reader, buffernext, bufferSize);
                    buffernext = vbpReturn.buffer;
                    ValueBufferPair vbpNew = createValueBuffer(vbpReturn.value, buffernext, bufferSize);
                    vbpNew.isArray = isArray;
                    return vbpNew;
                }

                if (!inBetweenQuotes && isEndTag(c)) {
                    // append to the value
                    value = stripQuotes(append(value, buffer, i, count));

                    return createValueBuffer(value, buffer, count, i, true, bufferSize);
                }

                if (!inBetweenQuotes && isEndArrayTag(c)) {
                    return createValueBuffer(value, buffer, count, i, false, bufferSize);
                }

            }

            if (cut != count) {
                // append to the value
                value = stripQuotes(append(value, buffer, cut, count));

                // walk value
                return createValueBuffer(value, buffer, count, cut, false, bufferSize);
            } else {
                value = append(value, buffer, -1, count);
                bufferSize = adjustBufferSize(count, bufferSize);
            }
        }
    }

    /**
     * Append.
     * 
     * @param appendTo
     *          the append to
     * @param buffer
     *          the buffer
     * @return the string
     */
    // BAD IDEA AS THE BUFFER IS NOT NECESSARILY ALWAYS FILLED
    //protected String append(String appendTo, char[] buffer) {
    //  return append(appendTo, buffer, -1);
    //}

    /**
     * Append.
     * 
     * @param appendTo
     *          the append to
     * @param buffer
     *          the buffer
     * @param cut
     *          the cut
     * @return the string
     */
    protected String append(String appendTo, char[] buffer, int cut, int read) {
        if (cut == -1) {
            appendTo += new String(buffer, 0, read);
        } else {
            char[] temp = new char[cut];
            copy(buffer, temp);
            appendTo += new String(temp);
        }
        return appendTo;
    }

    /**
     * Sets the conversion service.
     * 
     * @param conversionService
     *          the new conversion service
     */
    @Resource
    public void setConversionService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }
}