| /* |
| * 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; |
| |
| /** |
| * <p> |
| * Serializes an object into JavaScript Object Notation (JSON). If cyclic |
| * references are detected they will be nulled out. |
| * </p> |
| */ |
| public class DefaultJSONWriter implements JSONWriter { |
| |
| private static final Logger LOG = LogManager.getLogger(DefaultJSONWriter.class); |
| |
| private static char[] hex = "0123456789ABCDEF".toCharArray(); |
| |
| private static final ConcurrentMap<Class<?>, BeanInfo> BEAN_INFO_CACHE_IGNORE_HIERARCHY = new ConcurrentHashMap<>(); |
| private static final ConcurrentMap<Class<?>, BeanInfo> BEAN_INFO_CACHE = new ConcurrentHashMap<>(); |
| |
| private StringBuilder buf = new StringBuilder(); |
| private Stack<Object> stack = new Stack<>(); |
| private boolean ignoreHierarchy = true; |
| private Object root; |
| private boolean buildExpr = true; |
| private String exprStack = ""; |
| private Collection<Pattern> excludeProperties; |
| private Collection<Pattern> 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<Pattern> excludeProperties, |
| Collection<Pattern> 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<String, String> 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.<br> |
| * 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; |
| } |
| } |
| |
| } |