/* * 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.HashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.commons.lang3.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; /** * Jackson Serializer which generates a tree with binding and validation * property errors stored on a {@link BindingResult} object. * <p/> * Error messages will be {@link FieldError#getDefaultMessage()} so, it is * already translated to (current request) language (or supposed to). * <p/> * JSON generated for {@link List} binding errors: * * <pre> * { * OBJECT_INDEX : { FIELD1_NAME : FIELD_ERROR_MSG, FIELD2_NAME : FIELD_ERROR_MSG, ...}, * OBJECT_INDEX2 : { FIELD1_NAME : FIELD_ERROR_MSG, * FIELD_OBJECT_NAME : { SUBOBJECT_FIELD: FIELD_ERROR_MSG, ... } * FIELD_LIST_NAME: { * OBJECT_FIELD_ITEM_INDEX : {ITEM_LIST_FIELD: FIELD_ERROR_MSG, ... }, * OBJECT_FIELD_ITEM_INDEX2 : {ITEM_LIST_FIELD: FIELD_ERROR_MSG, ... }, * }, * ... * }, * ... * } * </pre> * * JSON for object binding errors: * * <pre> * { FIELD1_NAME : FIELD_ERROR_MSG, * FIELD_OBJECT_NAME : { SUBOBJECT_FIELD: FIELD_ERROR_MSG, ... } * FIELD_LIST_NAME: { * OBJECT_FIELD_ITEM_INDEX : {ITEM_LIST_FIELD: FIELD_ERROR_MSG, ... }, * OBJECT_FIELD_ITEM_INDEX2 : {ITEM_LIST_FIELD: FIELD_ERROR_MSG, ... }, * }, * ... * } * </pre> * * @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 BindingResultSerializer extends JsonSerializer<Object> { private static final Logger LOGGER = LoggerFactory .getLogger(BindingResultSerializer.class); private static final String ERROR_WRITTING_BINDING = "Error writting BindingResult"; /** * {@inheritDoc} */ @Override public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { try { // Cast to BindingResult BindingResult result = (BindingResult) value; // Create the result map Map<String, Object> allErrorsMessages = new HashMap<String, Object>(); // Get field errors List<FieldError> fieldErrors = result.getFieldErrors(); if (fieldErrors.isEmpty()) { // Nothing to do jgen.writeNull(); return; } // Check if target type is an array or a bean @SuppressWarnings("rawtypes") Class targetClass = result.getTarget().getClass(); if (targetClass.isArray() || Collection.class.isAssignableFrom(targetClass)) { loadListErrors(result.getFieldErrors(), allErrorsMessages); } else { loadObjectErrors(result.getFieldErrors(), allErrorsMessages); } jgen.writeObject(allErrorsMessages); } catch (JsonProcessingException e) { LOGGER.warn(ERROR_WRITTING_BINDING, e); throw e; } catch (IOException e) { LOGGER.warn(ERROR_WRITTING_BINDING, e); throw e; } catch (Exception e) { LOGGER.warn(ERROR_WRITTING_BINDING, e); throw new IOException(ERROR_WRITTING_BINDING, e); } } /** * Iterate over object errors and load it on allErrorsMessages map. * <p/> * Delegates on {@link #loadObjectError(FieldError, String, Map)} * * @param fieldErrors * @param allErrorsMessages */ private void loadObjectErrors(List<FieldError> fieldErrors, Map<String, Object> allErrorsMessages) { for (FieldError error : fieldErrors) { loadObjectError(error, error.getField(), allErrorsMessages); } } /** * Iterate over list items errors and load it on allErrorsMessages map. * <p/> * Delegates on {@link #loadObjectError(FieldError, String, Map)} * * @param fieldErrors * @param allErrorsMessages */ @SuppressWarnings("unchecked") private void loadListErrors(List<FieldError> fieldErrors, Map<String, Object> allErrorsMessages) { // Get prefix to unwrapping list: // "list[0].employedSince" String fieldNamePath = fieldErrors.get(0).getField(); // "list" String prefix = StringUtils.substringBefore(fieldNamePath, "["); String index; Map<String, Object> currentErrors; // Iterate over errors for (FieldError error : fieldErrors) { // get property path without list prefix // "[0].employedSince" fieldNamePath = StringUtils .substringAfter(error.getField(), prefix); // Get item's index: // "[0].employedSince" index = StringUtils.substringBefore( StringUtils.substringAfter(fieldNamePath, "["), "]"); // Remove index definition from field path // "employedSince" fieldNamePath = StringUtils.substringAfter(fieldNamePath, "."); // Check if this item already has errors registered currentErrors = (Map<String, Object>) allErrorsMessages.get(index); if (currentErrors == null) { // No errors registered: create map to contain this error currentErrors = new HashMap<String, Object>(); allErrorsMessages.put(index, currentErrors); } // Load error on item's map loadObjectError(error, fieldNamePath, currentErrors); } } /** * Loads an object field error in errors map. * <p/> * This method identifies if referred object property is an array, an object * or a simple property to decide how to store the error message. * * @param error * @param fieldNamePath * @param objectErrors */ @SuppressWarnings("unchecked") private void loadObjectError(FieldError error, String fieldNamePath, Map<String, Object> objectErrors) { String propertyName; boolean isObject = false; boolean isList = false; // Get this property name and if is a object property if (StringUtils.contains(fieldNamePath, ".")) { isObject = true; propertyName = StringUtils.substringBefore(fieldNamePath, "."); } else { isObject = false; propertyName = fieldNamePath; } // Check if property is an array or a list isList = StringUtils.contains(propertyName, "["); // Process a list item property if (isList) { // Get property name String listPropertyName = StringUtils.substringBefore(propertyName, "["); // Get referred item index String index = StringUtils.substringBefore( StringUtils.substringAfter(propertyName, "["), "]"); // Get item path String itemPath = StringUtils.substringAfter(fieldNamePath, "."); // Get container of list property errors Map<String, Object> listErrors = (Map<String, Object>) objectErrors .get(listPropertyName); if (listErrors == null) { // property has no errors yet: create a container for it listErrors = new HashMap<String, Object>(); objectErrors.put(listPropertyName, listErrors); } // Get current item errors Map<String, Object> itemErrors = (Map<String, Object>) listErrors .get(index); if (itemErrors == null) { // item has no errors yet: create a container for it itemErrors = new HashMap<String, Object>(); listErrors.put(index, itemErrors); } // Load error in item property path loadObjectError(error, itemPath, itemErrors); } else if (isObject) { // It's not a list but it has properties in it value // Get current property errors Map<String, Object> propertyErrors = (Map<String, Object>) objectErrors .get(propertyName); if (propertyErrors == null) { // item has no errors yet: create a container for it propertyErrors = new HashMap<String, Object>(); objectErrors.put(propertyName, propertyErrors); } // Get error sub path String subFieldPath = StringUtils .substringAfter(fieldNamePath, "."); // Load error in container loadObjectError(error, subFieldPath, propertyErrors); } else { // standard property with no children value // Store error message in container objectErrors.put(propertyName, error.getDefaultMessage()); } } }