mirror of
https://github.com/apache/struts.git
synced 2026-08-06 07:06:58 +00:00
WW-4428 feat(json): add java.time serialization and deserialization support (#1616)
- 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 - Add Calendar deserialization support (was serialize-only) - Add comprehensive tests including custom formats, null handling, malformed input, and round-trip serialization/deserialization Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,15 @@ 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.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
@@ -60,12 +69,12 @@ 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 final StringBuilder buf = new StringBuilder();
|
||||
private Stack<Object> stack = new Stack<>();
|
||||
private boolean ignoreHierarchy = true;
|
||||
private Object root;
|
||||
@@ -73,10 +82,9 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
private String exprStack = "";
|
||||
private Collection<Pattern> excludeProperties;
|
||||
private Collection<Pattern> includeProperties;
|
||||
private DateFormat formatter;
|
||||
private DateFormat dateFormatter;
|
||||
private boolean enumAsBean = ENUM_AS_BEAN_DEFAULT;
|
||||
private boolean excludeNullProperties;
|
||||
private boolean cacheBeanInfo = true;
|
||||
private boolean excludeProxyProperties;
|
||||
|
||||
@Inject(value = JSONConstants.RESULT_EXCLUDE_PROXY_PROPERTIES, required = false)
|
||||
@@ -95,14 +103,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
|
||||
*/
|
||||
@@ -128,7 +132,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 {
|
||||
@@ -159,8 +162,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);
|
||||
@@ -185,6 +187,8 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
this.date((Date) object, method);
|
||||
} else if (object instanceof Calendar) {
|
||||
this.date(((Calendar) object).getTime(), method);
|
||||
} else if (object instanceof TemporalAccessor) {
|
||||
this.temporal((TemporalAccessor) object, method);
|
||||
} else if (object instanceof Locale) {
|
||||
this.string(object);
|
||||
} else if (object instanceof Enum) {
|
||||
@@ -201,8 +205,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);
|
||||
@@ -212,8 +215,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("{");
|
||||
@@ -334,23 +336,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;
|
||||
@@ -361,8 +362,7 @@ 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 {
|
||||
if (enumAsBean) {
|
||||
@@ -385,7 +385,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;
|
||||
@@ -401,9 +401,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
if (this.excludeProperties != null) {
|
||||
for (Pattern pattern : this.excludeProperties) {
|
||||
if (pattern.matcher(expr).matches()) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Ignoring property because of exclude rule: " + expr);
|
||||
}
|
||||
LOG.debug("Ignoring property because of exclude rule: {}", expr);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -415,9 +413,7 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (LOG.isDebugEnabled()){
|
||||
LOG.debug("Ignoring property because of include rule: " + expr);
|
||||
}
|
||||
LOG.debug("Ignoring property because of include rule: {}", expr);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -498,14 +494,52 @@ 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.dateFormatter == null)
|
||||
this.dateFormatter = new SimpleDateFormat(JSONUtil.RFC3339_FORMAT);
|
||||
|
||||
DateFormat formatter = (json != null) && (json.format().length() > 0) ? new SimpleDateFormat(json
|
||||
.format()) : this.formatter;
|
||||
.format()) : this.dateFormatter;
|
||||
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().length() > 0) {
|
||||
formatter = DateTimeFormatter.ofPattern(json.format());
|
||||
if (temporal instanceof Instant) {
|
||||
formatter = formatter.withZone(ZoneOffset.UTC);
|
||||
}
|
||||
} else {
|
||||
formatter = getDefaultDateTimeFormatter(temporal);
|
||||
}
|
||||
this.string(formatter.format(temporal));
|
||||
}
|
||||
|
||||
private 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
|
||||
*/
|
||||
@@ -664,13 +698,13 @@ public class DefaultJSONWriter implements JSONWriter {
|
||||
@Override
|
||||
public void setDateFormatter(String defaultDateFormat) {
|
||||
if (defaultDateFormat != null) {
|
||||
this.formatter = new SimpleDateFormat(defaultDateFormat);
|
||||
this.dateFormatter = new SimpleDateFormat(defaultDateFormat);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setCacheBeanInfo(boolean cacheBeanInfo) {
|
||||
this.cacheBeanInfo = cacheBeanInfo;
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -699,7 +733,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.*;
|
||||
|
||||
/**
|
||||
@@ -132,7 +141,11 @@ public class JSONPopulator {
|
||||
|| 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")
|
||||
@@ -367,6 +380,32 @@ public class JSONPopulator {
|
||||
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().length() > 0) ? 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);
|
||||
@@ -424,4 +463,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().length() > 0) {
|
||||
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().length() > 0) {
|
||||
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,13 +19,20 @@
|
||||
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.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -33,7 +40,6 @@ import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
@Test
|
||||
public void testWrite() throws Exception {
|
||||
Bean bean1 = new Bean();
|
||||
bean1.setStringField("str");
|
||||
@@ -52,7 +58,6 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-01.txt"), json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteExcludeNull() throws Exception {
|
||||
BeanWithMap bean1 = new BeanWithMap();
|
||||
bean1.setStringField("str");
|
||||
@@ -78,7 +83,7 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-03.txt"), json);
|
||||
}
|
||||
|
||||
private class BeanWithMap extends Bean {
|
||||
private static class BeanWithMap extends Bean {
|
||||
private Map map;
|
||||
|
||||
public Map getMap() {
|
||||
@@ -90,7 +95,6 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteAnnotatedBean() throws Exception {
|
||||
AnnotatedBean bean1 = new AnnotatedBean();
|
||||
bean1.setStringField("str");
|
||||
@@ -111,7 +115,6 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-02.txt"), json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteBeanWithList() throws Exception {
|
||||
BeanWithList bean1 = new BeanWithList();
|
||||
bean1.setStringField("str");
|
||||
@@ -134,7 +137,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 +149,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;
|
||||
}
|
||||
@@ -159,7 +162,6 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanSerializeADate() throws Exception {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
||||
|
||||
@@ -174,7 +176,6 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
assertEquals("{\"date\":\"2012-12-23T10:10:10\"}", json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanSetDefaultDateFormat() throws Exception {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
||||
|
||||
@@ -188,4 +189,144 @@ public class DefaultJSONWriterTest extends StrutsTestCase {
|
||||
assertEquals("{\"date\":\"12-23-2012\"}", json);
|
||||
}
|
||||
|
||||
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\""));
|
||||
}
|
||||
|
||||
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\""));
|
||||
}
|
||||
|
||||
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\""));
|
||||
}
|
||||
|
||||
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]\""));
|
||||
}
|
||||
|
||||
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\""));
|
||||
}
|
||||
|
||||
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\""));
|
||||
}
|
||||
|
||||
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\""));
|
||||
}
|
||||
|
||||
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\""));
|
||||
}
|
||||
|
||||
public void testSerializeNullTemporalFields() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean, null, null, true);
|
||||
assertEquals("{}", json);
|
||||
}
|
||||
|
||||
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\""));
|
||||
}
|
||||
|
||||
public void testSerializeOffsetDateTimeWithCustomFormat() throws Exception {
|
||||
TemporalBean bean = new TemporalBean();
|
||||
bean.setCustomFormatOffsetDateTime(OffsetDateTime.of(2026, 2, 27, 14, 30, 0, 0, ZoneOffset.ofHours(1)));
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"customFormatOffsetDateTime\":\"27\\/02\\/2026 14:30:00+01:00\""));
|
||||
}
|
||||
|
||||
public void testRoundTripLocalDate() throws Exception {
|
||||
LocalDate original = LocalDate.of(2026, 2, 27);
|
||||
TemporalBean writeBean = new TemporalBean();
|
||||
writeBean.setLocalDate(original);
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(writeBean);
|
||||
|
||||
Object parsed = JSONUtil.deserialize(json);
|
||||
TemporalBean readBean = new TemporalBean();
|
||||
new JSONPopulator().populateObject(readBean, (Map) parsed);
|
||||
assertEquals(original, readBean.getLocalDate());
|
||||
}
|
||||
|
||||
public void testRoundTripInstant() throws Exception {
|
||||
Instant original = Instant.parse("2026-02-27T11:00:00Z");
|
||||
TemporalBean writeBean = new TemporalBean();
|
||||
writeBean.setInstant(original);
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(writeBean);
|
||||
|
||||
Object parsed = JSONUtil.deserialize(json);
|
||||
TemporalBean readBean = new TemporalBean();
|
||||
new JSONPopulator().populateObject(readBean, (Map) parsed);
|
||||
assertEquals(original, readBean.getInstant());
|
||||
}
|
||||
|
||||
public void testRoundTripZonedDateTime() throws Exception {
|
||||
ZonedDateTime original = ZonedDateTime.of(2026, 2, 27, 12, 0, 0, 0, ZoneId.of("Europe/Paris"));
|
||||
TemporalBean writeBean = new TemporalBean();
|
||||
writeBean.setZonedDateTime(original);
|
||||
|
||||
JSONWriter jsonWriter = new DefaultJSONWriter();
|
||||
String json = jsonWriter.write(writeBean);
|
||||
|
||||
Object parsed = JSONUtil.deserialize(json);
|
||||
TemporalBean readBean = new TemporalBean();
|
||||
new JSONPopulator().populateObject(readBean, (Map) parsed);
|
||||
assertEquals(original, readBean.getZonedDateTime());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,8 +23,18 @@ 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;
|
||||
@@ -184,4 +194,121 @@ public class JSONPopulatorTest extends TestCase {
|
||||
// @Test(expected = JSONException.class)
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
public void testDeserializeOffsetDateTimeWithCustomFormat() throws Exception {
|
||||
JSONPopulator populator = new JSONPopulator();
|
||||
TemporalBean bean = new TemporalBean();
|
||||
Map<String, Object> jsonMap = new HashMap<>();
|
||||
jsonMap.put("customFormatOffsetDateTime", "27/02/2026 14:30:00+01:00");
|
||||
populator.populateObject(bean, jsonMap);
|
||||
assertEquals(OffsetDateTime.of(2026, 2, 27, 14, 30, 0, 0, ZoneOffset.ofHours(1)), bean.getCustomFormatOffsetDateTime());
|
||||
}
|
||||
|
||||
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");
|
||||
try {
|
||||
populator.populateObject(bean, jsonMap);
|
||||
fail("Should have thrown JSONException");
|
||||
} catch (JSONException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
private OffsetDateTime customFormatOffsetDateTime;
|
||||
|
||||
@JSON(format = "dd/MM/yyyy HH:mm:ssXXX")
|
||||
public OffsetDateTime getCustomFormatOffsetDateTime() {
|
||||
return customFormatOffsetDateTime;
|
||||
}
|
||||
|
||||
@JSON(format = "dd/MM/yyyy HH:mm:ssXXX")
|
||||
public void setCustomFormatOffsetDateTime(OffsetDateTime customFormatOffsetDateTime) {
|
||||
this.customFormatOffsetDateTime = customFormatOffsetDateTime;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user