/* =================================================================== * JSONView.java * * Created Jan 3, 2007 12:20:21 PM * * Copyright (c) 2007 Matt Magoffin (spamsqr@msqr.us) * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * =================================================================== * $Id$ * =================================================================== */ package net.solarnetwork.web.support; import java.beans.PropertyDescriptor; import java.beans.PropertyEditor; import java.io.IOException; import java.io.Writer; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.solarnetwork.util.SerializeIgnore; import org.springframework.beans.BeanWrapper; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.beans.PropertyEditorRegistrar; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; /** * View to return JSON encoded data. * * <p> * The view model is turned into a complete JSON object. The model keys become * JSON object keys, and the model values the corresponding JSON object values. * Array and Collection object values will be rendered as JSON array values. * Primitive types will render as JSOM primitive values (numbers, strings). * Objects will be treated as JavaBeans and the bean properties will be used to * render nested JSON objects. * </p> * * <p> * All object values are handled in a recursive fashion, so array, collection, * and bean property values will be rendered accordingly. * </p> * * <p> * The JSON encoding is constructed in a streaming fashion, so object graphs of * arbitrary size should not cause any memory-related errors. * </p> * * <p> * The configurable properties of this class are: * </p> * * <dl> * <dt>indentAmount</dt> * <dd>The number of spaces to indent (pretty print) the JSON output with. If * set to zero no indentation will be added (this is the default).</dd> * * <dt>includeParentheses</dt> * <dd>If true, the entire response will be enclosed in parentheses, required * for JSON evaluation support in certain browsers. Defaults to <em>false</em>.</dd> * * <dt>propertyEditorRegistrar</dt> * <dd>An optional registrar of PropertyEditor instances that can be used to * serialize specific objects into String values. This can be useful for * formatting Date objects into strings, for example.</dd> * * </dl> * * @author Matt Magoffin * @version $Revision$ $Date$ */ public class JSONView extends AbstractView { /** The default content type: application/json;charset=UTF-8. */ public static final String JSON_CONTENT_TYPE = "application/json;charset=UTF-8"; /** The default character encoding used: UTF-8. */ public static final String UTF8_CHAR_ENCODING = "UTF-8"; private int indentAmount = 0; private boolean includeParentheses = false; private PropertyEditorRegistrar propertyEditorRegistrar = null; /** * Default constructor. */ public JSONView() { setContentType(JSON_CONTENT_TYPE); } @Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { PropertyEditorRegistrar registrar = this.propertyEditorRegistrar; Enumeration<String> attrEnum = request.getAttributeNames(); while ( attrEnum.hasMoreElements() ) { String key = attrEnum.nextElement(); Object val = request.getAttribute(key); if ( val instanceof PropertyEditorRegistrar ) { registrar = (PropertyEditorRegistrar) val; break; } } response.setCharacterEncoding(UTF8_CHAR_ENCODING); response.setContentType(getContentType()); Writer writer = response.getWriter(); if ( this.includeParentheses ) { writer.write('('); } JsonGenerator json = new JsonFactory().createGenerator(writer); json.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); if ( indentAmount > 0 ) { json.useDefaultPrettyPrinter(); } json.writeStartObject(); for ( String key : model.keySet() ) { Object val = model.get(key); writeJsonValue(json, key, val, registrar); } json.writeEndObject(); json.close(); if ( this.includeParentheses ) { writer.write(')'); } } private Collection<?> getPrimitiveCollection(Object array) { int len = Array.getLength(array); List<Object> result = new ArrayList<Object>(len); for ( int i = 0; i < len; i++ ) { result.add(Array.get(array, i)); } return result; } private void writeJsonValue(JsonGenerator json, String key, Object val, PropertyEditorRegistrar registrar) throws JsonGenerationException, IOException { if ( val instanceof Collection<?> || (val != null && val.getClass().isArray()) ) { Collection<?> col; if ( val instanceof Collection<?> ) { col = (Collection<?>) val; } else if ( !val.getClass().getComponentType().isPrimitive() ) { col = Arrays.asList((Object[]) val); } else { // damn you, primitives col = getPrimitiveCollection(val); } if ( key != null ) { json.writeFieldName(key); } json.writeStartArray(); for ( Object colObj : col ) { writeJsonValue(json, null, colObj, registrar); } json.writeEndArray(); } else if ( val instanceof Map<?, ?> ) { if ( key != null ) { json.writeFieldName(key); } json.writeStartObject(); for ( Map.Entry<?, ?> me : ((Map<?, ?>) val).entrySet() ) { Object propName = me.getKey(); if ( propName == null ) { continue; } writeJsonValue(json, propName.toString(), me.getValue(), registrar); } json.writeEndObject(); } else if ( val instanceof Double ) { if ( key == null ) { json.writeNumber((Double) val); } else { json.writeNumberField(key, (Double) val); } } else if ( val instanceof Integer ) { if ( key == null ) { json.writeNumber((Integer) val); } else { json.writeNumberField(key, (Integer) val); } } else if ( val instanceof Short ) { if ( key == null ) { json.writeNumber(((Short) val).intValue()); } else { json.writeNumberField(key, ((Short) val).intValue()); } } else if ( val instanceof Float ) { if ( key == null ) { json.writeNumber((Float) val); } else { json.writeNumberField(key, (Float) val); } } else if ( val instanceof Long ) { if ( key == null ) { json.writeNumber((Long) val); } else { json.writeNumberField(key, (Long) val); } } else if ( val instanceof Boolean ) { if ( key == null ) { json.writeBoolean((Boolean) val); } else { json.writeBooleanField(key, (Boolean) val); } } else if ( val instanceof String ) { if ( key == null ) { json.writeString((String) val); } else { json.writeStringField(key, (String) val); } } else { // create a JSON object from bean properties if ( getPropertySerializerRegistrar() != null && val != null ) { // try whole-bean serialization first Object o = getPropertySerializerRegistrar().serializeProperty(key, val.getClass(), val, val); if ( o != val ) { if ( o != null ) { writeJsonValue(json, key, o, registrar); } return; } } generateJavaBeanObject(json, key, val, registrar); } } private void generateJavaBeanObject(JsonGenerator json, String key, Object bean, PropertyEditorRegistrar registrar) throws JsonGenerationException, IOException { if ( key != null ) { json.writeFieldName(key); } if ( bean == null ) { json.writeNull(); return; } BeanWrapper wrapper = getPropertyAccessor(bean, registrar); PropertyDescriptor[] props = wrapper.getPropertyDescriptors(); json.writeStartObject(); for ( PropertyDescriptor prop : props ) { String name = prop.getName(); if ( this.getJavaBeanIgnoreProperties() != null && this.getJavaBeanIgnoreProperties().contains(name) ) { continue; } if ( wrapper.isReadableProperty(name) ) { Object propVal = wrapper.getPropertyValue(name); if ( propVal != null ) { // test for SerializeIgnore Method getter = prop.getReadMethod(); if ( getter != null && getter.isAnnotationPresent(SerializeIgnore.class) ) { continue; } if ( getPropertySerializerRegistrar() != null ) { propVal = getPropertySerializerRegistrar().serializeProperty(name, propVal.getClass(), bean, propVal); } else { // Spring does not apply PropertyEditors on read methods, so manually handle PropertyEditor editor = wrapper.findCustomEditor(null, name); if ( editor != null ) { editor.setValue(propVal); propVal = editor.getAsText(); } } if ( propVal instanceof Enum<?> || getJavaBeanTreatAsStringValues() != null && getJavaBeanTreatAsStringValues().contains(propVal.getClass()) ) { propVal = propVal.toString(); } writeJsonValue(json, name, propVal, registrar); } } } json.writeEndObject(); } private BeanWrapper getPropertyAccessor(Object obj, PropertyEditorRegistrar registrar) { BeanWrapper bean = PropertyAccessorFactory.forBeanPropertyAccess(obj); if ( registrar != null ) { registrar.registerCustomEditors(bean); } return bean; } public int getIndentAmount() { return indentAmount; } public void setIndentAmount(int indentAmount) { this.indentAmount = indentAmount; } public boolean isIncludeParentheses() { return includeParentheses; } public void setIncludeParentheses(boolean includeParentheses) { this.includeParentheses = includeParentheses; } public PropertyEditorRegistrar getPropertyEditorRegistrar() { return propertyEditorRegistrar; } public void setPropertyEditorRegistrar(PropertyEditorRegistrar propertyEditorRegistrar) { this.propertyEditorRegistrar = propertyEditorRegistrar; } }