org.gvnix.web.json.DataBinderDeserializer.java Source code

Java tutorial

Introduction

Here is the source code for org.gvnix.web.json.DataBinderDeserializer.java

Source

/*
 * gvNIX. Spring Roo based RAD tool for Generalitat Valenciana
 * Copyright (C) 2013 Generalitat Valenciana
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gvnix.web.json;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import org.gvnix.web.json.DataBinderMappingJackson2HttpMessageConverter.DataBinderList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.impl.ObjectIdReader;
import com.fasterxml.jackson.databind.util.NameTransformer;

/**
 * Jackson2 deserializer based on Spring DataBinder.
 * <p/>
 * This deserializer requires a {@link DataBinder} was stored in
 * {@link ThreadLocal} with key "{@link BindingResult#MODEL_KEY_PREFIX}" +
 * {@code "JSON_DataBinder"}
 * 
 * @author gvNIX Team
 * @since TODO: Class version
 */
public class DataBinderDeserializer extends BeanDeserializerBase {

    /**
     *
     */
    private static final long serialVersionUID = -7345091954698956061L;

    private static final Logger LOGGER = LoggerFactory.getLogger(DataBinderDeserializer.class);

    public DataBinderDeserializer(BeanDeserializerBase source) {
        super(source);
    }

    public DataBinderDeserializer(BeanDeserializerBase source, ObjectIdReader objectIdReader) {
        super(source, objectIdReader);
    }

    public DataBinderDeserializer(BeanDeserializerBase source, HashSet<String> ignorableProps) {
        super(source, ignorableProps);
    }

    /**
     * {@inheritDoc}
     * 
     * Uses {@link DataBinderDeserializer}
     */
    @Override
    public BeanDeserializerBase withObjectIdReader(ObjectIdReader objectIdReader) {
        return new DataBinderDeserializer(this, objectIdReader);
    }

    /**
     * {@inheritDoc}
     * 
     * Uses {@link DataBinderDeserializer}
     */
    @Override
    public BeanDeserializerBase withIgnorableProperties(HashSet<String> ignorableProps) {
        return new DataBinderDeserializer(this, ignorableProps);
    }

    /**
     * Deserializes JSON content into Map<String, String> format and then uses a
     * Spring {@link DataBinder} to bind the data from JSON message to JavaBean
     * objects.
     * <p/>
     * It is a workaround for issue
     * https://jira.springsource.org/browse/SPR-6731 that should be removed from
     * next gvNIX releases when that issue will be resolved.
     * 
     * @param parser Parsed used for reading JSON content
     * @param ctxt Context that can be used to access information about this
     *        deserialization activity.
     * 
     * @return Deserializer value
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Override
    public Object deserialize(JsonParser parser, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        JsonToken t = parser.getCurrentToken();
        MutablePropertyValues propertyValues = new MutablePropertyValues();

        // Get target from DataBinder from local thread. If its a bean
        // collection
        // prepares array index for property names. Otherwise continue.
        DataBinder binder = (DataBinder) ThreadLocalUtil
                .getThreadVariable(BindingResult.MODEL_KEY_PREFIX.concat("JSON_DataBinder"));
        Object target = binder.getTarget();

        // For DstaBinderList instances, contentTarget contains the final bean
        // for binding. DataBinderList is just a simple wrapper to deserialize
        // bean wrapper using DataBinder
        Object contentTarget = null;

        if (t == JsonToken.START_OBJECT) {
            String prefix = null;
            if (target instanceof DataBinderList) {
                prefix = binder.getObjectName().concat("[").concat(Integer.toString(((Collection) target).size()))
                        .concat("].");

                // BeanWrapperImpl cannot create new instances if generics
                // don't specify content class, so do it by hand
                contentTarget = BeanUtils.instantiateClass(((DataBinderList) target).getContentClass());
                ((Collection) target).add(contentTarget);
            } else if (target instanceof Map) {
                // TODO
                LOGGER.warn("Map deserialization not implemented yet!");
            }
            Map<String, String> obj = readObject(parser, ctxt, prefix);
            propertyValues.addPropertyValues(obj);
        } else {
            LOGGER.warn("Deserialization for non-object not implemented yet!");
            return null; // TODO?
        }

        // bind to the target object
        binder.bind(propertyValues);

        // Note there is no need to validate the target object because
        // RequestResponseBodyMethodProcessor.resolveArgument() does it on top
        // of including BindingResult as Model attribute

        // For DAtaBinderList the contentTarget contains the final bean to
        // make the binding, so we must return it
        if (contentTarget != null) {
            return contentTarget;
        }
        return binder.getTarget();
    }

    /**
     * Deserializes JSON object into Map<String, String> format to use it in a
     * Spring {@link DataBinder}.
     * <p/>
     * Iterate over every object's property and delegates on
     * {@link #readField(JsonParser, DeserializationContext, JsonToken, String)}
     * 
     * @param parser JSON parser
     * @param ctxt context
     * @param prefix object DataBinder path
     * @return property values
     * @throws IOException
     * @throws JsonProcessingException
     */
    public Map<String, String> readObject(JsonParser parser, DeserializationContext ctxt, String prefix)
            throws IOException, JsonProcessingException {
        JsonToken t = parser.getCurrentToken();

        if (t == JsonToken.START_OBJECT) {
            t = parser.nextToken();
            // Skip it to locate on first object data token
        }

        // Deserialize object properties
        Map<String, String> deserObj = new HashMap<String, String>();
        for (; t != JsonToken.END_OBJECT; t = parser.nextToken()) {
            Map<String, String> field = readField(parser, ctxt, t, prefix);
            deserObj.putAll(field);
        }
        return deserObj;
    }

