From 39bdb99a116080a56242dc33303c4ab6d2b28f8f Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Mon, 11 Sep 2017 18:09:32 +0430 Subject: [PATCH] WW-4034 Refactors and extracts interface from JSONWriter --- .../struts2/json/DefaultJSONWriter.java | 704 ++++++++++++++++++ .../org/apache/struts2/json/JSONUtil.java | 2 +- .../org/apache/struts2/json/JSONWriter.java | 700 +---------------- .../json/src/main/resources/struts-plugin.xml | 2 +- ...erTest.java => DefaultJSONWriterTest.java} | 22 +- .../org/apache/struts2/json/JSONEnumTest.java | 4 +- .../apache/struts2/json/JSONResultTest.java | 2 +- 7 files changed, 743 insertions(+), 693 deletions(-) create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java rename plugins/json/src/test/java/org/apache/struts2/json/{JSONWriterTest.java => DefaultJSONWriterTest.java} (85%) diff --git a/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java b/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java new file mode 100644 index 000000000..bdee41664 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java @@ -0,0 +1,704 @@ +/* + * $Id$ + * + * 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.*; +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 = false; + + @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; + } + } + +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java index c650e0626..feb8594f2 100644 --- a/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java @@ -60,7 +60,7 @@ public class JSONUtil { private static final Logger LOG = LogManager.getLogger(JSONUtil.class); - private static JSONWriter writer = new JSONWriter(); + private static JSONWriter writer = new DefaultJSONWriter(); @Inject public static void setWriter(JSONWriter writer) { diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONWriter.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONWriter.java index 00396c5c1..48b5e3c5a 100644 --- a/plugins/json/src/main/java/org/apache/struts2/json/JSONWriter.java +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONWriter.java @@ -1,698 +1,44 @@ /* - * $Id$ + * Copyright 2017 The Apache Software Foundation. * - * 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 + * Licensed 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 + * 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. + * 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.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import java.util.Collection; import java.util.regex.Pattern; /** *

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

*/ -public class JSONWriter { +public interface JSONWriter { + boolean ENUM_AS_BEAN_DEFAULT = false; - private static final Logger LOG = LogManager.getLogger(JSONWriter.class); + String write(Object object) throws JSONException; - /** - * By default, enums are serialised as name=value pairs - */ - public static final boolean ENUM_AS_BEAN_DEFAULT = false; + String write(Object object, Collection excludeProperties, + Collection includeProperties, boolean excludeNullProperties) throws JSONException; - private static char[] hex = "0123456789ABCDEF".toCharArray(); + void setIgnoreHierarchy(boolean ignoreHierarchy); - private static final ConcurrentMap, BeanInfo> BEAN_INFO_CACHE_IGNORE_HIERARCHY = new ConcurrentHashMap<>(); - private static final ConcurrentMap, BeanInfo> BEAN_INFO_CACHE = new ConcurrentHashMap<>(); + void setEnumAsBean(boolean enumAsBean); - 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 = false; + void setDateFormatter(String defaultDateFormat); - @Inject(value = JSONConstants.RESULT_EXCLUDE_PROXY_PROPERTIES, required = false) - public void setExcludeProxyProperties(String excludeProxyProperties) { - this.excludeProxyProperties = Boolean.parseBoolean(excludeProxyProperties); - } - - /** - * @param object Object to be serialized into JSON - * @return JSON string for object - * @throws JSONException in case of error during serialize - */ - 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 - */ - 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; - } - } - - 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) - */ - public void setEnumAsBean(boolean enumAsBean) { - this.enumAsBean = enumAsBean; - } - - public void setDateFormatter(String defaultDateFormat) { - if (defaultDateFormat != null) { - this.formatter = new SimpleDateFormat(defaultDateFormat); - } - } - - public void setCacheBeanInfo(boolean cacheBeanInfo) { - this.cacheBeanInfo = cacheBeanInfo; - } - - 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; - } - } + void setCacheBeanInfo(boolean cacheBeanInfo); + void setExcludeProxyProperties(boolean excludeProxyProperties); } diff --git a/plugins/json/src/main/resources/struts-plugin.xml b/plugins/json/src/main/resources/struts-plugin.xml index 01fc1bd30..ec00c1c52 100644 --- a/plugins/json/src/main/resources/struts-plugin.xml +++ b/plugins/json/src/main/resources/struts-plugin.xml @@ -5,7 +5,7 @@ "http://struts.apache.org/dtds/struts-2.5.dtd"> - + diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONWriterTest.java b/plugins/json/src/test/java/org/apache/struts2/json/DefaultJSONWriterTest.java similarity index 85% rename from plugins/json/src/test/java/org/apache/struts2/json/JSONWriterTest.java rename to plugins/json/src/test/java/org/apache/struts2/json/DefaultJSONWriterTest.java index 2a9d733cd..6e55fb8f1 100644 --- a/plugins/json/src/test/java/org/apache/struts2/json/JSONWriterTest.java +++ b/plugins/json/src/test/java/org/apache/struts2/json/DefaultJSONWriterTest.java @@ -13,7 +13,7 @@ import java.util.List; import java.util.Map; import java.util.TimeZone; -public class JSONWriterTest extends StrutsTestCase{ +public class DefaultJSONWriterTest extends StrutsTestCase{ @Test public void testWrite() throws Exception { Bean bean1=new Bean(); @@ -27,10 +27,10 @@ public class JSONWriterTest extends StrutsTestCase{ bean1.setEnumField(AnEnum.ValueA); bean1.setEnumBean(AnEnumBean.Two); - JSONWriter jsonWriter = new JSONWriter(); + JSONWriter jsonWriter = new DefaultJSONWriter(); jsonWriter.setEnumAsBean(false); String json = jsonWriter.write(bean1); - TestUtils.assertEquals(JSONWriter.class.getResource("jsonwriter-write-bean-01.txt"), json); + TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-01.txt"), json); } @Test @@ -52,11 +52,11 @@ public class JSONWriterTest extends StrutsTestCase{ m.put("c", "z"); bean1.setMap(m); - JSONWriter jsonWriter = new JSONWriter(); + JSONWriter jsonWriter = new DefaultJSONWriter(); jsonWriter.setEnumAsBean(false); jsonWriter.setIgnoreHierarchy(false); String json = jsonWriter.write(bean1, null, null, true); - TestUtils.assertEquals(JSONWriter.class.getResource("jsonwriter-write-bean-03.txt"), json); + TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-03.txt"), json); } private class BeanWithMap extends Bean{ @@ -85,11 +85,11 @@ public class JSONWriterTest extends StrutsTestCase{ bean1.setEnumBean(AnEnumBean.Two); bean1.setUrl(new URL("http://www.google.com")); - JSONWriter jsonWriter = new JSONWriter(); + JSONWriter jsonWriter = new DefaultJSONWriter(); jsonWriter.setEnumAsBean(false); jsonWriter.setIgnoreHierarchy(false); String json = jsonWriter.write(bean1); - TestUtils.assertEquals(JSONWriter.class.getResource("jsonwriter-write-bean-02.txt"), json); + TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-02.txt"), json); } @Test @@ -108,11 +108,11 @@ public class JSONWriterTest extends StrutsTestCase{ errors.add("Field is required"); bean1.setErrors(errors); - JSONWriter jsonWriter = new JSONWriter(); + JSONWriter jsonWriter = new DefaultJSONWriter(); jsonWriter.setEnumAsBean(false); jsonWriter.setIgnoreHierarchy(false); String json = jsonWriter.write(bean1); - TestUtils.assertEquals(JSONWriter.class.getResource("jsonwriter-write-bean-04.txt"), json); + TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-04.txt"), json); } private class BeanWithList extends Bean { @@ -147,7 +147,7 @@ public class JSONWriterTest extends StrutsTestCase{ SingleDateBean dateBean = new SingleDateBean(); dateBean.setDate(sdf.parse("2012-12-23 10:10:10 GMT")); - JSONWriter jsonWriter = new JSONWriter(); + JSONWriter jsonWriter = new DefaultJSONWriter(); jsonWriter.setEnumAsBean(false); TimeZone.setDefault(TimeZone.getTimeZone("GMT")); @@ -162,7 +162,7 @@ public class JSONWriterTest extends StrutsTestCase{ SingleDateBean dateBean = new SingleDateBean(); dateBean.setDate(sdf.parse("2012-12-23 10:10:10 GMT")); - JSONWriter jsonWriter = new JSONWriter(); + JSONWriter jsonWriter = new DefaultJSONWriter(); jsonWriter.setEnumAsBean(false); jsonWriter.setDateFormatter("MM-dd-yyyy"); String json = jsonWriter.write(dateBean); diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java index 0db7010c6..7d07f516a 100644 --- a/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java @@ -46,7 +46,7 @@ public class JSONEnumTest extends TestCase { bean1.setEnumField(AnEnum.ValueA); bean1.setEnumBean(AnEnumBean.Two); - JSONWriter jsonWriter = new JSONWriter(); + JSONWriter jsonWriter = new DefaultJSONWriter(); jsonWriter.setEnumAsBean(false); String json = jsonWriter.write(bean1); @@ -88,7 +88,7 @@ public class JSONEnumTest extends TestCase { bean1.setEnumField(AnEnum.ValueA); bean1.setEnumBean(AnEnumBean.Two); - JSONWriter jsonWriter = new JSONWriter(); + JSONWriter jsonWriter = new DefaultJSONWriter(); jsonWriter.setEnumAsBean(true); String json = jsonWriter.write(bean1); diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java index edf61545a..397384108 100644 --- a/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java @@ -71,7 +71,7 @@ public class JSONResultTest extends StrutsTestCase { public void testJSONWriterEndlessLoopOnExludedProperties() throws JSONException { Pattern all = Pattern.compile(".*"); - JSONWriter writer = new JSONWriter(); + JSONWriter writer = new DefaultJSONWriter(); writer.write(Arrays.asList("a", "b"), Arrays.asList(all), null, false); }