Java tutorial
/* =================================================================== * 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; } }