    /**
     * Deserializes JSON array into Map<String, String> format to use it in a
     * Spring {@link DataBinder}.
     * <p/>
     * Iterate over every array's item to generate a prefix for property names
     * on DataBinder style (
     * <em>{prefix}[{index}].<em>) and delegates on {@link #readField(JsonParser, DeserializationContext, JsonToken, String)}
     * 
     * @param parser JSON parser
     * @param ctxt context
     * @param prefix array dataBinder path
     * @return
     * @throws IOException
     * @throws JsonProcessingException
     */
    protected Map<String, String> readArray(JsonParser parser, DeserializationContext ctxt, String prefix)
            throws IOException, JsonProcessingException {
        JsonToken t = parser.getCurrentToken();

        if (t == JsonToken.START_ARRAY) {
            t = parser.nextToken();
            // Skip it to locate on first array data token
        }

        // Deserialize array properties
        int i = 0;
        Map<String, String> deserObj = new HashMap<String, String>();
        for (; t != JsonToken.END_ARRAY; t = parser.nextToken()) {
            // Property name must include prefix this way:
            // degrees[0].description
            Map<String, String> field = readField(parser, ctxt, t,
                    prefix.concat("[").concat(Integer.toString(i++)).concat("]."));
            deserObj.putAll(field);
        }
        return deserObj;
    }

    /**
     * Deserializes JSON property into Map<String, String> format to use it in a
     * Spring {@link DataBinder}.
     * <p/>
     * Check token's type to perform an action:
     * <ul>
     * <li>If it's a property, stores it in map</li>
     * <li>If it's an object, calls to
     * {@link #readObject(JsonParser, DeserializationContext, String)}</li>
     * <li>If it's an array, calls to
     * {@link #readArray(JsonParser, DeserializationContext, String)}</li>
     * </ul>
     * 
     * @param parser
     * @param ctxt
     * @param token current token
     * @param prefix property dataBinder path
     * @return
     * @throws IOException
     * @throws JsonProcessingException
     */
    protected Map<String, String> readField(JsonParser parser, DeserializationContext ctxt, JsonToken token,
            String prefix) throws IOException, JsonProcessingException {

        String fieldName = null;
        String fieldValue = null;

        // Read the field name
        fieldName = parser.getCurrentName();

        // If current token contains a field name
        if (!isEmptyString(fieldName)) {

            // Append the prefix if given
            if (isEmptyString(prefix)) {
                fieldName = parser.getCurrentName();
            } else {
                fieldName = prefix.concat(parser.getCurrentName());
            }
        }
        // If current token contains mark array or object start markers.
        // Note it cannot be a field value because it will be read below and
        // then the token is advanced to the next
        else {

            // Use the prefix in recursive calls
            if (!isEmptyString(prefix)) {
                fieldName = prefix;
            }
        }

        // If current token has been used to read the field name, advance
        // stream to the next token that contains the field value
        if (token == JsonToken.FIELD_NAME) {
            token = parser.nextToken();
        }

        // Field value
        switch (token) {
        case VALUE_STRING:
        case VALUE_NUMBER_INT:
        case VALUE_NUMBER_FLOAT:
        case VALUE_EMBEDDED_OBJECT:
        case VALUE_TRUE:
        case VALUE_FALSE:
            // Plain field: Store value
            Map<String, String> field = new HashMap<String, String>();
            fieldValue = parser.getText();
            field.put(fieldName, fieldValue);
            return field;
        case START_ARRAY:
            // Read array items
            return readArray(parser, ctxt, fieldName);
        case START_OBJECT:
            // Read object properties
            return readObject(parser, ctxt, fieldName);
        case END_ARRAY:
        case END_OBJECT:
            // Skip array and object end markers
            parser.nextToken();
            break;
        default:
            throw ctxt.mappingException(getBeanClass());
        }
        return Collections.emptyMap();
    }

    /**
     * @param string
     * @return true if string is null or is empty (ignore spaces)
     */
    private boolean isEmptyString(String string) {
        return string == null || string.trim().isEmpty();
    }

    /**
     * {@inheritDoc}
     * 
     * Not used
     */
    @Override
    public Object deserializeFromObject(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        // Not used
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * Not used
     */
    @Override
    protected BeanDeserializerBase asArrayDeserializer() {
        // Not used
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * Not used
     */
    @Override
    protected Object _deserializeUsingPropertyBased(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        // Not used
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * Not used
     */
    @Override
    public JsonDeserializer<Object> unwrappingDeserializer(NameTransformer unwrapper) {
        // Not used
        return null;
    }
}