All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.struts2.json.DefaultJSONWriter Maven / Gradle / Ivy

There is a newer version: 6.6.1
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.struts2.json;

import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.ProxyUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.json.annotations.JSON;
import org.apache.struts2.json.annotations.JSONFieldBridge;
import org.apache.struts2.json.annotations.JSONParameter;
import org.apache.struts2.json.bridge.FieldBridge;
import org.apache.struts2.json.bridge.ParameterizedBridge;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.text.CharacterIterator;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.StringCharacterIterator;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;

/**
 * 

* Serializes an object into JavaScript Object Notation (JSON). If cyclic * references are detected they will be nulled out. *

*/ public class DefaultJSONWriter implements JSONWriter { private static final Logger LOG = LogManager.getLogger(DefaultJSONWriter.class); private static char[] hex = "0123456789ABCDEF".toCharArray(); private static final ConcurrentMap, BeanInfo> BEAN_INFO_CACHE_IGNORE_HIERARCHY = new ConcurrentHashMap<>(); private static final ConcurrentMap, BeanInfo> BEAN_INFO_CACHE = new ConcurrentHashMap<>(); private StringBuilder buf = new StringBuilder(); private Stack stack = new Stack<>(); private boolean ignoreHierarchy = true; private Object root; private boolean buildExpr = true; private String exprStack = ""; private Collection excludeProperties; private Collection includeProperties; private DateFormat formatter; private boolean enumAsBean = ENUM_AS_BEAN_DEFAULT; private boolean excludeNullProperties; private boolean cacheBeanInfo = true; private boolean excludeProxyProperties; @Inject(value = JSONConstants.RESULT_EXCLUDE_PROXY_PROPERTIES, required = false) public void setExcludeProxyProperties(String excludeProxyProperties) { setExcludeProxyProperties(Boolean.parseBoolean(excludeProxyProperties)); } /** * @param object Object to be serialized into JSON * @return JSON string for object * @throws JSONException in case of error during serialize */ @Override public String write(Object object) throws JSONException { return this.write(object, null, null, false); } /** * @param object * Object to be serialized into JSON * @param excludeProperties * Patterns matching properties to ignore * @param includeProperties * Patterns matching properties to include * @param excludeNullProperties * enable/disable excluding of null properties * @return JSON string for object * @throws JSONException in case of error during serialize */ @Override public String write(Object object, Collection excludeProperties, Collection includeProperties, boolean excludeNullProperties) throws JSONException { this.excludeNullProperties = excludeNullProperties; this.buf.setLength(0); this.stack.clear(); this.root = object; this.exprStack = ""; this.buildExpr = ((excludeProperties != null) && !excludeProperties.isEmpty()) || ((includeProperties != null) && !includeProperties.isEmpty()); this.excludeProperties = excludeProperties; this.includeProperties = includeProperties; this.value(object, null); return this.buf.toString(); } /** * Detect cyclic references * * @param object Object to be serialized into JSON * @param method method * * @throws JSONException in case of error during serialize */ protected void value(Object object, Method method) throws JSONException { if (object == null) { this.add("null"); return; } if (this.stack.contains(object)) { Class clazz = object.getClass(); // cyclic reference if (clazz.isPrimitive() || clazz.equals(String.class)) { this.process(object, method); } else { LOG.debug("Cyclic reference detected on {}", object); this.add("null"); } return; } this.process(object, method); } /** * Serialize object into json * * @param object Object to be serialized into JSON * @param method method * * @throws JSONException in case of error during serialize */ protected void process(Object object, Method method) throws JSONException { this.stack.push(object); if (object instanceof Class) { this.string(object); } else if (object instanceof Boolean) { this.bool((Boolean) object); } else if (object instanceof Number) { this.add(object); } else if (object instanceof String) { this.string(object); } else if (object instanceof Character) { this.string(object); } else if (object instanceof Map) { this.map((Map) object, method); } else if (object.getClass().isArray()) { this.array(object, method); } else if (object instanceof Iterable) { this.array(((Iterable) object).iterator(), method); } else if (object instanceof Date) { this.date((Date) object, method); } else if (object instanceof Calendar) { this.date(((Calendar) object).getTime(), method); } else if (object instanceof Locale) { this.string(object); } else if (object instanceof Enum) { this.enumeration((Enum) object); } else { processCustom(object, method); } this.stack.pop(); } /** * Serialize custom object into json * * @param object object * @param method method * * @throws JSONException in case of error during serialize */ protected void processCustom(Object object, Method method) throws JSONException { this.bean(object); } /** * Instrospect bean and serialize its properties * * @param object object * * @throws JSONException in case of error during serialize */ protected void bean(Object object) throws JSONException { this.add("{"); BeanInfo info; try { Class clazz = excludeProxyProperties ? ProxyUtil.ultimateTargetClass(object) : object.getClass(); info = ((object == this.root) && this.ignoreHierarchy) ? getBeanInfoIgnoreHierarchy(clazz) : getBeanInfo(clazz); PropertyDescriptor[] props = info.getPropertyDescriptors(); boolean hasData = false; for (PropertyDescriptor prop : props) { String name = prop.getName(); Method accessor = prop.getReadMethod(); Method baseAccessor = findBaseAccessor(clazz, accessor); if (baseAccessor != null) { if (baseAccessor.isAnnotationPresent(JSON.class)) { JSONAnnotationFinder jsonFinder = new JSONAnnotationFinder(baseAccessor).invoke(); if (!jsonFinder.shouldSerialize()) continue; if (jsonFinder.getName() != null) { name = jsonFinder.getName(); } } // ignore "class" and others if (this.shouldExcludeProperty(prop)) { continue; } String expr = null; if (this.buildExpr) { expr = this.expandExpr(name); if (this.shouldExcludeProperty(expr)) { continue; } expr = this.setExprStack(expr); } Object value = accessor.invoke(object); if (baseAccessor.isAnnotationPresent(JSONFieldBridge.class)) { value = getBridgedValue(baseAccessor, value); } boolean propertyPrinted = this.add(name, value, accessor, hasData); hasData = hasData || propertyPrinted; if (this.buildExpr) { this.setExprStack(expr); } } } // special-case handling for an Enumeration - include the name() as // a property */ if (object instanceof Enum) { Object value = ((Enum) object).name(); this.add("_name", value, object.getClass().getMethod("name"), hasData); } } catch (Exception e) { throw new JSONException(e); } this.add("}"); } protected BeanInfo getBeanInfoIgnoreHierarchy(final Class clazz) throws IntrospectionException { BeanInfo beanInfo = BEAN_INFO_CACHE_IGNORE_HIERARCHY.get(clazz); if (beanInfo != null) { return beanInfo; } beanInfo = Introspector.getBeanInfo(clazz, clazz.getSuperclass()); BEAN_INFO_CACHE_IGNORE_HIERARCHY.put(clazz, beanInfo); return beanInfo; } protected BeanInfo getBeanInfo(final Class clazz) throws IntrospectionException { BeanInfo beanInfo = BEAN_INFO_CACHE.get(clazz); if (beanInfo != null) { return beanInfo; } beanInfo = Introspector.getBeanInfo(clazz); BEAN_INFO_CACHE.put(clazz, beanInfo); return beanInfo; } protected Object getBridgedValue(Method baseAccessor, Object value) throws InstantiationException, IllegalAccessException { JSONFieldBridge fieldBridgeAnn = baseAccessor.getAnnotation(JSONFieldBridge.class); if (fieldBridgeAnn != null) { Class impl = fieldBridgeAnn.impl(); FieldBridge instance = (FieldBridge) impl.newInstance(); if (fieldBridgeAnn.params().length > 0 && ParameterizedBridge.class.isAssignableFrom(impl)) { Map params = new HashMap<>(fieldBridgeAnn.params().length); for (JSONParameter param : fieldBridgeAnn.params()) { params.put(param.name(), param.value()); } ((ParameterizedBridge) instance).setParameterValues(params); } value = instance.objectToString(value); } return value; } protected Method findBaseAccessor(Class clazz, Method accessor) { Method baseAccessor = null; if (clazz.getName().contains("$$EnhancerByCGLIB$$")) { try { baseAccessor = Thread.currentThread().getContextClassLoader().loadClass( clazz.getName().substring(0, clazz.getName().indexOf("$$"))).getMethod( accessor.getName(), accessor.getParameterTypes()); } catch (Exception ex) { LOG.debug(ex.getMessage(), ex); } } else if (clazz.getName().contains("$$_javassist")) { try { baseAccessor = Class.forName( clazz.getName().substring(0, clazz.getName().indexOf("_$$"))) .getMethod(accessor.getName(), accessor.getParameterTypes()); } catch (Exception ex) { LOG.debug(ex.getMessage(), ex); } //in hibernate4.3.7,because javassist3.18.1's class name generate rule is '_$$_jvst'+... } else if(clazz.getName().contains("$$_jvst")){ try { baseAccessor = Class.forName( clazz.getName().substring(0, clazz.getName().indexOf("_$$"))) .getMethod(accessor.getName(), accessor.getParameterTypes()); } catch (Exception ex) { LOG.debug(ex.getMessage(), ex); } } else { return accessor; } return baseAccessor; } /** * Instrospect an Enum and serialize it as a name/value pair or as a bean * including all its own properties * * @param enumeration the enum * * @throws JSONException in case of error during serialize */ protected void enumeration(Enum enumeration) throws JSONException { if (enumAsBean) { this.bean(enumeration); } else { this.string(enumeration.name()); } } protected boolean shouldExcludeProperty(PropertyDescriptor prop) throws SecurityException, NoSuchFieldException { String name = prop.getName(); return name.equals("class") || name.equals("declaringClass") || name.equals("cachedSuperClass") || name.equals("metaClass"); } protected String expandExpr(int i) { return this.exprStack + "[" + i + "]"; } protected String expandExpr(String property) { if (this.exprStack.length() == 0) { return property; } return this.exprStack + "." + property; } protected String setExprStack(String expr) { String s = this.exprStack; this.exprStack = expr; return s; } protected boolean shouldExcludeProperty(String expr) { if (this.excludeProperties != null) { for (Pattern pattern : this.excludeProperties) { if (pattern.matcher(expr).matches()) { if (LOG.isDebugEnabled()) { LOG.debug("Ignoring property because of exclude rule: " + expr); } return true; } } } if (this.includeProperties != null) { for (Pattern pattern : this.includeProperties) { if (pattern.matcher(expr).matches()) { return false; } } if (LOG.isDebugEnabled()){ LOG.debug("Ignoring property because of include rule: " + expr); } return true; } return false; } /* * Add name/value pair to buffer */ protected boolean add(String name, Object value, Method method, boolean hasData) throws JSONException { if (excludeNullProperties && value == null) { return false; } if (hasData) { this.add(','); } this.add('"'); this.add(name); this.add("\":"); this.value(value, method); return true; } /* * Add map to buffer */ protected void map(Map map, Method method) throws JSONException { this.add("{"); Iterator it = map.entrySet().iterator(); boolean warnedNonString = false; // one report per map boolean hasData = false; while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); if (excludeNullProperties && entry.getValue() == null) { continue; } Object key = entry.getKey(); if (key == null) { LOG.error("Cannot build expression for null key in {}", exprStack); continue; } String expr = null; if (this.buildExpr) { expr = this.expandExpr(key.toString()); if (this.shouldExcludeProperty(expr)) { continue; } expr = this.setExprStack(expr); } if (hasData) { this.add(','); } hasData = true; if (!warnedNonString && !(key instanceof String)) { if (LOG.isWarnEnabled()) { LOG.warn("JavaScript doesn't support non-String keys, using toString() on {}", key.getClass().getName()); } warnedNonString = true; } this.value(key.toString(), method); this.add(":"); this.value(entry.getValue(), method); if (this.buildExpr) { this.setExprStack(expr); } } this.add("}"); } /* * Add date to buffer */ protected void date(Date date, Method method) { JSON json = null; if (method != null) json = method.getAnnotation(JSON.class); if (this.formatter == null) this.formatter = new SimpleDateFormat(JSONUtil.RFC3339_FORMAT); DateFormat formatter = (json != null) && (json.format().length() > 0) ? new SimpleDateFormat(json .format()) : this.formatter; this.string(formatter.format(date)); } /* * Add array to buffer */ protected void array(Iterator it, Method method) throws JSONException { this.add("["); boolean hasData = false; for (int i = 0; it.hasNext(); i++) { String expr = null; if (this.buildExpr) { expr = this.expandExpr(i); if (this.shouldExcludeProperty(expr)) { it.next(); continue; } expr = this.setExprStack(expr); } if (hasData) { this.add(','); } hasData = true; this.value(it.next(), method); if (this.buildExpr) { this.setExprStack(expr); } } this.add("]"); } /* * Add array to buffer */ protected void array(Object object, Method method) throws JSONException { this.add("["); int length = Array.getLength(object); boolean hasData = false; for (int i = 0; i < length; ++i) { String expr = null; if (this.buildExpr) { expr = this.expandExpr(i); if (this.shouldExcludeProperty(expr)) { continue; } expr = this.setExprStack(expr); } if (hasData) { this.add(','); } hasData = true; this.value(Array.get(object, i), method); if (this.buildExpr) { this.setExprStack(expr); } } this.add("]"); } /* * Add boolean to buffer */ protected void bool(boolean b) { this.add(b ? "true" : "false"); } /** * escape characters * * @param obj the object to escape */ protected void string(Object obj) { this.add('"'); CharacterIterator it = new StringCharacterIterator(obj.toString()); for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) { if (c == '"') { this.add("\\\""); } else if (c == '\\') { this.add("\\\\"); } else if (c == '/') { this.add("\\/"); } else if (c == '\b') { this.add("\\b"); } else if (c == '\f') { this.add("\\f"); } else if (c == '\n') { this.add("\\n"); } else if (c == '\r') { this.add("\\r"); } else if (c == '\t') { this.add("\\t"); } else if (Character.isISOControl(c)) { this.unicode(c); } else { this.add(c); } } this.add('"'); } /* * Add object to buffer */ protected void add(Object obj) { this.buf.append(obj); } /* * Add char to buffer */ protected void add(char c) { this.buf.append(c); } /** * Represent as unicode * * @param c character to be encoded */ protected void unicode(char c) { this.add("\\u"); int n = c; for (int i = 0; i < 4; ++i) { int digit = (n & 0xf000) >> 12; this.add(hex[digit]); n <<= 4; } } @Override public void setIgnoreHierarchy(boolean ignoreHierarchy) { this.ignoreHierarchy = ignoreHierarchy; } /** * If true, an Enum is serialized as a bean with a special property * _name=name() as all as all other properties defined within the enum.
* If false, an Enum is serialized as a name=value pair (name=name()) * * @param enumAsBean true to serialize an enum as a bean instead of as a name=value * pair (default=false) */ @Override public void setEnumAsBean(boolean enumAsBean) { this.enumAsBean = enumAsBean; } @Override public void setDateFormatter(String defaultDateFormat) { if (defaultDateFormat != null) { this.formatter = new SimpleDateFormat(defaultDateFormat); } } @Override public void setCacheBeanInfo(boolean cacheBeanInfo) { this.cacheBeanInfo = cacheBeanInfo; } @Override public void setExcludeProxyProperties(boolean excludeProxyProperties) { this.excludeProxyProperties = excludeProxyProperties; } protected static class JSONAnnotationFinder { private boolean serialize = true; private Method accessor; private String name; public JSONAnnotationFinder(Method accessor) { this.accessor = accessor; } public boolean shouldSerialize() { return serialize; } public String getName() { return name; } public JSONAnnotationFinder invoke() { JSON json = accessor.getAnnotation(JSON.class); serialize = json.serialize(); if (serialize && json.name().length() > 0) { name = json.name(); } return this; } } }