blob: 4de1e94bc2fe49b6e460028994b9c470ac1e57a6 [file] [log] [blame]
/*
* 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 java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.json.annotations.SMDMethod;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.WildcardUtil;
/**
* Wrapper for JSONWriter with some utility methods.
*/
public class JSONUtil {
public final static String RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
public static final boolean CACHE_BEAN_INFO_DEFAULT = true;
private static final Logger LOG = LogManager.getLogger(JSONUtil.class);
private JSONWriter writer;
public void setWriter(JSONWriter writer) {
this.writer = writer;
}
@Inject
public void setContainer(Container container) {
setWriter(container.getInstance(JSONWriter.class, container.getInstance(String.class,
JSONConstants.JSON_WRITER)));
}
/**
* Serializes an object into JSON.
*
* @param object
* to be serialized
* @param cacheBeanInfo
* Specifies whether to cache bean info in the JSONWriter
* @return JSON string
* @throws JSONException in case of error during serialize
*/
public String serialize(Object object, boolean cacheBeanInfo) throws JSONException {
writer.setCacheBeanInfo(cacheBeanInfo);
return writer.write(object);
}
/**
* Serializes an object into JSON, excluding any properties matching any of
* the regular expressions in the given collection.
*
* @param object
* to be serialized
* @param excludeProperties
* Patterns matching properties to exclude
* @param includeProperties
* Patterns matching properties to include
* @param ignoreHierarchy
* whether to ignore properties defined on base classes of the
* root object
* @param excludeNullProperties
* enable/disable excluding of null properties
* @return JSON string
* @throws JSONException in case of error during serialize
*/
public String serialize(Object object, Collection<Pattern> excludeProperties,
Collection<Pattern> includeProperties, boolean ignoreHierarchy, boolean excludeNullProperties)
throws JSONException {
return serialize(object, excludeProperties, includeProperties,
ignoreHierarchy, excludeNullProperties, CACHE_BEAN_INFO_DEFAULT);
}
/**
* Serializes an object into JSON, excluding any properties matching any of
* the regular expressions in the given collection.
*
* @param object
* to be serialized
* @param excludeProperties
* Patterns matching properties to exclude
* @param includeProperties
* Patterns matching properties to include
* @param ignoreHierarchy
* whether to ignore properties defined on base classes of the
* root object
* @param excludeNullProperties
* enable/disable excluding of null properties
* @param cacheBeanInfo
* Specifies whether to cache bean info in the JSONWriter
* @return JSON string
* @throws JSONException in case of error during serialize
*/
public String serialize(Object object, Collection<Pattern> excludeProperties,
Collection<Pattern> includeProperties, boolean ignoreHierarchy, boolean excludeNullProperties,
boolean cacheBeanInfo)
throws JSONException {
writer.setIgnoreHierarchy(ignoreHierarchy);
writer.setCacheBeanInfo(cacheBeanInfo);
return writer.write(object, excludeProperties, includeProperties, excludeNullProperties);
}
/**
* Serializes an object into JSON, excluding any properties matching any of
* the regular expressions in the given collection.
*
* @param object
* to be serialized
* @param excludeProperties
* Patterns matching properties to exclude
* @param includeProperties
* Patterns matching properties to include
* @param ignoreHierarchy
* whether to ignore properties defined on base classes of the
* root object
* @param enumAsBean
* whether to serialized enums a Bean or name=value pair
* @param excludeNullProperties
* enable/disable excluding of null properties
* @param defaultDateFormat
* date format used to serialize dates
* @return JSON string
* @throws JSONException in case of error during serialize
*/
public String serialize(Object object, Collection<Pattern> excludeProperties,
Collection<Pattern> includeProperties, boolean ignoreHierarchy, boolean enumAsBean,
boolean excludeNullProperties, String defaultDateFormat) throws JSONException {
return serialize(object, excludeProperties, includeProperties, ignoreHierarchy, enumAsBean,
excludeNullProperties, defaultDateFormat, CACHE_BEAN_INFO_DEFAULT);
}
/**
* Serializes an object into JSON, excluding any properties matching any of
* the regular expressions in the given collection.
*
* @param object
* to be serialized
* @param excludeProperties
* Patterns matching properties to exclude
* @param includeProperties
* Patterns matching properties to include
* @param ignoreHierarchy
* whether to ignore properties defined on base classes of the
* root object
* @param enumAsBean
* whether to serialized enums a Bean or name=value pair
* @param excludeNullProperties
* enable/disable excluding of null properties
* @param defaultDateFormat
* date format used to serialize dates
* @param cacheBeanInfo
* Specifies whether to cache bean info in the JSONWriter
* @return JSON string
* @throws JSONException in case of error during serialize
*/
public String serialize(Object object, Collection<Pattern> excludeProperties,
Collection<Pattern> includeProperties, boolean ignoreHierarchy, boolean enumAsBean,
boolean excludeNullProperties, String defaultDateFormat, boolean cacheBeanInfo) throws JSONException {
writer.setIgnoreHierarchy(ignoreHierarchy);
writer.setEnumAsBean(enumAsBean);
writer.setDateFormatter(defaultDateFormat);
writer.setCacheBeanInfo(cacheBeanInfo);
return writer.write(object, excludeProperties, includeProperties, excludeNullProperties);
}
/**
* Serializes an object into JSON to the given writer.
*
* @param writer
* Writer to serialize the object to
* @param object
* object to be serialized
* @throws IOException in case of IO errors
* @throws JSONException in case of error during serialize
*/
public void serialize(Writer writer, Object object) throws IOException, JSONException {
serialize(writer, object, CACHE_BEAN_INFO_DEFAULT);
}
/**
* Serializes an object into JSON to the given writer.
*
* @param writer
* Writer to serialize the object to
* @param object
* object to be serialized
* @param cacheBeanInfo
* Specifies whether to cache bean info in the JSONWriter
* @throws IOException in case of IO errors
* @throws JSONException in case of error during serialize
*/
public void serialize(Writer writer, Object object, boolean cacheBeanInfo) throws IOException, JSONException {
writer.write(serialize(object, cacheBeanInfo));
}
/**
* Serializes an object into JSON to the given writer, excluding any
* properties matching any of the regular expressions in the given
* collection.
*
* @param writer
* Writer to serialize the object to
* @param object
* object to be serialized
* @param excludeProperties
* Patterns matching properties to ignore
* @param includeProperties
* Patterns matching properties to include
* @param excludeNullProperties
* enable/disable excluding of null properties
* @throws IOException in case of IO errors
* @throws JSONException in case of error during serialize
*/
public void serialize(Writer writer, Object object, Collection<Pattern> excludeProperties,
Collection<Pattern> includeProperties, boolean excludeNullProperties) throws IOException,
JSONException {
serialize(writer, object, excludeProperties, includeProperties, excludeNullProperties, CACHE_BEAN_INFO_DEFAULT);
}
/**
* Serializes an object into JSON to the given writer, excluding any
* properties matching any of the regular expressions in the given
* collection.
*
* @param writer
* Writer to serialize the object to
* @param object
* object to be serialized
* @param excludeProperties
* Patterns matching properties to ignore
* @param includeProperties
* Patterns matching properties to include
* @param excludeNullProperties
* enable/disable excluding of null properties
* @param cacheBeanInfo
* Specifies whether to cache bean info in the JSONWriter
* @throws IOException in case of IO errors
* @throws JSONException in case of error during serialize
*/
public void serialize(Writer writer, Object object, Collection<Pattern> excludeProperties,
Collection<Pattern> includeProperties, boolean excludeNullProperties, boolean cacheBeanInfo)
throws IOException, JSONException {
writer.write(serialize(object, excludeProperties, includeProperties, true, excludeNullProperties, cacheBeanInfo));
}
/**
* Deserializes a object from JSON
*
* @param json
* string in JSON
* @return desrialized object
* @throws JSONException in case of error during serialize
*/
public static Object deserialize(String json) throws JSONException {
JSONReader reader = new JSONReader();
return reader.read(json);
}
/**
* Deserializes a object from JSON
*
* @param reader
* Reader to read a JSON string from
* @return deserialized object
* @throws JSONException
* when IOException happens
*/
public static Object deserialize(Reader reader) throws JSONException {
// read content
BufferedReader bufferReader = new BufferedReader(reader);
String line;
StringBuilder buffer = new StringBuilder();
try {
while ((line = bufferReader.readLine()) != null) {
buffer.append(line);
}
} catch (IOException e) {
throw new JSONException(e);
}
return deserialize(buffer.toString());
}
public static void writeJSONToResponse(SerializationParams serializationParams) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
if (StringUtils.isNotBlank(serializationParams.getSerializedJSON()))
stringBuilder.append(serializationParams.getSerializedJSON());
if (StringUtils.isNotBlank(serializationParams.getWrapPrefix()))
stringBuilder.insert(0, serializationParams.getWrapPrefix());
else if (serializationParams.isWrapWithComments()) {
stringBuilder.insert(0, "/* ");
stringBuilder.append(" */");
} else if (serializationParams.isPrefix())
stringBuilder.insert(0, "{}&& ");
if (StringUtils.isNotBlank(serializationParams.getWrapSuffix()))
stringBuilder.append(serializationParams.getWrapSuffix());
String json = stringBuilder.toString();
LOG.debug("[JSON] {}", json);
HttpServletResponse response = serializationParams.getResponse();
// status or error code
if (serializationParams.getStatusCode() > 0)
response.setStatus(serializationParams.getStatusCode());
else if (serializationParams.getErrorCode() > 0)
response.sendError(serializationParams.getErrorCode());
// content type
response.setContentType(serializationParams.getContentType() + ";charset="
+ serializationParams.getEncoding());
if (serializationParams.isNoCache()) {
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
response.setHeader("Pragma", "No-cache");
}
if (serializationParams.isGzip()) {
response.addHeader("Content-Encoding", "gzip");
GZIPOutputStream out = null;
InputStream in = null;
try {
out = new GZIPOutputStream(response.getOutputStream());
in = new ByteArrayInputStream(json.getBytes(serializationParams.getEncoding()));
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
} finally {
if (in != null)
in.close();
if (out != null) {
out.finish();
out.close();
}
}
} else {
response.setContentLength(json.getBytes(serializationParams.getEncoding()).length);
PrintWriter out = response.getWriter();
out.print(json);
}
}
public static Set<String> asSet(String commaDelim) {
if ((commaDelim == null) || (commaDelim.trim().length() == 0))
return null;
return TextParseUtil.commaDelimitedStringToSet(commaDelim);
}
/**
* List visible methods carrying the
*
* {@literal @}SMDMethod annotation
*
* @param clazz
* class
* @param ignoreInterfaces
* if true, only the methods of the class are examined. If false,
* annotations on every interfaces' methods are examined.
* @return array of SMD methods
*/
@SuppressWarnings("unchecked")
public static Method[] listSMDMethods(Class clazz, boolean ignoreInterfaces) {
final List<Method> methods = new LinkedList<>();
if (ignoreInterfaces) {
Method[] SMDMethods = clazz.getMethods();
Arrays.sort(SMDMethods, (a, b) -> a.toString().compareTo(b.toString()));
for (Method method : SMDMethods) {
SMDMethod smdMethodAnnotation = method.getAnnotation(SMDMethod.class);
if (smdMethodAnnotation != null) {
methods.add(method);
}
}
} else {
// recurse the entire superclass/interface hierarchy and add in
// order encountered
JSONUtil.visitInterfaces(clazz, new JSONUtil.ClassVisitor() {
public boolean visit(Class aClass) {
Method[] SMDMethods = aClass.getMethods();
Arrays.sort(SMDMethods, (a, b) -> a.toString().compareTo(b.toString()));
for (Method method : SMDMethods) {
SMDMethod smdMethodAnnotation = method.getAnnotation(SMDMethod.class);
if ((smdMethodAnnotation != null) && !methods.contains(method)) {
methods.add(method);
}
}
return true;
}
});
}
Method[] methodResult = new Method[methods.size()];
return methods.toArray(methodResult);
}
/**
* Realizes the visit(Class) method called by vistInterfaces for all
* encountered classes/interfaces
*/
public static interface ClassVisitor {
/**
* Called when a new interface/class is encountered
*
* @param aClass
* the encountered class/interface
* @return true if the recursion should continue, false to stop
* recursion immediately
*/
@SuppressWarnings("unchecked")
boolean visit(Class aClass);
}
/**
* Visit all the interfaces realized by the specified object, its
* superclasses and its interfaces <br> Visitation is performed in the
* following order: aClass aClass' interfaces the interface's superclasses
* (interfaces) aClass' superclass superclass' interfaces superclass'
* interface's superclasses (interfaces) super-superclass and so on <br> The
* Object base class is base excluded. Classes/interfaces are only visited
* once each
*
* @param aClass
* the class to start recursing upwards from
* @param visitor
* this vistor is called for each class/interface encountered
* @return true if all classes/interfaces were visited, false if it was
* exited early as specified by a ClassVisitor result
*/
@SuppressWarnings("unchecked")
public static boolean visitInterfaces(Class aClass, ClassVisitor visitor) {
List<Class> classesVisited = new LinkedList<>();
return visitUniqueInterfaces(aClass, visitor, classesVisited);
}
/*
* Recursive method to visit all the interfaces of a class (and its
* superclasses and super-interfaces) if they haven't already been visited.
* <br> Always visits itself if it hasn't already been visited
*
* @param thisClass
* the current class to visit (if not already done so)
* @param classesVisited
* classes already visited
* @param visitor
* this vistor is called for each class/interface encountered
* @return true if recursion can continue, false if it should be aborted
*/
private static boolean visitUniqueInterfaces(Class thisClass, ClassVisitor visitor,
List<Class> classesVisited) {
boolean okayToContinue = true;
if (!classesVisited.contains(thisClass)) {
classesVisited.add(thisClass);
okayToContinue = visitor.visit(thisClass);
if (okayToContinue) {
Class[] interfaces = thisClass.getInterfaces();
int index = 0;
while ((index < interfaces.length) && (okayToContinue)) {
okayToContinue = visitUniqueInterfaces(interfaces[index++], visitor, classesVisited);
}
if (okayToContinue) {
Class superClass = thisClass.getSuperclass();
if ((superClass != null) && (!Object.class.equals(superClass))) {
okayToContinue = visitUniqueInterfaces(superClass, visitor, classesVisited);
}
}
}
}
return okayToContinue;
}
public static boolean isGzipInRequest(HttpServletRequest request) {
return StringUtils.contains(request.getHeader("Accept-Encoding"), "gzip");
}
public static final String REGEXP_PATTERN = "regexp";
public static final String WILDCARD_PATTERN = "wildcard";
/* package */ static final String SPLIT_PATTERN = "split";
/* package */ static final String JOIN_STRING = "join";
/* package */ static final String ARRAY_BEGIN_STRING = "array-begin";
/* package */ static final String ARRAY_END_STRING = "array-end";
/* package */ static Map<String, Map<String, String>> getIncludePatternData()
{
Map<String, Map<String, String>> includePatternData = new HashMap<>();
Map<String, String> data = new HashMap<>();
data.put(REGEXP_PATTERN, "\\\\\\.");
data.put(WILDCARD_PATTERN, "\\.");
includePatternData.put(SPLIT_PATTERN, data);
data = new HashMap<>();
data.put(REGEXP_PATTERN, "\\.");
data.put(WILDCARD_PATTERN, ".");
includePatternData.put(JOIN_STRING, data);
data = new HashMap<>();
data.put(REGEXP_PATTERN, "\\[");
data.put(WILDCARD_PATTERN, "[");
includePatternData.put(ARRAY_BEGIN_STRING, data);
data = new HashMap<>();
data.put(REGEXP_PATTERN, "\\]");
data.put(WILDCARD_PATTERN, "]");
includePatternData.put(ARRAY_END_STRING, data);
return includePatternData;
}
private static final Map<String, Map<String, String>> defaultIncludePatternData = getIncludePatternData();
public static List<Pattern> processIncludePatterns(Set<String> includePatterns, String type) {
return processIncludePatterns(includePatterns, type, defaultIncludePatternData);
}
/* package */ static List<Pattern> processIncludePatterns(Set<String> includePatterns, String type, Map<String, Map<String, String>> includePatternData) {
if (includePatterns != null) {
List<Pattern> results = new ArrayList<>(includePatterns.size());
Map<String, String> existingPatterns = new HashMap<>();
for (String pattern : includePatterns) {
processPattern(results, existingPatterns, pattern, type, includePatternData);
}
return results;
} else {
return null;
}
}
private static void processPattern(List<Pattern> results, Map<String, String> existingPatterns, String pattern, String type, Map<String, Map<String, String>> includePatternData) {
// Compile a pattern for each *unique* "level" of the object
// hierarchy specified in the regex.
String[] patternPieces = pattern.split(includePatternData.get(SPLIT_PATTERN).get(type));
String patternExpr = "";
for (String patternPiece : patternPieces) {
patternExpr = processPatternPiece(results, existingPatterns, patternExpr, patternPiece, type, includePatternData);
}
}
private static String processPatternPiece(List<Pattern> results, Map<String, String> existingPatterns, String patternExpr, String patternPiece, String type, Map<String, Map<String, String>> includePatternData) {
if (patternExpr.length() > 0) {
patternExpr += includePatternData.get(JOIN_STRING).get(type);
}
patternExpr += patternPiece;
// Check for duplicate patterns so that there is no overlap.
if (!existingPatterns.containsKey(patternExpr)) {
existingPatterns.put(patternExpr, patternExpr);
if (isIndexedProperty(patternPiece, type, includePatternData)) {
addPattern(results, patternExpr.substring(0, patternExpr.lastIndexOf(includePatternData.get(ARRAY_BEGIN_STRING).get(type))), type);
}
addPattern(results, patternExpr, type);
}
return patternExpr;
}
/*
* Add a pattern that does not have the indexed property matching (ie. list\[\d+\] becomes list).
*/
private static boolean isIndexedProperty(String patternPiece, String type, Map<String, Map<String, String>> includePatternData) {
return patternPiece.endsWith(includePatternData.get(ARRAY_END_STRING).get(type));
}
private static void addPattern(List<Pattern> results, String pattern, String type) {
results.add(REGEXP_PATTERN.equals(type) ? Pattern.compile(pattern) : WildcardUtil.compileWildcardPattern(pattern));
if (LOG.isTraceEnabled()) {
LOG.trace("Adding include {} expression: {}", (REGEXP_PATTERN.equals(type) ? "property" : "wildcard"), pattern);
}
}
}