/*
* gvNIX is an open source tool for rapid application development (RAD).
* Copyright (C) 2010 Generalitat Valenciana
*
* 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 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 <a href="http://www.disid.com">DISID Corporation S.L.</a> made for <a
* href="http://www.dgti.gva.es">General Directorate for Information
* Technologies (DGTI)</a>
* @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;
}
}