WW-5618 feat(json): add configurable limits to JSON plugin (#1625)

Add configurable limits to the JSON plugin to prevent denial-of-service
attacks via malicious payloads (deeply nested objects, huge arrays, long
strings).

Changes:
- Extract JSONReader interface from class, create StrutsJSONReader impl
  with maxElements, maxDepth, maxStringLength, maxKeyLength enforcement
- Rename DefaultJSONWriter to StrutsJSONWriter (Struts* naming convention)
- Add JSONBeanSelectionProvider for bean aliasing via constants
- Update JSONUtil with @Inject for reader/writer, add instance
  deserializeInput() with maxLength check, deprecate static deserialize()
- Wire limits into JSONInterceptor with @Inject from constants
- Register beans and defaults in struts-plugin.xml

Default limits: 10K elements, 64 depth, 2MB length, 256KB strings, 512 keys.
All configurable via struts.xml constants or per-action interceptor params.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukasz Lenart
2026-03-21 12:11:06 +01:00
committed by GitHub
parent 311cda1681
commit 8ac63e535a
19 changed files with 1314 additions and 367 deletions
@@ -0,0 +1,35 @@
/*
* 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 org.apache.struts2.config.AbstractBeanSelectionProvider;
import org.apache.struts2.config.ConfigurationException;
import org.apache.struts2.inject.ContainerBuilder;
import org.apache.struts2.inject.Scope;
import org.apache.struts2.util.location.LocatableProperties;
public class JSONBeanSelectionProvider extends AbstractBeanSelectionProvider {
@Override
public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException {
alias(JSONReader.class, JSONConstants.JSON_READER, builder, props, Scope.PROTOTYPE);
alias(JSONWriter.class, JSONConstants.JSON_WRITER, builder, props, Scope.PROTOTYPE);
}
}
@@ -29,6 +29,12 @@ package org.apache.struts2.json;
public class JSONConstants {
public static final String JSON_WRITER = "struts.json.writer";
public static final String JSON_READER = "struts.json.reader";
public static final String RESULT_EXCLUDE_PROXY_PROPERTIES = "struts.json.result.excludeProxyProperties";
public static final String DATE_FORMAT = "struts.json.dateformat";
public static final String JSON_MAX_ELEMENTS = "struts.json.maxElements";
public static final String JSON_MAX_DEPTH = "struts.json.maxDepth";
public static final String JSON_MAX_LENGTH = "struts.json.maxLength";
public static final String JSON_MAX_STRING_LENGTH = "struts.json.maxStringLength";
public static final String JSON_MAX_KEY_LENGTH = "struts.json.maxKeyLength";
}
@@ -71,6 +71,13 @@ public class JSONInterceptor extends AbstractInterceptor {
private String jsonContentType = "application/json";
private String jsonRpcContentType = "application/json-rpc";
private JSONUtil jsonUtil;
private int maxElements = JSONReader.DEFAULT_MAX_ELEMENTS;
private int maxDepth = JSONReader.DEFAULT_MAX_DEPTH;
private int maxLength = 2_097_152; // 2MB
private int maxStringLength = JSONReader.DEFAULT_MAX_STRING_LENGTH;
private int maxKeyLength = JSONReader.DEFAULT_MAX_KEY_LENGTH;
@SuppressWarnings("unchecked")
public String intercept(ActionInvocation invocation) throws Exception {
HttpServletRequest request = ServletActionContext.getRequest();
@@ -91,7 +98,8 @@ public class JSONInterceptor extends AbstractInterceptor {
if (jsonContentType.equalsIgnoreCase(requestContentType)) {
// load JSON object
Object obj = JSONUtil.deserialize(request.getReader());
applyLimitsToReader();
Object obj = jsonUtil.deserializeInput(request.getReader(), maxLength);
// JSON array (this.root cannot be null in this case)
if(obj instanceof List && this.root != null) {
@@ -133,7 +141,8 @@ public class JSONInterceptor extends AbstractInterceptor {
Object result;
if (this.enableSMD) {
// load JSON object
Object obj = JSONUtil.deserialize(request.getReader());
applyLimitsToReader();
Object obj = jsonUtil.deserializeInput(request.getReader(), maxLength);
if (obj instanceof Map) {
Map smd = (Map) obj;
@@ -168,8 +177,6 @@ public class JSONInterceptor extends AbstractInterceptor {
result = rpcResponse;
}
JSONUtil jsonUtil = invocation.getInvocationContext().getContainer().getInstance(JSONUtil.class);
String json = jsonUtil.serialize(result, excludeProperties, getIncludeProperties(),
ignoreHierarchy, excludeNullProperties);
json = addCallbackIfApplicable(request, json);
@@ -185,6 +192,14 @@ public class JSONInterceptor extends AbstractInterceptor {
return invocation.invoke();
}
private void applyLimitsToReader() {
JSONReader reader = jsonUtil.getReader();
reader.setMaxElements(maxElements);
reader.setMaxDepth(maxDepth);
reader.setMaxStringLength(maxStringLength);
reader.setMaxKeyLength(maxKeyLength);
}
protected String readContentType(HttpServletRequest request) {
String contentType = request.getHeader("Content-Type");
LOG.debug("Content Type from request: {}", contentType);
@@ -564,4 +579,34 @@ public class JSONInterceptor extends AbstractInterceptor {
public void setJsonRpcContentType(String jsonRpcContentType) {
this.jsonRpcContentType = jsonRpcContentType;
}
@Inject
public void setJsonUtil(JSONUtil jsonUtil) {
this.jsonUtil = jsonUtil;
}
@Inject(value = JSONConstants.JSON_MAX_ELEMENTS, required = false)
public void setMaxElements(String maxElements) {
this.maxElements = Integer.parseInt(maxElements);
}
@Inject(value = JSONConstants.JSON_MAX_DEPTH, required = false)
public void setMaxDepth(String maxDepth) {
this.maxDepth = Integer.parseInt(maxDepth);
}
@Inject(value = JSONConstants.JSON_MAX_LENGTH, required = false)
public void setMaxLength(String maxLength) {
this.maxLength = Integer.parseInt(maxLength);
}
@Inject(value = JSONConstants.JSON_MAX_STRING_LENGTH, required = false)
public void setMaxStringLength(String maxStringLength) {
this.maxStringLength = Integer.parseInt(maxStringLength);
}
@Inject(value = JSONConstants.JSON_MAX_KEY_LENGTH, required = false)
public void setMaxKeyLength(String maxKeyLength) {
this.maxKeyLength = Integer.parseInt(maxKeyLength);
}
}
@@ -18,285 +18,25 @@
*/
package org.apache.struts2.json;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <p>
* Deserializes and object from a JSON string
* Deserializes an object from a JSON string.
* </p>
*/
public class JSONReader {
private static final Object OBJECT_END = new Object();
private static final Object ARRAY_END = new Object();
private static final Object COLON = new Object();
private static final Object COMMA = new Object();
private static Map<Character, Character> escapes = new HashMap<Character, Character>();
public interface JSONReader {
static {
escapes.put('"', '"');
escapes.put('\\', '\\');
escapes.put('/', '/');
escapes.put('b', '\b');
escapes.put('f', '\f');
escapes.put('n', '\n');
escapes.put('r', '\r');
escapes.put('t', '\t');
}
int DEFAULT_MAX_ELEMENTS = 10_000;
int DEFAULT_MAX_DEPTH = 64;
int DEFAULT_MAX_STRING_LENGTH = 262_144; // 256KB
int DEFAULT_MAX_KEY_LENGTH = 512;
private CharacterIterator it;
private char c;
private Object token;
private StringBuilder buf = new StringBuilder();
Object read(String string) throws JSONException;
protected char next() {
this.c = this.it.next();
void setMaxElements(int maxElements);
return this.c;
}
void setMaxDepth(int maxDepth);
protected void skipWhiteSpace() {
while (Character.isWhitespace(this.c)) {
this.next();
}
}
void setMaxStringLength(int maxStringLength);
public Object read(String string) throws JSONException {
this.it = new StringCharacterIterator(string);
this.c = this.it.first();
return this.read();
}
protected Object read() throws JSONException {
Object ret;
this.skipWhiteSpace();
if (this.c == '"') {
this.next();
ret = this.string('"');
} else if (this.c == '\'') {
this.next();
ret = this.string('\'');
} else if (this.c == '[') {
this.next();
ret = this.array();
} else if (this.c == ']') {
ret = ARRAY_END;
this.next();
} else if (this.c == ',') {
ret = COMMA;
this.next();
} else if (this.c == '{') {
this.next();
ret = this.object();
} else if (this.c == '}') {
ret = OBJECT_END;
this.next();
} else if (this.c == ':') {
ret = COLON;
this.next();
} else if ((this.c == 't') && (this.next() == 'r') && (this.next() == 'u') && (this.next() == 'e')) {
ret = Boolean.TRUE;
this.next();
} else if ((this.c == 'f') && (this.next() == 'a') && (this.next() == 'l') && (this.next() == 's')
&& (this.next() == 'e')) {
ret = Boolean.FALSE;
this.next();
} else if ((this.c == 'n') && (this.next() == 'u') && (this.next() == 'l') && (this.next() == 'l')) {
ret = null;
this.next();
} else if (Character.isDigit(this.c) || (this.c == '-')) {
ret = this.number();
} else {
throw buildInvalidInputException();
}
this.token = ret;
return ret;
}
@SuppressWarnings("unchecked")
protected Map object() throws JSONException {
Map ret = new HashMap();
Object next = this.read();
if (next != OBJECT_END) {
String key = (String) next;
while (this.token != OBJECT_END) {
this.read(); // should be a colon
if (this.token != OBJECT_END) {
ret.put(key, this.read());
if (this.read() == COMMA) {
Object name = this.read();
if (name instanceof String) {
key = (String) name;
} else
throw buildInvalidInputException();
}
}
}
}
return ret;
}
protected JSONException buildInvalidInputException() {
return new JSONException("Input string is not well formed JSON (invalid char " + this.c + ")");
}
@SuppressWarnings("unchecked")
protected List array() throws JSONException {
List ret = new ArrayList();
Object value = this.read();
while (this.token != ARRAY_END) {
ret.add(value);
Object read = this.read();
if (read == COMMA) {
value = this.read();
} else if (read != ARRAY_END) {
throw buildInvalidInputException();
}
}
return ret;
}
protected Object number() throws JSONException {
this.buf.setLength(0);
boolean toDouble = false;
if (this.c == '-') {
this.add();
}
this.addDigits();
if (this.c == '.') {
toDouble = true;
this.add();
this.addDigits();
}
if ((this.c == 'e') || (this.c == 'E')) {
toDouble = true;
this.add();
if ((this.c == '+') || (this.c == '-')) {
this.add();
}
this.addDigits();
}
if (toDouble) {
try {
return Double.parseDouble(this.buf.toString());
} catch (NumberFormatException e) {
throw buildInvalidInputException();
}
} else {
try {
return Long.parseLong(this.buf.toString());
} catch (NumberFormatException e) {
throw buildInvalidInputException();
}
}
}
protected Object string(char quote) {
this.buf.setLength(0);
while ((this.c != quote) && (this.c != CharacterIterator.DONE)) {
if (this.c == '\\') {
this.next();
if (this.c == 'u') {
this.add(this.unicode());
} else {
Object value = escapes.get(this.c);
if (value != null) {
this.add((Character) value);
}
}
} else {
this.add();
}
}
this.next();
return this.buf.toString();
}
protected void add(char cc) {
this.buf.append(cc);
this.next();
}
protected void add() {
this.add(this.c);
}
protected void addDigits() {
while (Character.isDigit(this.c)) {
this.add();
}
}
protected char unicode() {
int value = 0;
for (int i = 0; i < 4; ++i) {
switch (this.next()) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
value = (value << 4) + (this.c - '0');
break;
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
value = (value << 4) + (this.c - 'W');
break;
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
value = (value << 4) + (this.c - '7');
break;
}
}
return (char) value;
}
void setMaxKeyLength(int maxKeyLength);
}
@@ -40,7 +40,6 @@ import java.util.Arrays;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.struts2.inject.Container;
import org.apache.struts2.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
@@ -60,16 +59,21 @@ public class JSONUtil {
private static final Logger LOG = LogManager.getLogger(JSONUtil.class);
private JSONReader reader;
private JSONWriter writer;
public void setWriter(JSONWriter writer) {
this.writer = writer;
@Inject
public void setReader(JSONReader reader) {
this.reader = reader;
}
public JSONReader getReader() {
return reader;
}
@Inject
public void setContainer(Container container) {
setWriter(container.getInstance(JSONWriter.class, container.getInstance(String.class,
JSONConstants.JSON_WRITER)));
public void setWriter(JSONWriter writer) {
this.writer = writer;
}
/**
@@ -284,6 +288,34 @@ public class JSONUtil {
writer.write(serialize(object, excludeProperties, includeProperties, true, excludeNullProperties, cacheBeanInfo));
}
/**
* Deserializes an object from JSON using the injected reader with limit enforcement.
*
* @param reader Reader to read a JSON string from
* @param maxLength maximum allowed length of the JSON input
* @return deserialized object
* @throws JSONException when IOException happens or limits are exceeded
*/
public Object deserializeInput(Reader reader, int maxLength) throws JSONException {
BufferedReader bufferReader = new BufferedReader(reader);
String line;
StringBuilder buffer = new StringBuilder();
try {
while ((line = bufferReader.readLine()) != null) {
buffer.append(line);
if (buffer.length() > maxLength) {
throw new JSONException("JSON input exceeds maximum allowed length ("
+ maxLength + "). Use " + JSONConstants.JSON_MAX_LENGTH + " to increase the limit.");
}
}
} catch (IOException e) {
throw new JSONException(e);
}
return this.reader.read(buffer.toString());
}
/**
* Deserializes a object from JSON
*
@@ -291,9 +323,11 @@ public class JSONUtil {
* string in JSON
* @return desrialized object
* @throws JSONException in case of error during serialize
* @deprecated Use instance method {@link #deserializeInput(Reader, int)} with injected JSONUtil instead
*/
@Deprecated( forRemoval = true, since = "7.2.0")
public static Object deserialize(String json) throws JSONException {
JSONReader reader = new JSONReader();
StrutsJSONReader reader = new StrutsJSONReader();
return reader.read(json);
}
@@ -305,7 +339,9 @@ public class JSONUtil {
* @return deserialized object
* @throws JSONException
* when IOException happens
* @deprecated Use instance method {@link #deserializeInput(Reader, int)} with injected JSONUtil instead
*/
@Deprecated( forRemoval = true, since = "7.2.0")
public static Object deserialize(Reader reader) throws JSONException {
// read content
BufferedReader bufferReader = new BufferedReader(reader);
@@ -0,0 +1,338 @@
/*
* 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.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <p>
* Deserializes an object from a JSON string with configurable limits
* to prevent denial-of-service attacks via malicious payloads.
* </p>
*/
public class StrutsJSONReader implements JSONReader {
private static final Object OBJECT_END = new Object();
private static final Object ARRAY_END = new Object();
private static final Object COLON = new Object();
private static final Object COMMA = new Object();
private static final Map<Character, Character> escapes = Map.of(
'"', '"',
'\\', '\\',
'/', '/',
'b', '\b',
'f', '\f',
'n', '\n',
'r', '\r',
't', '\t'
);
private CharacterIterator it;
private char c;
private Object token;
private final StringBuilder buf = new StringBuilder();
private int maxElements = DEFAULT_MAX_ELEMENTS;
private int maxDepth = DEFAULT_MAX_DEPTH;
private int maxStringLength = DEFAULT_MAX_STRING_LENGTH;
private int maxKeyLength = DEFAULT_MAX_KEY_LENGTH;
private int depth;
@Override
public void setMaxElements(int maxElements) {
this.maxElements = maxElements;
}
@Override
public void setMaxDepth(int maxDepth) {
this.maxDepth = maxDepth;
}
@Override
public void setMaxStringLength(int maxStringLength) {
this.maxStringLength = maxStringLength;
}
@Override
public void setMaxKeyLength(int maxKeyLength) {
this.maxKeyLength = maxKeyLength;
}
protected char next() {
this.c = this.it.next();
return this.c;
}
protected void skipWhiteSpace() {
while (Character.isWhitespace(this.c)) {
this.next();
}
}
@Override
public Object read(String string) throws JSONException {
this.it = new StringCharacterIterator(string);
this.c = this.it.first();
this.depth = 0;
return this.read();
}
protected Object read() throws JSONException {
Object ret;
this.skipWhiteSpace();
if (this.c == '"') {
this.next();
ret = this.string('"');
} else if (this.c == '\'') {
this.next();
ret = this.string('\'');
} else if (this.c == '[') {
this.next();
ret = this.array();
} else if (this.c == ']') {
ret = ARRAY_END;
this.next();
} else if (this.c == ',') {
ret = COMMA;
this.next();
} else if (this.c == '{') {
this.next();
ret = this.object();
} else if (this.c == '}') {
ret = OBJECT_END;
this.next();
} else if (this.c == ':') {
ret = COLON;
this.next();
} else if ((this.c == 't') && (this.next() == 'r') && (this.next() == 'u') && (this.next() == 'e')) {
ret = Boolean.TRUE;
this.next();
} else if ((this.c == 'f') && (this.next() == 'a') && (this.next() == 'l') && (this.next() == 's')
&& (this.next() == 'e')) {
ret = Boolean.FALSE;
this.next();
} else if ((this.c == 'n') && (this.next() == 'u') && (this.next() == 'l') && (this.next() == 'l')) {
ret = null;
this.next();
} else if (Character.isDigit(this.c) || (this.c == '-')) {
ret = this.number();
} else {
throw buildInvalidInputException();
}
this.token = ret;
return ret;
}
protected Map<String, Object> object() throws JSONException {
if (this.depth >= this.maxDepth) {
throw new JSONException("JSON object nesting exceeds maximum allowed depth ("
+ this.maxDepth + "). Use " + JSONConstants.JSON_MAX_DEPTH + " to increase the limit.");
}
this.depth++;
try {
Map<String, Object> ret = new HashMap<>();
Object next = this.read();
if (next != OBJECT_END) {
String key = (String) next;
validateKeyLength(key);
while (this.token != OBJECT_END) {
this.read(); // should be a colon
if (this.token != OBJECT_END) {
if (ret.size() >= this.maxElements) {
throw new JSONException("JSON object exceeds maximum allowed elements ("
+ this.maxElements + "). Use " + JSONConstants.JSON_MAX_ELEMENTS + " to increase the limit.");
}
ret.put(key, this.read());
if (this.read() == COMMA) {
Object name = this.read();
if (name instanceof String nextKey) {
key = nextKey;
validateKeyLength(key);
} else {
throw buildInvalidInputException();
}
}
}
}
}
return ret;
} finally {
this.depth--;
}
}
private void validateKeyLength(String key) throws JSONException {
if (key.length() > this.maxKeyLength) {
throw new JSONException("JSON object key exceeds maximum allowed length ("
+ this.maxKeyLength + "). Use " + JSONConstants.JSON_MAX_KEY_LENGTH + " to increase the limit.");
}
}
protected JSONException buildInvalidInputException() {
return new JSONException("Input string is not well formed JSON (invalid char " + this.c + ")");
}
protected List<Object> array() throws JSONException {
if (this.depth >= this.maxDepth) {
throw new JSONException("JSON array nesting exceeds maximum allowed depth ("
+ this.maxDepth + "). Use " + JSONConstants.JSON_MAX_DEPTH + " to increase the limit.");
}
this.depth++;
try {
List<Object> ret = new ArrayList<>();
Object value = this.read();
while (this.token != ARRAY_END) {
if (ret.size() >= this.maxElements) {
throw new JSONException("JSON array exceeds maximum allowed elements ("
+ this.maxElements + "). Use " + JSONConstants.JSON_MAX_ELEMENTS + " to increase the limit.");
}
ret.add(value);
Object read = this.read();
if (read == COMMA) {
value = this.read();
} else if (read != ARRAY_END) {
throw buildInvalidInputException();
}
}
return ret;
} finally {
this.depth--;
}
}
protected Object number() throws JSONException {
this.buf.setLength(0);
boolean toDouble = false;
if (this.c == '-') {
this.add();
}
this.addDigits();
if (this.c == '.') {
toDouble = true;
this.add();
this.addDigits();
}
if ((this.c == 'e') || (this.c == 'E')) {
toDouble = true;
this.add();
if ((this.c == '+') || (this.c == '-')) {
this.add();
}
this.addDigits();
}
if (toDouble) {
try {
return Double.parseDouble(this.buf.toString());
} catch (NumberFormatException e) {
throw buildInvalidInputException();
}
} else {
try {
return Long.parseLong(this.buf.toString());
} catch (NumberFormatException e) {
throw buildInvalidInputException();
}
}
}
protected Object string(char quote) throws JSONException {
this.buf.setLength(0);
while ((this.c != quote) && (this.c != CharacterIterator.DONE)) {
if (this.c == '\\') {
this.next();
if (this.c == 'u') {
this.add(this.unicode());
} else {
Character value = escapes.get(this.c);
if (value != null) {
this.add(value);
}
}
} else {
this.add();
}
if (this.buf.length() > this.maxStringLength) {
throw new JSONException("JSON string exceeds maximum allowed length ("
+ this.maxStringLength + "). Use " + JSONConstants.JSON_MAX_STRING_LENGTH + " to increase the limit.");
}
}
this.next();
return this.buf.toString();
}
protected void add(char cc) {
this.buf.append(cc);
this.next();
}
protected void add() {
this.add(this.c);
}
protected void addDigits() {
while (Character.isDigit(this.c)) {
this.add();
}
}
protected char unicode() {
int value = 0;
for (int i = 0; i < 4; ++i) {
value = switch (this.next()) {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> (value << 4) + (this.c - '0');
case 'a', 'b', 'c', 'd', 'e', 'f' -> (value << 4) + (this.c - 'W');
case 'A', 'B', 'C', 'D', 'E', 'F' -> (value << 4) + (this.c - '7');
default -> value;
};
}
return (char) value;
}
}
@@ -67,9 +67,9 @@ import java.util.regex.Pattern;
* references are detected they will be nulled out.
* </p>
*/
public class DefaultJSONWriter implements JSONWriter {
public class StrutsJSONWriter implements JSONWriter {
private static final Logger LOG = LogManager.getLogger(DefaultJSONWriter.class);
private static final Logger LOG = LogManager.getLogger(StrutsJSONWriter.class);
private static final char[] hex = "0123456789ABCDEF".toCharArray();
@@ -27,16 +27,28 @@ import org.apache.struts2.json.JSONConstants;
public class JSONConstantConfig extends ConstantConfig {
private BeanConfig jsonWriter;
private BeanConfig jsonReader;
private Boolean jsonResultExcludeProxyProperties;
private String jsonDateFormat;
private Integer jsonMaxElements;
private Integer jsonMaxDepth;
private Integer jsonMaxLength;
private Integer jsonMaxStringLength;
private Integer jsonMaxKeyLength;
@Override
public Map<String, String> getAllAsStringsMap() {
Map<String, String> map = super.getAllAsStringsMap();
map.put(JSONConstants.JSON_WRITER, beanConfToString(jsonWriter));
map.put(JSONConstants.JSON_READER, beanConfToString(jsonReader));
map.put(JSONConstants.RESULT_EXCLUDE_PROXY_PROPERTIES, Objects.toString(jsonResultExcludeProxyProperties, null));
map.put(JSONConstants.DATE_FORMAT, jsonDateFormat);
map.put(JSONConstants.JSON_MAX_ELEMENTS, Objects.toString(jsonMaxElements, null));
map.put(JSONConstants.JSON_MAX_DEPTH, Objects.toString(jsonMaxDepth, null));
map.put(JSONConstants.JSON_MAX_LENGTH, Objects.toString(jsonMaxLength, null));
map.put(JSONConstants.JSON_MAX_STRING_LENGTH, Objects.toString(jsonMaxStringLength, null));
map.put(JSONConstants.JSON_MAX_KEY_LENGTH, Objects.toString(jsonMaxKeyLength, null));
return map;
}
@@ -68,4 +80,56 @@ public class JSONConstantConfig extends ConstantConfig {
public void setJsonDateFormat(String jsonDateFormat) {
this.jsonDateFormat = jsonDateFormat;
}
public BeanConfig getJsonReader() {
return jsonReader;
}
public void setJsonReader(BeanConfig jsonReader) {
this.jsonReader = jsonReader;
}
public void setJsonReader(Class<?> clazz) {
this.jsonReader = new BeanConfig(clazz, clazz.getName());
}
public Integer getJsonMaxElements() {
return jsonMaxElements;
}
public void setJsonMaxElements(Integer jsonMaxElements) {
this.jsonMaxElements = jsonMaxElements;
}
public Integer getJsonMaxDepth() {
return jsonMaxDepth;
}
public void setJsonMaxDepth(Integer jsonMaxDepth) {
this.jsonMaxDepth = jsonMaxDepth;
}
public Integer getJsonMaxLength() {
return jsonMaxLength;
}
public void setJsonMaxLength(Integer jsonMaxLength) {
this.jsonMaxLength = jsonMaxLength;
}
public Integer getJsonMaxStringLength() {
return jsonMaxStringLength;
}
public void setJsonMaxStringLength(Integer jsonMaxStringLength) {
this.jsonMaxStringLength = jsonMaxStringLength;
}
public Integer getJsonMaxKeyLength() {
return jsonMaxKeyLength;
}
public void setJsonMaxKeyLength(Integer jsonMaxKeyLength) {
this.jsonMaxKeyLength = jsonMaxKeyLength;
}
}
@@ -24,12 +24,20 @@
"https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
<bean type="org.apache.struts2.json.JSONWriter" name="struts" class="org.apache.struts2.json.DefaultJSONWriter"
<bean type="org.apache.struts2.json.JSONWriter" name="struts" class="org.apache.struts2.json.StrutsJSONWriter"
scope="prototype"/>
<bean type="org.apache.struts2.json.JSONReader" name="struts" class="org.apache.struts2.json.StrutsJSONReader"
scope="prototype"/>
<constant name="struts.json.writer" value="struts"/>
<!-- TODO: Make DefaultJSONWriter thread-safe to remove "prototype"s -->
<bean class="org.apache.struts2.json.JSONUtil" scope="prototype"/>
<constant name="struts.json.writer" value="struts"/>
<constant name="struts.json.reader" value="struts"/>
<constant name="struts.json.maxElements" value="10000"/>
<constant name="struts.json.maxDepth" value="64"/>
<constant name="struts.json.maxLength" value="2097152"/>
<constant name="struts.json.maxStringLength" value="262144"/>
<constant name="struts.json.maxKeyLength" value="512"/>
<package name="json-default" extends="struts-default">
<result-types>
@@ -54,4 +62,6 @@
</interceptors>
</package>
<bean-selection name="jsonBeans" class="org.apache.struts2.json.JSONBeanSelectionProvider"/>
</struts>
@@ -44,11 +44,11 @@ public class JSONEnumTest extends TestCase {
bean1.setEnumField(AnEnum.ValueA);
bean1.setEnumBean(AnEnumBean.Two);
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
String json = jsonWriter.write(bean1);
Map result = (Map) JSONUtil.deserialize(json);
Map result = (Map) new StrutsJSONReader().read(json);
assertEquals("str", result.get("stringField"));
assertEquals(true, result.get("booleanField"));
assertEquals("s", result.get("charField")); // note: this is a
@@ -86,11 +86,11 @@ public class JSONEnumTest extends TestCase {
bean1.setEnumField(AnEnum.ValueA);
bean1.setEnumBean(AnEnumBean.Two);
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(true);
String json = jsonWriter.write(bean1);
Map result = (Map) JSONUtil.deserialize(json);
Map result = (Map) new StrutsJSONReader().read(json);
assertEquals("str", result.get("stringField"));
assertEquals(true, result.get("booleanField"));
assertEquals("s", result.get("charField")); // note: this is a
@@ -41,6 +41,15 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.setContent(content.getBytes());
}
private JSONInterceptor createInterceptor() {
JSONInterceptor interceptor = new JSONInterceptor();
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setReader(new StrutsJSONReader());
jsonUtil.setWriter(new StrutsJSONWriter());
interceptor.setJsonUtil(jsonUtil);
return interceptor;
}
public void testBadJSON1() throws Exception {
tryBadJSON("bad-1.txt");
}
@@ -70,7 +79,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent(fileName);
this.request.addHeader("Content-Type", "application/json; charset=UTF-8");
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest1 action = new SMDActionTest1();
@@ -91,7 +100,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-3.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
SMDActionTest1 action = new SMDActionTest1();
this.invocation.setAction(action);
@@ -110,7 +119,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-14.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest2 action = new SMDActionTest2();
@@ -128,7 +137,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-15.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest2 action = new SMDActionTest2();
@@ -146,7 +155,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-4.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest1 action = new SMDActionTest1();
@@ -170,7 +179,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-9.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest1 action = new SMDActionTest1();
@@ -191,7 +200,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-6.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest1 action = new SMDActionTest1();
@@ -226,7 +235,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-10.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest2 action = new SMDActionTest2();
@@ -251,7 +260,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-7.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest1 action = new SMDActionTest1();
@@ -300,7 +309,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.addHeader("Content-Type", "application/json");
// interceptor
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
TestAction action = new TestAction();
this.invocation.setAction(action);
@@ -315,7 +324,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.addHeader("Content-Type", "application/json");
// interceptor
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
TestAction action = new TestAction();
this.invocation.setAction(action);
@@ -437,7 +446,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.addHeader("Content-Type", "application/json");
// interceptor
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setRoot("bean");
TestAction4 action = new TestAction4();
@@ -462,7 +471,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.addHeader("Content-Type", "application/json");
// interceptor
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setRoot("beans");
TestAction5 action = new TestAction5();
@@ -488,7 +497,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.addHeader("Content-Type", "application/json");
// interceptor
JSONInterceptor interceptor = new JSONInterceptor();
JSONInterceptor interceptor = createInterceptor();
interceptor.setRoot("anotherBean.yetAnotherBean.beans");
TestAction5 action = new TestAction5();
@@ -509,6 +518,63 @@ public class JSONInterceptorTest extends StrutsTestCase {
assertEquals(beans.get(0).getByteField(), 3);
}
public void testMaxLengthExceededThrows() throws Exception {
// Body is 27 bytes, set maxLength to 10
this.request.setContent("{\"a\":1, \"b\":2, \"c\":\"hello\"}".getBytes());
this.request.addHeader("Content-Type", "application/json");
JSONInterceptor interceptor = createInterceptor();
interceptor.setMaxLength("10");
TestAction action = new TestAction();
this.invocation.setAction(action);
try {
interceptor.intercept(this.invocation);
fail("Should have thrown JSONException for exceeding maxLength");
} catch (JSONException e) {
assertTrue(e.getMessage().contains(JSONConstants.JSON_MAX_LENGTH));
}
}
public void testMaxDepthEnforcedThroughInterceptor() throws Exception {
// Nested 3 levels deep, set maxDepth to 2
this.request.setContent("{\"a\":{\"b\":{\"c\":1}}}".getBytes());
this.request.addHeader("Content-Type", "application/json");
JSONInterceptor interceptor = createInterceptor();
interceptor.setMaxDepth("2");
TestAction action = new TestAction();
this.invocation.setAction(action);
try {
interceptor.intercept(this.invocation);
fail("Should have thrown JSONException for exceeding maxDepth");
} catch (JSONException e) {
assertTrue(e.getMessage().contains("maximum allowed depth"));
}
}
public void testMaxElementsEnforcedThroughInterceptor() throws Exception {
// JSON object with 5 keys, set maxElements to 3
this.request.setContent("{\"a\":1, \"b\":2, \"c\":3, \"d\":4, \"e\":5}".getBytes());
this.request.addHeader("Content-Type", "application/json");
JSONInterceptor interceptor = createInterceptor();
interceptor.setMaxElements("3");
TestAction action = new TestAction();
this.invocation.setAction(action);
try {
interceptor.intercept(this.invocation);
fail("Should have thrown JSONException for exceeding maxElements");
} catch (JSONException e) {
assertTrue(e.getMessage().contains("maximum allowed elements"));
}
}
@Override
protected void setUp() throws Exception {
super.setUp();
@@ -22,7 +22,6 @@ import org.apache.struts2.junit.util.TestUtils;
import org.junit.Test;
import java.beans.IntrospectionException;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -74,9 +73,8 @@ public class JSONPopulatorTest {
@Test
public void testPrimitiveBean() throws Exception {
StringReader stringReader = new StringReader(TestUtils.readContent(JSONInterceptorTest.class
.getResource("json-7.txt")));
Object json = JSONUtil.deserialize(stringReader);
String text = TestUtils.readContent(JSONInterceptorTest.class.getResource("json-7.txt"));
Object json = new StrutsJSONReader().read(text);
assertNotNull(json);
assertTrue(json instanceof Map);
Map<?, ?> jsonMap = (Map<?, ?>) json;
@@ -96,7 +94,7 @@ public class JSONPopulatorTest {
@Test
public void testObjectBean() throws Exception {
String text = TestUtils.readContent(JSONInterceptorTest.class.getResource("json-7.txt"));
Object json = JSONUtil.deserialize(text);
Object json = new StrutsJSONReader().read(text);
assertNotNull(json);
assertTrue(json instanceof Map);
Map<?, ?> jsonMap = (Map<?, ?>) json;
@@ -162,9 +160,8 @@ public class JSONPopulatorTest {
@Test
public void testObjectBeanWithStrings() throws Exception {
StringReader stringReader = new StringReader(TestUtils.readContent(JSONInterceptorTest.class
.getResource("json-8.txt")));
Object json = JSONUtil.deserialize(stringReader);
String text = TestUtils.readContent(JSONInterceptorTest.class.getResource("json-8.txt"));
Object json = new StrutsJSONReader().read(text);
assertNotNull(json);
assertTrue(json instanceof Map);
Map<?, ?> jsonMap = (Map<?, ?>) json;
@@ -187,7 +184,7 @@ public class JSONPopulatorTest {
@Test
public void testInfiniteLoop() {
try {
JSONReader reader = new JSONReader();
JSONReader reader = new StrutsJSONReader();
reader.read("[1,\"a]");
fail("Should have thrown an exception");
} catch (JSONException e) {
@@ -198,7 +195,7 @@ public class JSONPopulatorTest {
@Test
public void testParseBadInput() {
try {
JSONReader reader = new JSONReader();
JSONReader reader = new StrutsJSONReader();
reader.read("[1,\"a\"1]");
fail("Should have thrown an exception");
} catch (JSONException e) {
@@ -29,7 +29,7 @@ import static org.junit.Assert.assertNotNull;
* Time: 17.26
*/
public class JSONReaderTest {
private JSONReader reader = new JSONReader();
private StrutsJSONReader reader = new StrutsJSONReader();
@Test
public void testExponentialNumber() throws Exception {
@@ -66,7 +66,7 @@ public class JSONResultTest extends StrutsTestCase {
map.put("createtime", new Date());
try {
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
jsonUtil.serialize(map, JSONUtil.CACHE_BEAN_INFO_DEFAULT);
} catch (JSONException e) {
fail(e.getMessage());
@@ -76,14 +76,14 @@ public class JSONResultTest extends StrutsTestCase {
public void testJSONWriterEndlessLoopOnExludedProperties() throws JSONException {
Pattern all = Pattern.compile(".*");
JSONWriter writer = new DefaultJSONWriter();
JSONWriter writer = new StrutsJSONWriter();
writer.write(Arrays.asList("a", "b"), Arrays.asList(all), null, false);
}
public void testSMDDisabledSMD() throws Exception {
JSONResult result = new JSONResult();
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
SMDActionTest1 action = new SMDActionTest1();
stack.push(action);
@@ -102,7 +102,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setEnableSMD(true);
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
SMDActionTest1 action = new SMDActionTest1();
stack.push(action);
@@ -122,7 +122,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setEnableSMD(true);
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
SMDActionTest2 action = new SMDActionTest2();
stack.push(action);
@@ -142,7 +142,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setExcludeNullProperties(true);
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -161,7 +161,7 @@ public class JSONResultTest extends StrutsTestCase {
public void testNotTraverseOrIncludeProxyInfo() throws Exception {
JSONResult result = new JSONResult();
JSONUtil jsonUtil = new JSONUtil();
DefaultJSONWriter writer = new DefaultJSONWriter();
StrutsJSONWriter writer = new StrutsJSONWriter();
writer.setProxyService(proxyService);
jsonUtil.setWriter(writer);
result.setJsonUtil(jsonUtil);
@@ -195,7 +195,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setWrapPrefix("_prefix_");
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction2 action = new TestAction2();
stack.push(action);
@@ -214,7 +214,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setWrapSuffix("_suffix_");
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction2 action = new TestAction2();
stack.push(action);
@@ -233,7 +233,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setDefaultDateFormat("MM-dd-yyyy");
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
@@ -254,7 +254,7 @@ public class JSONResultTest extends StrutsTestCase {
result.setWrapPrefix("_prefix_");
result.setWrapSuffix("_suffix_");
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction2 action = new TestAction2();
stack.push(action);
@@ -274,7 +274,7 @@ public class JSONResultTest extends StrutsTestCase {
result.setExcludeNullProperties(true);
result.setPrefix(true);
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -294,7 +294,7 @@ public class JSONResultTest extends StrutsTestCase {
public void test() throws Exception {
JSONResult result = new JSONResult();
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
@@ -380,7 +380,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setIgnoreHierarchy(false);
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction3 action = new TestAction3();
@@ -399,7 +399,7 @@ public class JSONResultTest extends StrutsTestCase {
public void testCommentWrap() throws Exception {
JSONResult result = new JSONResult();
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
@@ -481,7 +481,7 @@ public class JSONResultTest extends StrutsTestCase {
private void executeTest2Action(JSONResult result) throws Exception {
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -526,7 +526,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setCallbackParameter("callback");
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
request.addParameter("callback", "exec");
@@ -543,7 +543,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setNoCache(true);
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
executeTest2Action(result);
@@ -557,7 +557,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setContentType("some_super_content");
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
executeTest2Action(result);
@@ -569,7 +569,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setStatusCode(HttpServletResponse.SC_CONTINUE);
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
executeTest2Action(result);
@@ -584,7 +584,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setEnumAsBean(true);
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
executeTest2Action(result);
@@ -605,7 +605,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setIncludeProperties("foo");
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -625,7 +625,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setIncludeProperties("^list\\[\\d+\\]\\.booleanField");
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -652,7 +652,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setIncludeProperties("^set\\[\\d+\\]\\.list\\[\\d+\\]\\.booleanField");
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -44,10 +44,10 @@ public class JSONUtilTest extends TestCase {
bean1.setEnumBean(AnEnumBean.Two);
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
String json = jsonUtil.serialize(bean1, JSONUtil.CACHE_BEAN_INFO_DEFAULT);
Map result = (Map) JSONUtil.deserialize(json);
Map result = (Map) new StrutsJSONReader().read(json);
assertEquals("str", result.get("stringField"));
assertEquals(true, result.get("booleanField"));
assertEquals("s", result.get("charField")); // note: this is a
@@ -73,7 +73,7 @@ public class JSONUtilTest extends TestCase {
List<Pattern> includeProperties = JSONUtil.processIncludePatterns(JSONUtil.asSet("listOfLists,listOfLists\\[\\d+\\]\\[\\d+\\]"), JSONUtil.REGEXP_PATTERN);
JSONUtil jsonUtil = new JSONUtil();
jsonUtil.setWriter(new DefaultJSONWriter());
jsonUtil.setWriter(new StrutsJSONWriter());
String actual = jsonUtil.serialize(bean, null, new ArrayList<Pattern>(includeProperties), false, false);
assertEquals("{\"listOfLists\":[[\"1\",\"2\"],[\"3\",\"4\"],[\"5\",\"6\"],[\"7\",\"8\"],[\"9\",\"0\"]]}", actual);
@@ -0,0 +1,159 @@
/*
* 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 org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class StrutsJSONReaderTest {
@Test
public void testArrayExceedingMaxElementsThrows() {
var reader = new StrutsJSONReader();
reader.setMaxElements(3);
String json = "[1, 2, 3, 4]";
var ex = assertThrows(JSONException.class, () -> reader.read(json));
assertTrue(ex.getMessage().contains("maximum allowed elements (3)"));
assertTrue(ex.getMessage().contains(JSONConstants.JSON_MAX_ELEMENTS));
}
@Test
public void testArrayAtExactMaxElementsAllowed() throws Exception {
var reader = new StrutsJSONReader();
reader.setMaxElements(3);
// Exactly 3 elements: check is >= before add, so 3 elements fit (size 0,1,2 when checked)
var result = reader.read("[1, 2, 3]");
assertNotNull(result);
assertEquals(3, ((List<?>) result).size());
}
@Test
public void testObjectExceedingMaxElementsThrows() {
var reader = new StrutsJSONReader();
reader.setMaxElements(2);
String json = "{\"a\":1, \"b\":2, \"c\":3}";
var ex = assertThrows(JSONException.class, () -> reader.read(json));
assertTrue(ex.getMessage().contains("maximum allowed elements (2)"));
}
@Test
public void testNestingExceedingMaxDepthThrows() {
var reader = new StrutsJSONReader();
reader.setMaxDepth(2);
String json = "{\"a\":{\"b\":{\"c\":1}}}";
var ex = assertThrows(JSONException.class, () -> reader.read(json));
assertTrue(ex.getMessage().contains("maximum allowed depth (2)"));
assertTrue(ex.getMessage().contains(JSONConstants.JSON_MAX_DEPTH));
}
@Test
public void testArrayNestingExceedingMaxDepthThrows() {
var reader = new StrutsJSONReader();
reader.setMaxDepth(2);
String json = "[[[1]]]";
var ex = assertThrows(JSONException.class, () -> reader.read(json));
assertTrue(ex.getMessage().contains("maximum allowed depth (2)"));
}
@Test
public void testStringAtExactMaxStringLengthAllowed() throws Exception {
var reader = new StrutsJSONReader();
reader.setMaxStringLength(5);
// String "abcde" has length exactly 5, should be allowed (check is >)
Object result = reader.read("\"abcde\"");
assertEquals("abcde", result);
}
@Test
public void testStringExceedingMaxStringLengthThrows() {
var reader = new StrutsJSONReader();
reader.setMaxStringLength(5);
String json = "\"abcdefghij\"";
var ex = assertThrows(JSONException.class, () -> reader.read(json));
assertTrue(ex.getMessage().contains("maximum allowed length (5)"));
assertTrue(ex.getMessage().contains(JSONConstants.JSON_MAX_STRING_LENGTH));
}
@Test
public void testObjectKeyExceedingMaxKeyLengthThrowsOnFirstKey() {
var reader = new StrutsJSONReader();
reader.setMaxKeyLength(3);
String json = "{\"longkey\":1}";
var ex = assertThrows(JSONException.class, () -> reader.read(json));
assertTrue(ex.getMessage().contains("maximum allowed length (3)"));
assertTrue(ex.getMessage().contains(JSONConstants.JSON_MAX_KEY_LENGTH));
}
@Test
public void testObjectKeyExceedingMaxKeyLengthThrowsOnSubsequentKey() {
var reader = new StrutsJSONReader();
reader.setMaxKeyLength(3);
// First key "a" is within limit, second key "longkey" exceeds it
String json = "{\"a\":1, \"longkey\":2}";
var ex = assertThrows(JSONException.class, () -> reader.read(json));
assertTrue(ex.getMessage().contains("maximum allowed length (3)"));
assertTrue(ex.getMessage().contains(JSONConstants.JSON_MAX_KEY_LENGTH));
}
@Test
public void testObjectKeyAtExactMaxKeyLengthAllowed() throws Exception {
var reader = new StrutsJSONReader();
reader.setMaxKeyLength(3);
// Key "abc" has length exactly 3, should be allowed (check is >)
var result = reader.read("{\"abc\":1}");
assertTrue(result instanceof Map);
assertEquals(1L, ((Map<?, ?>) result).get("abc"));
}
@Test
public void testDefaultLimitsAllowTypicalPayload() throws Exception {
var reader = new StrutsJSONReader();
var json = "{\"name\":\"test\", \"values\":[1, 2, 3], \"nested\":{\"key\":\"value\"}}";
assertTrue(reader.read(json) instanceof Map);
}
@Test
public void testDepthCounterResetsAfterParsing() throws Exception {
var reader = new StrutsJSONReader();
reader.setMaxDepth(3);
// First parse: uses depth up to 2
assertTrue(reader.read("{\"a\":{\"b\":1}}") instanceof Map);
// Second parse: should work fine if depth was reset
assertTrue(reader.read("{\"a\":{\"b\":1}}") instanceof Map);
}
@Test
public void testMixedNestingDepth() {
var reader = new StrutsJSONReader();
reader.setMaxDepth(2);
// array inside object inside array = depth 3, should fail at depth 2
String json = "[{\"a\":[1]}]";
var ex = assertThrows(JSONException.class, () -> reader.read(json));
assertTrue(ex.getMessage().contains("maximum allowed depth"));
}
}
@@ -22,6 +22,7 @@ import org.apache.struts2.json.annotations.JSONFieldBridge;
import org.apache.struts2.junit.util.TestUtils;
import org.junit.Test;
import java.net.URI;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.time.Instant;
@@ -43,7 +44,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class DefaultJSONWriterTest {
public class StrutsJSONWriterTest {
@Test
public void testWrite() throws Exception {
@@ -58,10 +59,10 @@ public class DefaultJSONWriterTest {
bean1.setEnumField(AnEnum.ValueA);
bean1.setEnumBean(AnEnumBean.Two);
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
String json = jsonWriter.write(bean1);
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-01.txt"), json);
TestUtils.assertEquals(StrutsJSONWriter.class.getResource("jsonwriter-write-bean-01.txt"), json);
}
@Test
@@ -83,11 +84,11 @@ public class DefaultJSONWriterTest {
m.put("c", "z");
bean1.setMap(m);
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
jsonWriter.setIgnoreHierarchy(false);
String json = jsonWriter.write(bean1, null, null, true);
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-03.txt"), json);
TestUtils.assertEquals(StrutsJSONWriter.class.getResource("jsonwriter-write-bean-03.txt"), json);
}
private static class BeanWithMap extends Bean {
@@ -114,13 +115,13 @@ public class DefaultJSONWriterTest {
bean1.setLongField(100);
bean1.setEnumField(AnEnum.ValueA);
bean1.setEnumBean(AnEnumBean.Two);
bean1.setUrl(new URL("http://www.google.com"));
bean1.setUrl(URI.create("https://www.google.com").toURL());
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
jsonWriter.setIgnoreHierarchy(false);
String json = jsonWriter.write(bean1);
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-02.txt"), json);
TestUtils.assertEquals(StrutsJSONWriter.class.getResource("jsonwriter-write-bean-02.txt"), json);
}
@Test
@@ -139,11 +140,11 @@ public class DefaultJSONWriterTest {
errors.add("Field is required");
bean1.setErrors(errors);
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
jsonWriter.setIgnoreHierarchy(false);
String json = jsonWriter.write(bean1);
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-04.txt"), json);
TestUtils.assertEquals(StrutsJSONWriter.class.getResource("jsonwriter-write-bean-04.txt"), json);
}
private static class BeanWithList extends Bean {
@@ -178,7 +179,7 @@ public class DefaultJSONWriterTest {
SingleDateBean dateBean = new SingleDateBean();
dateBean.setDate(sdf.parse("2012-12-23 10:10:10 GMT"));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
@@ -193,7 +194,7 @@ public class DefaultJSONWriterTest {
SingleDateBean dateBean = new SingleDateBean();
dateBean.setDate(sdf.parse("2012-12-23 10:10:10 GMT"));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
jsonWriter.setDateFormatter("MM-dd-yyyy");
String json = jsonWriter.write(dateBean);
@@ -205,7 +206,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setLocalDate(LocalDate.of(2026, 2, 27));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"localDate\":\"2026-02-27\""));
}
@@ -215,7 +216,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setLocalDateTime(LocalDateTime.of(2026, 2, 27, 12, 0, 0));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"localDateTime\":\"2026-02-27T12:00:00\""));
}
@@ -225,7 +226,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setLocalTime(LocalTime.of(12, 0, 0));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"localTime\":\"12:00:00\""));
}
@@ -235,7 +236,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setZonedDateTime(ZonedDateTime.of(2026, 2, 27, 12, 0, 0, 0, ZoneId.of("Europe/Paris")));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"zonedDateTime\":\"2026-02-27T12:00:00+01:00[Europe\\/Paris]\""));
}
@@ -245,7 +246,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setOffsetDateTime(OffsetDateTime.of(2026, 2, 27, 12, 0, 0, 0, ZoneOffset.ofHours(1)));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"offsetDateTime\":\"2026-02-27T12:00:00+01:00\""));
}
@@ -255,7 +256,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setInstant(Instant.parse("2026-02-27T11:00:00Z"));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"instant\":\"2026-02-27T11:00:00Z\""));
}
@@ -265,7 +266,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setCustomFormatDate(LocalDate.of(2026, 2, 27));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"customFormatDate\":\"27\\/02\\/2026\""));
}
@@ -275,7 +276,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setCustomFormatDateTime(LocalDateTime.of(2026, 2, 27, 14, 30));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"customFormatDateTime\":\"27\\/02\\/2026 14:30\""));
}
@@ -284,7 +285,7 @@ public class DefaultJSONWriterTest {
public void testSerializeNullTemporalField() throws Exception {
TemporalBean bean = new TemporalBean();
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean, null, null, true);
assertFalse(json.contains("\"localDate\""));
}
@@ -294,7 +295,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setCustomFormatInstant(Instant.parse("2026-02-27T11:00:00Z"));
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"customFormatInstant\":\"2026-02-27 11:00:00\""));
}
@@ -308,7 +309,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setCalendar(cal);
JSONWriter jsonWriter = new DefaultJSONWriter();
JSONWriter jsonWriter = new StrutsJSONWriter();
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"calendar\":\"2012-12-23T10:10:10\""));
@@ -12,5 +12,5 @@
"longField":100,
"objectField":null,
"stringField":"str",
"url":"http:\/\/www.google.com"
"url":"https:\/\/www.google.com"
}