mirror of
https://github.com/apache/struts.git
synced 2026-08-05 06:36:58 +00:00
WW-4428 feat(json): add java.time serialization and deserialization support (#1603)
- Add serialization support for LocalDate, LocalDateTime, LocalTime, ZonedDateTime, OffsetDateTime, and Instant in DefaultJSONWriter - Add deserialization support for the same types in JSONPopulator - Support @JSON(format=...) custom formats for all temporal types - Fix Instant custom-format serialization requiring UTC zone - Add Calendar serialization/deserialization via temporal bridge - Add comprehensive tests for all temporal types including custom formats, malformed input, and null handling Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,19 +33,30 @@ import java.beans.IntrospectionException;
|
||||
import java.beans.Introspector;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.text.CharacterIterator;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.text.StringCharacterIterator;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -60,23 +71,22 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(DefaultJSONWriter.class);
|
||||
|
||||
private static char[] hex = "0123456789ABCDEF".toCharArray();
|
||||
private static final char[] hex = "0123456789ABCDEF".toCharArray();
|
||||
|
||||
private static final ConcurrentMap<Class<?>, BeanInfo> BEAN_INFO_CACHE_IGNORE_HIERARCHY = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentMap<Class<?>, BeanInfo> BEAN_INFO_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private StringBuilder buf = new StringBuilder();
|
||||
private Stack<Object> stack = new Stack<>();
|
||||
private final StringBuilder buf = new StringBuilder();
|
||||
private final Deque<Object> stack = new ArrayDeque<>();
|
||||
private boolean ignoreHierarchy = true;
|
||||
private Object root;
|
||||
private boolean buildExpr = true;
|
||||
private String exprStack = "";
|
||||
private Collection<Pattern> excludeProperties;
|
||||
private Collection<Pattern> includeProperties;
|
||||
private DateFormat formatter;
|
||||
private DateFormat dateFormat;
|
||||
private boolean enumAsBean = ENUM_AS_BEAN_DEFAULT;
|
||||
private boolean excludeNullProperties;
|
||||
private boolean cacheBeanInfo = true;
|
||||
private boolean excludeProxyProperties;
|
||||
private ProxyService proxyService;
|
||||
|
||||
@@ -101,14 +111,10 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @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
|
||||
*/
|
||||
@@ -134,7 +140,6 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
*
|
||||
* @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 {
|
||||
@@ -144,7 +149,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
}
|
||||
|
||||
if (this.stack.contains(object)) {
|
||||
Class clazz = object.getClass();
|
||||
Class<?> clazz = object.getClass();
|
||||
|
||||
// cyclic reference
|
||||
if (clazz.isPrimitive() || clazz.equals(String.class)) {
|
||||
@@ -165,8 +170,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
*
|
||||
* @param object Object to be serialized into JSON
|
||||
* @param method method
|
||||
*
|
||||
* @throws JSONException in case of error during serialize
|
||||
* @throws JSONException in case of error during serialize
|
||||
*/
|
||||
protected void process(Object object, Method method) throws JSONException {
|
||||
this.stack.push(object);
|
||||
@@ -181,20 +185,22 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
this.string(object);
|
||||
} else if (object instanceof Character) {
|
||||
this.string(object);
|
||||
} else if (object instanceof Map) {
|
||||
this.map((Map) object, method);
|
||||
} else if (object instanceof Map<?, ?> map) {
|
||||
this.map(map, 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 Iterable<?> iterable) {
|
||||
this.array(iterable.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 TemporalAccessor temporalAccessor) {
|
||||
this.temporal(temporalAccessor, method);
|
||||
} else if (object instanceof Locale) {
|
||||
this.string(object);
|
||||
} else if (object instanceof Enum) {
|
||||
this.enumeration((Enum) object);
|
||||
} else if (object instanceof Enum<?> enumValue) {
|
||||
this.enumeration(enumValue);
|
||||
} else {
|
||||
processCustom(object, method);
|
||||
}
|
||||
@@ -207,8 +213,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
*
|
||||
* @param object object
|
||||
* @param method method
|
||||
*
|
||||
* @throws JSONException in case of error during serialize
|
||||
* @throws JSONException in case of error during serialize
|
||||
*/
|
||||
protected void processCustom(Object object, Method method) throws JSONException {
|
||||
this.bean(object);
|
||||
@@ -218,8 +223,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
* Instrospect bean and serialize its properties
|
||||
*
|
||||
* @param object object
|
||||
*
|
||||
* @throws JSONException in case of error during serialize
|
||||
* @throws JSONException in case of error during serialize
|
||||
*/
|
||||
protected void bean(Object object) throws JSONException {
|
||||
this.add("{");
|
||||
@@ -279,7 +283,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
// special-case handling for an Enumeration - include the name() as
|
||||
// a property */
|
||||
if (object instanceof Enum) {
|
||||
Object value = ((Enum) object).name();
|
||||
Object value = ((Enum<?>) object).name();
|
||||
this.add("_name", value, object.getClass().getMethod("name"), hasData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -309,11 +313,11 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
return beanInfo;
|
||||
}
|
||||
|
||||
protected Object getBridgedValue(Method baseAccessor, Object value) throws InstantiationException, IllegalAccessException {
|
||||
protected Object getBridgedValue(Method baseAccessor, Object value) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
|
||||
JSONFieldBridge fieldBridgeAnn = baseAccessor.getAnnotation(JSONFieldBridge.class);
|
||||
if (fieldBridgeAnn != null) {
|
||||
Class impl = fieldBridgeAnn.impl();
|
||||
FieldBridge instance = (FieldBridge) impl.newInstance();
|
||||
Class<?> impl = fieldBridgeAnn.impl();
|
||||
FieldBridge instance = (FieldBridge) impl.getDeclaredConstructor().newInstance();
|
||||
|
||||
if (fieldBridgeAnn.params().length > 0 && ParameterizedBridge.class.isAssignableFrom(impl)) {
|
||||
Map<String, String> params = new HashMap<>(fieldBridgeAnn.params().length);
|
||||
@@ -327,7 +331,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
return value;
|
||||
}
|
||||
|
||||
protected Method findBaseAccessor(Class clazz, Method accessor) {
|
||||
protected Method findBaseAccessor(Class<?> clazz, Method accessor) {
|
||||
Method baseAccessor = null;
|
||||
if (clazz.getName().contains("$$EnhancerByCGLIB$$")) {
|
||||
try {
|
||||
@@ -340,23 +344,22 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
} else if (clazz.getName().contains("$$_javassist")) {
|
||||
try {
|
||||
baseAccessor = Class.forName(
|
||||
clazz.getName().substring(0, clazz.getName().indexOf("_$$")))
|
||||
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")){
|
||||
//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("_$$")))
|
||||
clazz.getName().substring(0, clazz.getName().indexOf("_$$")))
|
||||
.getMethod(accessor.getName(), accessor.getParameterTypes());
|
||||
} catch (Exception ex) {
|
||||
LOG.debug(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return accessor;
|
||||
}
|
||||
return baseAccessor;
|
||||
@@ -367,10 +370,9 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
* including all its own properties
|
||||
*
|
||||
* @param enumeration the enum
|
||||
*
|
||||
* @throws JSONException in case of error during serialize
|
||||
* @throws JSONException in case of error during serialize
|
||||
*/
|
||||
protected void enumeration(Enum enumeration) throws JSONException {
|
||||
protected void enumeration(Enum<?> enumeration) throws JSONException {
|
||||
if (enumAsBean) {
|
||||
this.bean(enumeration);
|
||||
} else {
|
||||
@@ -378,7 +380,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean shouldExcludeProperty(PropertyDescriptor prop) throws SecurityException, NoSuchFieldException {
|
||||
protected boolean shouldExcludeProperty(PropertyDescriptor prop) throws SecurityException {
|
||||
String name = prop.getName();
|
||||
return name.equals("class")
|
||||
|| name.equals("declaringClass")
|
||||
@@ -391,7 +393,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
}
|
||||
|
||||
protected String expandExpr(String property) {
|
||||
if (this.exprStack.length() == 0) {
|
||||
if (this.exprStack.isEmpty()) {
|
||||
return property;
|
||||
}
|
||||
return this.exprStack + "." + property;
|
||||
@@ -421,7 +423,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (LOG.isDebugEnabled()){
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Ignoring property because of include rule: " + expr);
|
||||
}
|
||||
return true;
|
||||
@@ -449,15 +451,15 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
/*
|
||||
* Add map to buffer
|
||||
*/
|
||||
protected void map(Map map, Method method) throws JSONException {
|
||||
protected void map(Map<?, ?> map, Method method) throws JSONException {
|
||||
this.add("{");
|
||||
|
||||
Iterator it = map.entrySet().iterator();
|
||||
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();
|
||||
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) it.next();
|
||||
if (excludeNullProperties && entry.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -504,18 +506,56 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
JSON json = null;
|
||||
if (method != null)
|
||||
json = method.getAnnotation(JSON.class);
|
||||
if (this.formatter == null)
|
||||
this.formatter = new SimpleDateFormat(JSONUtil.RFC3339_FORMAT);
|
||||
if (this.dateFormat == null)
|
||||
this.dateFormat = new SimpleDateFormat(JSONUtil.RFC3339_FORMAT);
|
||||
|
||||
DateFormat formatter = (json != null) && (json.format().length() > 0) ? new SimpleDateFormat(json
|
||||
.format()) : this.formatter;
|
||||
DateFormat formatter = (json != null) && (!json.format().isEmpty()) ? new SimpleDateFormat(json
|
||||
.format()) : this.dateFormat;
|
||||
this.string(formatter.format(date));
|
||||
}
|
||||
|
||||
/*
|
||||
* Add temporal (java.time) value to buffer
|
||||
*/
|
||||
protected void temporal(TemporalAccessor temporal, Method method) {
|
||||
JSON json = null;
|
||||
if (method != null) {
|
||||
json = method.getAnnotation(JSON.class);
|
||||
}
|
||||
|
||||
DateTimeFormatter formatter;
|
||||
if (json != null && !json.format().isEmpty()) {
|
||||
formatter = DateTimeFormatter.ofPattern(json.format());
|
||||
if (temporal instanceof Instant) {
|
||||
formatter = formatter.withZone(ZoneOffset.UTC);
|
||||
}
|
||||
} else {
|
||||
formatter = getDefaultDateTimeFormatter(temporal);
|
||||
}
|
||||
this.string(formatter.format(temporal));
|
||||
}
|
||||
|
||||
private static DateTimeFormatter getDefaultDateTimeFormatter(TemporalAccessor temporal) {
|
||||
if (temporal instanceof LocalDate) {
|
||||
return DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
} else if (temporal instanceof LocalDateTime) {
|
||||
return DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
} else if (temporal instanceof LocalTime) {
|
||||
return DateTimeFormatter.ISO_LOCAL_TIME;
|
||||
} else if (temporal instanceof ZonedDateTime) {
|
||||
return DateTimeFormatter.ISO_ZONED_DATE_TIME;
|
||||
} else if (temporal instanceof OffsetDateTime) {
|
||||
return DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
} else if (temporal instanceof Instant) {
|
||||
return DateTimeFormatter.ISO_INSTANT;
|
||||
}
|
||||
return DateTimeFormatter.ISO_DATE_TIME;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add array to buffer
|
||||
*/
|
||||
protected void array(Iterator it, Method method) throws JSONException {
|
||||
protected void array(Iterator<?> it, Method method) throws JSONException {
|
||||
this.add("[");
|
||||
|
||||
boolean hasData = false;
|
||||
@@ -670,13 +710,13 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
@Override
|
||||
public void setDateFormatter(String defaultDateFormat) {
|
||||
if (defaultDateFormat != null) {
|
||||
this.formatter = new SimpleDateFormat(defaultDateFormat);
|
||||
this.dateFormat = new SimpleDateFormat(defaultDateFormat);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCacheBeanInfo(boolean cacheBeanInfo) {
|
||||
this.cacheBeanInfo = cacheBeanInfo;
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -686,7 +726,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
|
||||
protected static class JSONAnnotationFinder {
|
||||
private boolean serialize = true;
|
||||
private Method accessor;
|
||||
private final Method accessor;
|
||||
private String name;
|
||||
|
||||
public JSONAnnotationFinder(Method accessor) {
|
||||
@@ -705,7 +745,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
public JSONAnnotationFinder invoke() {
|
||||
JSON json = accessor.getAnnotation(JSON.class);
|
||||
serialize = json.serialize();
|
||||
if (serialize && json.name().length() > 0) {
|
||||
if (serialize && !json.name().isEmpty()) {
|
||||
name = json.name();
|
||||
}
|
||||
return this;
|
||||
|
||||
@@ -32,6 +32,15 @@ import java.math.BigInteger;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalQuery;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -59,11 +68,10 @@ public class JSONPopulator {
|
||||
this.dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void populateObject(Object object, final Map elements) throws IllegalAccessException,
|
||||
InvocationTargetException, NoSuchMethodException, IntrospectionException,
|
||||
IllegalArgumentException, JSONException, InstantiationException {
|
||||
Class clazz = object.getClass();
|
||||
Class<?> clazz = object.getClass();
|
||||
|
||||
BeanInfo info = Introspector.getBeanInfo(clazz);
|
||||
PropertyDescriptor[] props = info.getPropertyDescriptors();
|
||||
@@ -84,12 +92,12 @@ public class JSONPopulator {
|
||||
|
||||
// use only public setters
|
||||
if (Modifier.isPublic(method.getModifiers())) {
|
||||
Class[] paramTypes = method.getParameterTypes();
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
Type[] genericTypes = method.getGenericParameterTypes();
|
||||
|
||||
if (paramTypes.length == 1) {
|
||||
Object convertedValue = this.convert(paramTypes[0], genericTypes[0], value, method);
|
||||
method.invoke(object, new Object[] { convertedValue });
|
||||
method.invoke(object, convertedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,8 +105,7 @@ public class JSONPopulator {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Object convert(Class clazz, Type type, Object value, Method method)
|
||||
public Object convert(Class<?> clazz, Type type, Object value, Method method)
|
||||
throws IllegalArgumentException, JSONException, IllegalAccessException,
|
||||
InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException {
|
||||
|
||||
@@ -116,7 +123,7 @@ public class JSONPopulator {
|
||||
return convertToArray(clazz, type, value, method);
|
||||
else if (value instanceof Map) {
|
||||
// nested bean
|
||||
Object convertedValue = clazz.newInstance();
|
||||
Object convertedValue = clazz.getDeclaredConstructor().newInstance();
|
||||
this.populateObject(convertedValue, (Map) value);
|
||||
return convertedValue;
|
||||
} else if (BigDecimal.class.equals(clazz)) {
|
||||
@@ -127,22 +134,25 @@ public class JSONPopulator {
|
||||
throw new JSONException("Incompatible types for property " + method.getName());
|
||||
}
|
||||
|
||||
private static boolean isJSONPrimitive(Class clazz) {
|
||||
private static boolean isJSONPrimitive(Class<?> clazz) {
|
||||
return clazz.isPrimitive() || clazz.equals(String.class) || clazz.equals(Date.class)
|
||||
|| clazz.equals(Boolean.class) || clazz.equals(Byte.class) || clazz.equals(Character.class)
|
||||
|| clazz.equals(Double.class) || clazz.equals(Float.class) || clazz.equals(Integer.class)
|
||||
|| clazz.equals(Long.class) || clazz.equals(Short.class) || clazz.equals(Locale.class)
|
||||
|| clazz.isEnum();
|
||||
|| clazz.isEnum()
|
||||
|| Calendar.class.isAssignableFrom(clazz)
|
||||
|| clazz.equals(LocalDate.class) || clazz.equals(LocalDateTime.class)
|
||||
|| clazz.equals(LocalTime.class) || clazz.equals(ZonedDateTime.class)
|
||||
|| clazz.equals(OffsetDateTime.class) || clazz.equals(Instant.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object convertToArray(Class clazz, Type type, Object value, Method accessor)
|
||||
private Object convertToArray(Class<?> clazz, Type type, Object value, Method accessor)
|
||||
throws JSONException, IllegalArgumentException, IllegalAccessException,
|
||||
InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException {
|
||||
if (value == null)
|
||||
return null;
|
||||
else if (value instanceof List) {
|
||||
Class arrayType = clazz.getComponentType();
|
||||
Class<?> arrayType = clazz.getComponentType();
|
||||
List values = (List) value;
|
||||
Object newArray = Array.newInstance(arrayType, values.size());
|
||||
|
||||
@@ -164,7 +174,7 @@ public class JSONPopulator {
|
||||
} else if (List.class.isAssignableFrom(arrayType)) {
|
||||
newObject = convertToCollection(arrayType, type, listValue, accessor);
|
||||
} else {
|
||||
newObject = arrayType.newInstance();
|
||||
newObject = arrayType.getDeclaredConstructor().newInstance();
|
||||
this.populateObject(newObject, (Map) listValue);
|
||||
}
|
||||
|
||||
@@ -179,16 +189,15 @@ public class JSONPopulator {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object convertToCollection(Class clazz, Type type, Object value, Method accessor)
|
||||
private Object convertToCollection(Class<?> clazz, Type type, Object value, Method accessor)
|
||||
throws JSONException, IllegalArgumentException, IllegalAccessException,
|
||||
InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException {
|
||||
if (value == null)
|
||||
return null;
|
||||
else if (value instanceof List) {
|
||||
Class itemClass = Object.class;
|
||||
Class<?> itemClass = Object.class;
|
||||
Type itemType = null;
|
||||
if ((type != null) && (type instanceof ParameterizedType)) {
|
||||
ParameterizedType ptype = (ParameterizedType) type;
|
||||
if (type instanceof ParameterizedType ptype) {
|
||||
itemType = ptype.getActualTypeArguments()[0];
|
||||
if (itemType.getClass().equals(Class.class)) {
|
||||
itemClass = (Class) itemType;
|
||||
@@ -198,11 +207,8 @@ public class JSONPopulator {
|
||||
}
|
||||
List values = (List) value;
|
||||
|
||||
Collection newCollection = null;
|
||||
try {
|
||||
newCollection = (Collection) clazz.newInstance();
|
||||
} catch (InstantiationException ex) {
|
||||
// fallback if clazz represents an interface or abstract class
|
||||
Collection newCollection;
|
||||
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) {
|
||||
if (SortedSet.class.isAssignableFrom(clazz)) {
|
||||
newCollection = new TreeSet();
|
||||
} else if (Set.class.isAssignableFrom(clazz)) {
|
||||
@@ -212,6 +218,8 @@ public class JSONPopulator {
|
||||
} else {
|
||||
newCollection = new ArrayList();
|
||||
}
|
||||
} else {
|
||||
newCollection = (Collection) clazz.getDeclaredConstructor().newInstance();
|
||||
}
|
||||
|
||||
// create an object for each element
|
||||
@@ -231,7 +239,7 @@ public class JSONPopulator {
|
||||
newCollection.add(newObject);
|
||||
} else if (listValue instanceof Map) {
|
||||
// array of beans
|
||||
Object newObject = itemClass.newInstance();
|
||||
Object newObject = itemClass.getDeclaredConstructor().newInstance();
|
||||
this.populateObject(newObject, (Map) listValue);
|
||||
newCollection.add(newObject);
|
||||
} else
|
||||
@@ -244,16 +252,15 @@ public class JSONPopulator {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object convertToMap(Class clazz, Type type, Object value, Method accessor) throws JSONException,
|
||||
private Object convertToMap(Class<?> clazz, Type type, Object value, Method accessor) throws JSONException,
|
||||
IllegalArgumentException, IllegalAccessException, InvocationTargetException,
|
||||
InstantiationException, NoSuchMethodException, IntrospectionException {
|
||||
if (value == null)
|
||||
return null;
|
||||
else if (value instanceof Map) {
|
||||
Class itemClass = Object.class;
|
||||
Class<?> itemClass = Object.class;
|
||||
Type itemType = null;
|
||||
if ((type != null) && (type instanceof ParameterizedType)) {
|
||||
ParameterizedType ptype = (ParameterizedType) type;
|
||||
if (type instanceof ParameterizedType ptype) {
|
||||
itemType = ptype.getActualTypeArguments()[1];
|
||||
if (itemType.getClass().equals(Class.class)) {
|
||||
itemClass = (Class) itemType;
|
||||
@@ -264,11 +271,14 @@ public class JSONPopulator {
|
||||
Map values = (Map) value;
|
||||
|
||||
Map newMap;
|
||||
try {
|
||||
newMap = (Map) clazz.newInstance();
|
||||
} catch (InstantiationException ex) {
|
||||
// fallback if clazz represents an interface or abstract class
|
||||
newMap = new HashMap();
|
||||
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) {
|
||||
if (SortedMap.class.isAssignableFrom(clazz)) {
|
||||
newMap = new TreeMap();
|
||||
} else {
|
||||
newMap = new HashMap();
|
||||
}
|
||||
} else {
|
||||
newMap = (Map) clazz.getDeclaredConstructor().newInstance();
|
||||
}
|
||||
|
||||
// create an object for each element
|
||||
@@ -291,7 +301,7 @@ public class JSONPopulator {
|
||||
newMap.put(key, newObject);
|
||||
} else if (v instanceof Map) {
|
||||
// map of beans
|
||||
Object newObject = itemClass.newInstance();
|
||||
Object newObject = itemClass.getDeclaredConstructor().newInstance();
|
||||
this.populateObject(newObject, (Map) v);
|
||||
newMap.put(key, newObject);
|
||||
} else
|
||||
@@ -305,7 +315,7 @@ public class JSONPopulator {
|
||||
|
||||
/**
|
||||
* Converts numbers to the desired class, if possible
|
||||
*
|
||||
*
|
||||
* @throws JSONException
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -361,17 +371,42 @@ public class JSONPopulator {
|
||||
JSON json = method.getAnnotation(JSON.class);
|
||||
|
||||
DateFormat formatter = new SimpleDateFormat(
|
||||
(json != null) && (json.format().length() > 0) ? json.format() : this.dateFormat);
|
||||
(json != null) && (!json.format().isEmpty()) ? json.format() : this.dateFormat);
|
||||
return formatter.parse((String) value);
|
||||
} catch (ParseException e) {
|
||||
LOG.error("Unable to parse date from: {}", value, e);
|
||||
throw new JSONException("Unable to parse date from: " + value);
|
||||
}
|
||||
} else if (Calendar.class.isAssignableFrom(clazz)) {
|
||||
try {
|
||||
JSON json = method.getAnnotation(JSON.class);
|
||||
|
||||
DateFormat formatter = new SimpleDateFormat(
|
||||
(json != null) && (!json.format().isEmpty()) ? json.format() : this.dateFormat);
|
||||
Date date = formatter.parse((String) value);
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(date);
|
||||
return cal;
|
||||
} catch (ParseException e) {
|
||||
LOG.error("Unable to parse calendar from: {}", value, e);
|
||||
throw new JSONException("Unable to parse calendar from: " + value);
|
||||
}
|
||||
} else if (clazz.equals(LocalDate.class)) {
|
||||
return parseTemporalFromString(value, method, DateTimeFormatter.ISO_LOCAL_DATE, LocalDate::from);
|
||||
} else if (clazz.equals(LocalDateTime.class)) {
|
||||
return parseTemporalFromString(value, method, DateTimeFormatter.ISO_LOCAL_DATE_TIME, LocalDateTime::from);
|
||||
} else if (clazz.equals(LocalTime.class)) {
|
||||
return parseTemporalFromString(value, method, DateTimeFormatter.ISO_LOCAL_TIME, LocalTime::from);
|
||||
} else if (clazz.equals(ZonedDateTime.class)) {
|
||||
return parseTemporalFromString(value, method, DateTimeFormatter.ISO_ZONED_DATE_TIME, ZonedDateTime::from);
|
||||
} else if (clazz.equals(OffsetDateTime.class)) {
|
||||
return parseTemporalFromString(value, method, DateTimeFormatter.ISO_OFFSET_DATE_TIME, OffsetDateTime::from);
|
||||
} else if (clazz.equals(Instant.class)) {
|
||||
return parseInstantFromString(value, method);
|
||||
} else if (clazz.isEnum()) {
|
||||
String sValue = (String) value;
|
||||
return Enum.valueOf(clazz, sValue);
|
||||
} else if (value instanceof String) {
|
||||
String sValue = (String) value;
|
||||
} else if (value instanceof String sValue) {
|
||||
if (Boolean.TYPE.equals(clazz))
|
||||
return Boolean.parseBoolean(sValue);
|
||||
else if (Boolean.class.equals(clazz))
|
||||
@@ -402,7 +437,7 @@ public class JSONPopulator {
|
||||
return Double.valueOf(sValue);
|
||||
else if (Character.TYPE.equals(clazz) || Character.class.equals(clazz)) {
|
||||
char charValue = 0;
|
||||
if (sValue.length() > 0) {
|
||||
if (!sValue.isEmpty()) {
|
||||
charValue = sValue.charAt(0);
|
||||
}
|
||||
if (Character.TYPE.equals(clazz))
|
||||
@@ -424,4 +459,38 @@ public class JSONPopulator {
|
||||
return value;
|
||||
}
|
||||
|
||||
private <T> T parseTemporalFromString(Object value, Method method, DateTimeFormatter defaultFormatter, TemporalQuery<T> query) throws JSONException {
|
||||
try {
|
||||
String sValue = (String) value;
|
||||
JSON json = method.getAnnotation(JSON.class);
|
||||
|
||||
DateTimeFormatter formatter;
|
||||
if (json != null && !json.format().isEmpty()) {
|
||||
formatter = DateTimeFormatter.ofPattern(json.format());
|
||||
} else {
|
||||
formatter = defaultFormatter;
|
||||
}
|
||||
return formatter.parse(sValue, query);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Unable to parse temporal from: {}", value, e);
|
||||
throw new JSONException("Unable to parse temporal from: " + value);
|
||||
}
|
||||
}
|
||||
|
||||
private Instant parseInstantFromString(Object value, Method method) throws JSONException {
|
||||
try {
|
||||
String sValue = (String) value;
|
||||
JSON json = method.getAnnotation(JSON.class);
|
||||
|
||||
if (json != null && !json.format().isEmpty()) {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(json.format()).withZone(ZoneOffset.UTC);
|
||||
return Instant.from(formatter.parse(sValue));
|
||||
}
|
||||
return Instant.parse(sValue);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Unable to parse instant from: {}", value, e);
|
||||
throw new JSONException("Unable to parse instant from: " + value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,20 +19,32 @@
|
||||
package org.apache.struts2.json;
|
||||
|
||||
import org.apache.struts2.json.annotations.JSONFieldBridge;
|
||||
import org.apache.struts2.json.bridge.StringBridge;
|
||||
import org.apache.struts2.junit.StrutsTestCase;
|
||||
import org.apache.struts2.junit.util.TestUtils;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.net.URL;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class DefaultJSONWriterTest {
|
||||
|
||||
@Test
|
||||
public void testWrite() throws Exception {
|
||||
Bean bean1 = new Bean();
|
||||
@@ -65,7 +77,7 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
bean1.setEnumField(AnEnum.ValueA);
|
||||
bean1.setEnumBean(AnEnumBean.Two);
|
||||
|
||||
Map m = new LinkedHashMap();
|
||||
Map<String, String> m = new LinkedHashMap<>();
|
||||
m.put("a", "x");
|
||||
m.put("b", null);
|
||||
m.put("c", "z");
|
||||
@@ -78,14 +90,14 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-03.txt"), json);
|
||||
}
|
||||
|
||||
private class BeanWithMap extends Bean {
|
||||
private Map map;
|
||||
private static class BeanWithMap extends Bean {
|
||||
private Map<?, ?> map;
|
||||
|
||||
public Map getMap() {
|
||||
public Map<?, ?> getMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
public void setMap(Map map) {
|
||||
public void setMap(Map<?, ?> map) {
|
||||
this.map = map;
|
||||
}
|
||||
}
|
||||
@@ -123,7 +135,7 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
bean1.setLongField(100);
|
||||
bean1.setEnumField(AnEnum.ValueA);
|
||||
bean1.setEnumBean(AnEnumBean.Two);
|
||||
List<String> errors = new ArrayList<String>();
|
||||
List<String> errors = new ArrayList<>();
|
||||
errors.add("Field is required");
|
||||
bean1.setErrors(errors);
|
||||
|
||||
@@ -134,7 +146,7 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-04.txt"), json);
|
||||
}
|
||||
|
||||
private class BeanWithList extends Bean {
|
||||
private static class BeanWithList extends Bean {
|
||||
private List<String> errors;
|
||||
|
||||
public List<String> getErrors() {
|
||||
@@ -146,10 +158,10 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
private class AnnotatedBean extends Bean {
|
||||
private static class AnnotatedBean extends Bean {
|
||||
private URL url;
|
||||
|
||||
@JSONFieldBridge(impl = StringBridge.class)
|
||||
@JSONFieldBridge()
|
||||
public URL getUrl() {
|
||||
return url;
|
||||
}
|
||||
@@ -188,4 +200,118 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
assertEquals("{\"date\":\"12-23-2012\"}", json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeLocalDate() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setLocalDate(LocalDate.of(2026, 2, 27));
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"localDate\":\"2026-02-27\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeLocalDateTime() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setLocalDateTime(LocalDateTime.of(2026, 2, 27, 12, 0, 0));
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"localDateTime\":\"2026-02-27T12:00:00\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeLocalTime() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setLocalTime(LocalTime.of(12, 0, 0));
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"localTime\":\"12:00:00\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeZonedDateTime() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setZonedDateTime(ZonedDateTime.of(2026, 2, 27, 12, 0, 0, 0, ZoneId.of("Europe/Paris")));
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"zonedDateTime\":\"2026-02-27T12:00:00+01:00[Europe\\/Paris]\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeOffsetDateTime() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setOffsetDateTime(OffsetDateTime.of(2026, 2, 27, 12, 0, 0, 0, ZoneOffset.ofHours(1)));
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"offsetDateTime\":\"2026-02-27T12:00:00+01:00\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeInstant() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setInstant(Instant.parse("2026-02-27T11:00:00Z"));
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"instant\":\"2026-02-27T11:00:00Z\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeLocalDateWithCustomFormat() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setCustomFormatDate(LocalDate.of(2026, 2, 27));
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"customFormatDate\":\"27\\/02\\/2026\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeLocalDateTimeWithCustomFormat() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setCustomFormatDateTime(LocalDateTime.of(2026, 2, 27, 14, 30));
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"customFormatDateTime\":\"27\\/02\\/2026 14:30\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeNullTemporalField() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean, null, null, true);
|
||||
assertFalse(json.contains("\"localDate\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeInstantWithCustomFormat() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setCustomFormatInstant(Instant.parse("2026-02-27T11:00:00Z"));
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"customFormatInstant\":\"2026-02-27 11:00:00\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeCalendar() throws Exception {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(sdf.parse("2012-12-23 10:10:10 GMT"));
|
||||
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setCalendar(cal);
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"calendar\":\"2012-12-23T10:10:10\""));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,24 +18,42 @@
|
||||
*/
|
||||
package org.apache.struts2.json;
|
||||
|
||||
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;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
import org.apache.struts2.junit.util.TestUtils;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class JSONPopulatorTest extends TestCase {
|
||||
public class JSONPopulatorTest {
|
||||
|
||||
@Test
|
||||
public void testNulls() throws IntrospectionException, InvocationTargetException, NoSuchMethodException,
|
||||
JSONException, InstantiationException, IllegalAccessException {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
OtherBean bean = new OtherBean();
|
||||
Map jsonMap = new HashMap();
|
||||
Map<String, ?> jsonMap = new HashMap<>();
|
||||
|
||||
jsonMap.put("intField", null);
|
||||
jsonMap.put("booleanField", null);
|
||||
@@ -54,13 +72,14 @@ public class JSONPopulatorTest extends TestCase {
|
||||
assertNull(bean.getByteField());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrimitiveBean() throws Exception {
|
||||
StringReader stringReader = new StringReader(TestUtils.readContent(JSONInterceptorTest.class
|
||||
.getResource("json-7.txt")));
|
||||
Object json = JSONUtil.deserialize(stringReader);
|
||||
assertNotNull(json);
|
||||
assertTrue(json instanceof Map);
|
||||
Map jsonMap = (Map) json;
|
||||
Map<?, ?> jsonMap = (Map<?, ?>) json;
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
Bean bean = new Bean();
|
||||
populator.populateObject(bean, jsonMap);
|
||||
@@ -70,29 +89,30 @@ public class JSONPopulatorTest extends TestCase {
|
||||
assertEquals('s', bean.getCharField());
|
||||
assertEquals(10.1d, bean.getDoubleField(), 0d);
|
||||
assertEquals(3, bean.getByteField());
|
||||
assertEquals(new BigDecimal(111111.5d), bean.getBigDecimal());
|
||||
assertEquals(BigDecimal.valueOf(111111.5d), bean.getBigDecimal());
|
||||
assertEquals(new BigInteger("111111"), bean.getBigInteger());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testObjectBean() throws Exception {
|
||||
String text = TestUtils.readContent(JSONInterceptorTest.class.getResource("json-7.txt"));
|
||||
Object json = JSONUtil.deserialize(text);
|
||||
assertNotNull(json);
|
||||
assertTrue(json instanceof Map);
|
||||
Map jsonMap = (Map) json;
|
||||
Map<?, ?> jsonMap = (Map<?, ?>) json;
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
WrapperClassBean bean = new WrapperClassBean();
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(Boolean.TRUE, bean.getBooleanField());
|
||||
assertEquals(true, bean.isPrimitiveBooleanField1());
|
||||
assertEquals(false, bean.isPrimitiveBooleanField2());
|
||||
assertEquals(false, bean.isPrimitiveBooleanField3());
|
||||
assertTrue(bean.isPrimitiveBooleanField1());
|
||||
assertFalse(bean.isPrimitiveBooleanField2());
|
||||
assertFalse(bean.isPrimitiveBooleanField3());
|
||||
assertEquals("test\u000E\u000f", bean.getStringField());
|
||||
assertEquals(new Integer(10), bean.getIntField());
|
||||
assertEquals(Integer.valueOf(10), bean.getIntField());
|
||||
assertEquals(0, bean.getNullIntField());
|
||||
assertEquals(new Character('s'), bean.getCharField());
|
||||
assertEquals(10.1d, bean.getDoubleField());
|
||||
assertEquals(new Byte((byte) 3), bean.getByteField());
|
||||
assertEquals(Character.valueOf('s'), bean.getCharField());
|
||||
assertEquals(Double.valueOf(10.1d), bean.getDoubleField());
|
||||
assertEquals(Byte.valueOf((byte) 3), bean.getByteField());
|
||||
|
||||
assertEquals(2, bean.getListField().size());
|
||||
assertEquals("1", bean.getListField().get(0).getValue());
|
||||
@@ -100,33 +120,33 @@ public class JSONPopulatorTest extends TestCase {
|
||||
|
||||
assertEquals(1, bean.getListMapField().size());
|
||||
assertEquals(2, bean.getListMapField().get(0).size());
|
||||
assertEquals(new Long(2073501), bean.getListMapField().get(0).get("id1"));
|
||||
assertEquals(new Long(3), bean.getListMapField().get(0).get("id2"));
|
||||
assertEquals(Long.valueOf(2073501L), bean.getListMapField().get(0).get("id1"));
|
||||
assertEquals(Long.valueOf(3L), bean.getListMapField().get(0).get("id2"));
|
||||
|
||||
assertEquals(2, bean.getMapListField().size());
|
||||
assertEquals(3, bean.getMapListField().get("id1").size());
|
||||
assertEquals(new Long(2), bean.getMapListField().get("id1").get(1));
|
||||
assertEquals(Long.valueOf(2L), bean.getMapListField().get("id1").get(1));
|
||||
assertEquals(4, bean.getMapListField().get("id2").size());
|
||||
assertEquals(new Long(3), bean.getMapListField().get("id2").get(1));
|
||||
assertEquals(Long.valueOf(3L), bean.getMapListField().get("id2").get(1));
|
||||
|
||||
assertEquals(1, bean.getArrayMapField().length);
|
||||
assertEquals(2, bean.getArrayMapField()[0].size());
|
||||
assertEquals(new Long(2073501), bean.getArrayMapField()[0].get("id1"));
|
||||
assertEquals(new Long(3), bean.getArrayMapField()[0].get("id2"));
|
||||
assertEquals(Long.valueOf(2073501L), bean.getArrayMapField()[0].get("id1"));
|
||||
assertEquals(Long.valueOf(3L), bean.getArrayMapField()[0].get("id2"));
|
||||
|
||||
assertEquals(3, bean.getSetField().size());
|
||||
assertEquals(true, bean.getSetField().contains("A"));
|
||||
assertEquals(true, bean.getSetField().contains("B"));
|
||||
assertEquals(true, bean.getSetField().contains("C"));
|
||||
assertTrue(bean.getSetField().contains("A"));
|
||||
assertTrue(bean.getSetField().contains("B"));
|
||||
assertTrue(bean.getSetField().contains("C"));
|
||||
|
||||
assertEquals(3, bean.getSortedSetField().size());
|
||||
assertEquals("A", bean.getSortedSetField().first());
|
||||
assertEquals(true, bean.getSortedSetField().contains("B"));
|
||||
assertTrue(bean.getSortedSetField().contains("B"));
|
||||
assertEquals("C", bean.getSortedSetField().last());
|
||||
|
||||
assertEquals(3, bean.getNavigableSetField().size());
|
||||
assertEquals("A", bean.getNavigableSetField().first());
|
||||
assertEquals(true, bean.getNavigableSetField().contains("B"));
|
||||
assertTrue(bean.getNavigableSetField().contains("B"));
|
||||
assertEquals("C", bean.getNavigableSetField().last());
|
||||
|
||||
assertEquals(3, bean.getQueueField().size());
|
||||
@@ -140,41 +160,43 @@ public class JSONPopulatorTest extends TestCase {
|
||||
assertEquals("C", bean.getDequeField().pollFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testObjectBeanWithStrings() throws Exception {
|
||||
StringReader stringReader = new StringReader(TestUtils.readContent(JSONInterceptorTest.class
|
||||
.getResource("json-8.txt")));
|
||||
Object json = JSONUtil.deserialize(stringReader);
|
||||
assertNotNull(json);
|
||||
assertTrue(json instanceof Map);
|
||||
Map jsonMap = (Map) json;
|
||||
Map<?, ?> jsonMap = (Map<?, ?>) json;
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
WrapperClassBean bean = new WrapperClassBean();
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(Boolean.TRUE, bean.getBooleanField());
|
||||
assertEquals("test", bean.getStringField());
|
||||
assertEquals(new Integer(10), bean.getIntField());
|
||||
assertEquals(new Character('s'), bean.getCharField());
|
||||
assertEquals(10.1d, bean.getDoubleField());
|
||||
assertEquals(new Byte((byte) 3), bean.getByteField());
|
||||
assertEquals(Integer.valueOf(10), bean.getIntField());
|
||||
assertEquals(Character.valueOf('s'), bean.getCharField());
|
||||
assertEquals(Double.valueOf(10.1d), bean.getDoubleField());
|
||||
assertEquals(Byte.valueOf((byte) 3), bean.getByteField());
|
||||
|
||||
assertEquals(null, bean.getListField());
|
||||
assertEquals(null, bean.getListMapField());
|
||||
assertEquals(null, bean.getMapListField());
|
||||
assertEquals(null, bean.getArrayMapField());
|
||||
assertNull(bean.getListField());
|
||||
assertNull(bean.getListMapField());
|
||||
assertNull(bean.getMapListField());
|
||||
assertNull(bean.getArrayMapField());
|
||||
}
|
||||
|
||||
public void testInfiniteLoop() throws JSONException {
|
||||
@Test
|
||||
public void testInfiniteLoop() {
|
||||
try {
|
||||
JSONReader reader = new JSONReader();
|
||||
reader.read("[1,\"a]");
|
||||
fail("Should have thrown an exception");
|
||||
} catch (JSONException e) {
|
||||
// I can't get JUnit to ignore the exception
|
||||
// @Test(expected = JSONException.class)
|
||||
assertEquals("Input string is not well formed JSON (invalid char \uFFFF)", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void testParseBadInput() throws JSONException {
|
||||
@Test
|
||||
public void testParseBadInput() {
|
||||
try {
|
||||
JSONReader reader = new JSONReader();
|
||||
reader.read("[1,\"a\"1]");
|
||||
@@ -184,4 +206,127 @@ public class JSONPopulatorTest extends TestCase {
|
||||
// @Test(expected = JSONException.class)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeLocalDate() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("localDate", "2026-02-27");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(LocalDate.of(2026, 2, 27), bean.getLocalDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeLocalDateTime() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("localDateTime", "2026-02-27T12:00:00");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(LocalDateTime.of(2026, 2, 27, 12, 0, 0), bean.getLocalDateTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeLocalTime() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("localTime", "12:00:00");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(LocalTime.of(12, 0, 0), bean.getLocalTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeZonedDateTime() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("zonedDateTime", "2026-02-27T12:00:00+01:00[Europe/Paris]");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(ZonedDateTime.of(2026, 2, 27, 12, 0, 0, 0, ZoneId.of("Europe/Paris")), bean.getZonedDateTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeOffsetDateTime() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("offsetDateTime", "2026-02-27T12:00:00+01:00");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(OffsetDateTime.of(2026, 2, 27, 12, 0, 0, 0, ZoneOffset.ofHours(1)), bean.getOffsetDateTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeInstant() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("instant", "2026-02-27T11:00:00Z");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(Instant.parse("2026-02-27T11:00:00Z"), bean.getInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeCalendar() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("calendar", "2012-12-23T10:10:10");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertNotNull(bean.getCalendar());
|
||||
Calendar expected = Calendar.getInstance();
|
||||
expected.setTimeZone(TimeZone.getDefault());
|
||||
expected.set(2012, Calendar.DECEMBER, 23, 10, 10, 10);
|
||||
expected.set(Calendar.MILLISECOND, 0);
|
||||
assertEquals(expected.getTimeInMillis() / 1000, bean.getCalendar().getTimeInMillis() / 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeLocalDateWithCustomFormat() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("customFormatDate", "27/02/2026");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(LocalDate.of(2026, 2, 27), bean.getCustomFormatDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeInstantWithCustomFormat() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("customFormatInstant", "2026-02-27 11:00:00");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(Instant.parse("2026-02-27T11:00:00Z"), bean.getCustomFormatInstant());
|
||||
}
|
||||
|
||||
@Test(expected = JSONException.class)
|
||||
public void testDeserializeMalformedTemporalThrowsException() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("localDate", "not-a-date");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
}
|
||||
|
||||
@Test(expected = JSONException.class)
|
||||
public void testDeserializeMalformedInstantThrowsException() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("instant", "not-an-instant");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeLocalDateTimeWithCustomFormat() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("customFormatDateTime", "27/02/2026 14:30");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(LocalDateTime.of(2026, 2, 27, 14, 30), bean.getCustomFormatDateTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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.json.annotations.JSON;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Calendar;
|
||||
|
||||
public class TemporalBean {
|
||||
|
||||
private LocalDate localDate;
|
||||
private LocalDateTime localDateTime;
|
||||
private LocalTime localTime;
|
||||
private ZonedDateTime zonedDateTime;
|
||||
private OffsetDateTime offsetDateTime;
|
||||
private Instant instant;
|
||||
private Calendar calendar;
|
||||
private LocalDate customFormatDate;
|
||||
|
||||
public LocalDate getLocalDate() {
|
||||
return localDate;
|
||||
}
|
||||
|
||||
public void setLocalDate(LocalDate localDate) {
|
||||
this.localDate = localDate;
|
||||
}
|
||||
|
||||
public LocalDateTime getLocalDateTime() {
|
||||
return localDateTime;
|
||||
}
|
||||
|
||||
public void setLocalDateTime(LocalDateTime localDateTime) {
|
||||
this.localDateTime = localDateTime;
|
||||
}
|
||||
|
||||
public LocalTime getLocalTime() {
|
||||
return localTime;
|
||||
}
|
||||
|
||||
public void setLocalTime(LocalTime localTime) {
|
||||
this.localTime = localTime;
|
||||
}
|
||||
|
||||
public ZonedDateTime getZonedDateTime() {
|
||||
return zonedDateTime;
|
||||
}
|
||||
|
||||
public void setZonedDateTime(ZonedDateTime zonedDateTime) {
|
||||
this.zonedDateTime = zonedDateTime;
|
||||
}
|
||||
|
||||
public OffsetDateTime getOffsetDateTime() {
|
||||
return offsetDateTime;
|
||||
}
|
||||
|
||||
public void setOffsetDateTime(OffsetDateTime offsetDateTime) {
|
||||
this.offsetDateTime = offsetDateTime;
|
||||
}
|
||||
|
||||
public Instant getInstant() {
|
||||
return instant;
|
||||
}
|
||||
|
||||
public void setInstant(Instant instant) {
|
||||
this.instant = instant;
|
||||
}
|
||||
|
||||
public Calendar getCalendar() {
|
||||
return calendar;
|
||||
}
|
||||
|
||||
public void setCalendar(Calendar calendar) {
|
||||
this.calendar = calendar;
|
||||
}
|
||||
|
||||
@JSON(format = "dd/MM/yyyy")
|
||||
public LocalDate getCustomFormatDate() {
|
||||
return customFormatDate;
|
||||
}
|
||||
|
||||
@JSON(format = "dd/MM/yyyy")
|
||||
public void setCustomFormatDate(LocalDate customFormatDate) {
|
||||
this.customFormatDate = customFormatDate;
|
||||
}
|
||||
|
||||
private LocalDateTime customFormatDateTime;
|
||||
|
||||
@JSON(format = "dd/MM/yyyy HH:mm")
|
||||
public LocalDateTime getCustomFormatDateTime() {
|
||||
return customFormatDateTime;
|
||||
}
|
||||
|
||||
@JSON(format = "dd/MM/yyyy HH:mm")
|
||||
public void setCustomFormatDateTime(LocalDateTime customFormatDateTime) {
|
||||
this.customFormatDateTime = customFormatDateTime;
|
||||
}
|
||||
|
||||
private Instant customFormatInstant;
|
||||
|
||||
@JSON(format = "yyyy-MM-dd HH:mm:ss")
|
||||
public Instant getCustomFormatInstant() {
|
||||
return customFormatInstant;
|
||||
}
|
||||
|
||||
@JSON(format = "yyyy-MM-dd HH:mm:ss")
|
||||
public void setCustomFormatInstant(Instant customFormatInstant) {
|
||||
this.customFormatInstant = customFormatInstant;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
date: 2026-02-27T12:00:00+01:00
|
||||
topic: "WW-4428: Add java.time (LocalDate, LocalDateTime) support to JSON plugin"
|
||||
tags: [research, codebase, json-plugin, java-time, localdate, localdatetime, serialization, deserialization]
|
||||
status: complete
|
||||
git_commit: 4d2eb938351b0e84a393979045248e21b75766e9
|
||||
---
|
||||
|
||||
# Research: WW-4428 — Java 8 Date/Time Support in JSON Plugin
|
||||
|
||||
**Date**: 2026-02-27
|
||||
|
||||
## Research Question
|
||||
|
||||
What is the current state of Java 8 `java.time` support (LocalDate, LocalDateTime, etc.) in the Struts JSON plugin, and what changes are needed to implement WW-4428?
|
||||
|
||||
## Summary
|
||||
|
||||
The JSON plugin has **zero java.time support**. Only `java.util.Date` and `java.util.Calendar` are handled. Java 8 date types like `LocalDate` and `LocalDateTime` fall through to JavaBean introspection during serialization (producing garbage like `{"dayOfMonth":23,"month":"DECEMBER",...}`) and throw exceptions during deserialization. The core module already has comprehensive java.time support via `DateConverter`, but none of it is wired into the JSON plugin.
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### 1. Serialization — DefaultJSONWriter
|
||||
|
||||
**File**: [`plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java`](https://github.com/apache/struts/blob/4d2eb938351b0e84a393979045248e21b75766e9/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java)
|
||||
|
||||
The `process()` method (line ~163) dispatches on type:
|
||||
|
||||
```java
|
||||
} else if (object instanceof Date) {
|
||||
this.date((Date) object, method);
|
||||
} else if (object instanceof Calendar) {
|
||||
this.date(((Calendar) object).getTime(), method);
|
||||
}
|
||||
```
|
||||
|
||||
There is no branch for `java.time.temporal.TemporalAccessor` or any specific java.time type. These objects fall through to `processCustom()` → `bean()`, which introspects them as JavaBeans.
|
||||
|
||||
The `date()` method (line ~335) only accepts `java.util.Date` and uses `SimpleDateFormat`:
|
||||
|
||||
```java
|
||||
protected void date(Date date, Method method) {
|
||||
// uses SimpleDateFormat with JSONUtil.RFC3339_FORMAT default
|
||||
}
|
||||
```
|
||||
|
||||
The `setDateFormatter()` method (line ~487) only creates a `SimpleDateFormat`.
|
||||
|
||||
### 2. Deserialization — JSONPopulator
|
||||
|
||||
**File**: [`plugins/json/src/main/java/org/apache/struts2/json/JSONPopulator.java`](https://github.com/apache/struts/blob/4d2eb938351b0e84a393979045248e21b75766e9/plugins/json/src/main/java/org/apache/struts2/json/JSONPopulator.java)
|
||||
|
||||
`isJSONPrimitive()` (line ~92) only recognizes `Date.class`:
|
||||
|
||||
```java
|
||||
return clazz.isPrimitive() || clazz.equals(String.class) || clazz.equals(Date.class) ...
|
||||
```
|
||||
|
||||
`convertPrimitive()` (line ~255) only handles `Date.class` via `SimpleDateFormat.parse()`. Java.time types will throw `JSONException("Incompatible types for property ...")`.
|
||||
|
||||
### 3. Format Configuration
|
||||
|
||||
**File**: [`plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java`](https://github.com/apache/struts/blob/4d2eb938351b0e84a393979045248e21b75766e9/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java)
|
||||
|
||||
- `RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"` (line 53) — the default format
|
||||
- The java.time equivalent is `DateTimeFormatter.ISO_LOCAL_DATE_TIME`
|
||||
|
||||
### 4. @JSON Annotation
|
||||
|
||||
**File**: [`plugins/json/src/main/java/org/apache/struts2/json/annotations/JSON.java`](https://github.com/apache/struts/blob/4d2eb938351b0e84a393979045248e21b75766e9/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSON.java)
|
||||
|
||||
Has `format()` attribute for per-property date format overrides — currently only used with `SimpleDateFormat`. Should be extended to work with `DateTimeFormatter` for java.time types.
|
||||
|
||||
### 5. @JSONFieldBridge Workaround
|
||||
|
||||
**Directory**: `plugins/json/src/main/java/org/apache/struts2/json/bridge/`
|
||||
|
||||
The `FieldBridge` interface provides a manual escape hatch (`objectToString`) but only supports serialization, not deserialization.
|
||||
|
||||
### 6. Core Module Already Has java.time Support
|
||||
|
||||
**File**: [`core/src/main/java/org/apache/struts2/conversion/impl/DateConverter.java`](https://github.com/apache/struts/blob/4d2eb938351b0e84a393979045248e21b75766e9/core/src/main/java/org/apache/struts2/conversion/impl/DateConverter.java)
|
||||
|
||||
Handles `LocalDate`, `LocalDateTime`, `LocalTime`, `OffsetDateTime` using `DateTimeFormatter.parseBest()`. This is not wired into the JSON plugin.
|
||||
|
||||
### 7. Test Coverage
|
||||
|
||||
- `DefaultJSONWriterTest.java` — only tests `java.util.Date` serialization (lines 115-142)
|
||||
- `SingleDateBean.java` — test fixture with only a `java.util.Date` field
|
||||
- `JSONPopulatorTest.java` — no dedicated date deserialization test
|
||||
- Zero tests for any java.time type
|
||||
|
||||
## Gap Analysis
|
||||
|
||||
| Type | Serialization | Deserialization |
|
||||
|---|---|---|
|
||||
| `java.util.Date` | Supported | Supported |
|
||||
| `java.util.Calendar` | Supported (→ Date) | Not supported |
|
||||
| `java.time.LocalDate` | **Not supported** | **Not supported** |
|
||||
| `java.time.LocalDateTime` | **Not supported** | **Not supported** |
|
||||
| `java.time.LocalTime` | **Not supported** | **Not supported** |
|
||||
| `java.time.ZonedDateTime` | **Not supported** | **Not supported** |
|
||||
| `java.time.Instant` | **Not supported** | **Not supported** |
|
||||
| `java.time.OffsetDateTime` | **Not supported** | **Not supported** |
|
||||
|
||||
## Implementation Points
|
||||
|
||||
To implement WW-4428, changes are needed in:
|
||||
|
||||
### DefaultJSONWriter.java
|
||||
1. Add `instanceof` checks in `process()` for `LocalDate`, `LocalDateTime`, `LocalTime`, `ZonedDateTime`, `Instant`, `OffsetDateTime` (or a blanket `TemporalAccessor` check)
|
||||
2. Add a new `temporal(TemporalAccessor, Method)` method using `DateTimeFormatter`
|
||||
3. Use sensible defaults: `ISO_LOCAL_DATE` for `LocalDate`, `ISO_LOCAL_DATE_TIME` for `LocalDateTime`, etc.
|
||||
4. Respect `@JSON(format=...)` annotation via `DateTimeFormatter.ofPattern()`
|
||||
|
||||
### JSONPopulator.java
|
||||
1. Extend `isJSONPrimitive()` to recognize java.time classes
|
||||
2. Extend `convertPrimitive()` to parse java.time types from strings using `DateTimeFormatter`
|
||||
3. Respect `@JSON(format=...)` for custom formats
|
||||
|
||||
### JSONWriter.java (interface)
|
||||
1. Consider adding `setDateTimeFormatter(String)` or reusing `setDateFormatter()` for both legacy and java.time
|
||||
|
||||
### Tests
|
||||
1. Add test beans with java.time fields
|
||||
2. Add serialization tests for each supported java.time type
|
||||
3. Add deserialization tests for each type
|
||||
4. Test `@JSON(format=...)` with java.time types
|
||||
5. Test default format behavior
|
||||
|
||||
## Architecture Insights
|
||||
|
||||
- The JSON plugin was designed when Java 6 was the target, hence `SimpleDateFormat` throughout
|
||||
- The `@JSON(format=...)` annotation is the natural extension point for per-field formatting
|
||||
- The core module's `DateConverter` shows the established pattern for handling java.time in Struts
|
||||
- Since Struts now requires Java 17+, there are no compatibility concerns with using java.time directly
|
||||
|
||||
## Historical Context
|
||||
|
||||
- WW-4428 was filed in December 2014 (Struts 2.3.20 era, targeting Java 6/7)
|
||||
- Original constraint: couldn't add java.time directly due to Java 6/7 compatibility
|
||||
- Related ticket: WW-5016 — Support java 8 date time in the date tag (already implemented in `components/Date.java`)
|
||||
- No prior research documents in thoughts/ for this topic
|
||||
|
||||
## Related Research
|
||||
|
||||
None found in thoughts/shared/research/.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Should `Instant` be serialized as epoch millis (number) or ISO-8601 string?
|
||||
2. Should `ZonedDateTime` include the zone info in the default format?
|
||||
3. Should the implementation use a blanket `TemporalAccessor` check or individual type checks?
|
||||
4. Should `Calendar` deserialization also be added while we're at it?
|
||||
Reference in New Issue
Block a user