diff --git a/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java b/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java index bd2ebe90e5..16d13df3d5 100644 --- a/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java +++ b/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java @@ -38,6 +38,15 @@ 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, BeanInfo> BEAN_INFO_CACHE_IGNORE_HIERARCHY = new ConcurrentHashMap<>(); private static final ConcurrentMap, BeanInfo> BEAN_INFO_CACHE = new ConcurrentHashMap<>(); - private StringBuilder buf = new StringBuilder(); + private final StringBuilder buf = new StringBuilder(); private Stack stack = new Stack<>(); private boolean ignoreHierarchy = true; private Object root; @@ -73,10 +82,9 @@ public class DefaultJSONWriter implements JSONWriter { private String exprStack = ""; private Collection excludeProperties; private Collection 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 String write(Object object) throws JSONException { } /** - * @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 String write(Object object, Collection excludeProperties, * * @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 @@ protected void value(Object object, Method method) throws JSONException { * * @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 @@ protected void process(Object object, Method method) throws JSONException { 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 @@ protected void process(Object object, Method method) throws JSONException { * * @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 @@ protected void processCustom(Object object, Method method) throws JSONException * 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 @@ protected Method findBaseAccessor(Class clazz, Method accessor) { } 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 @@ protected Method findBaseAccessor(Class clazz, Method accessor) { * 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 @@ protected String expandExpr(int i) { } protected String expandExpr(String property) { - if (this.exprStack.length() == 0) { + if (this.exprStack.isEmpty()) { return property; } return this.exprStack + "." + property; @@ -401,9 +401,7 @@ protected boolean shouldExcludeProperty(String expr) { if (this.excludeProperties != null) { for (Pattern pattern : this.excludeProperties) { if (pattern.matcher(expr).matches()) { - if (LOG.isDebugEnabled()) { - LOG.debug("Ignoring property because of exclude rule: " + expr); - } + LOG.debug("Ignoring property because of exclude rule: {}", expr); return true; } } @@ -415,9 +413,7 @@ protected boolean shouldExcludeProperty(String expr) { 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 @@ protected void date(Date date, Method method) { JSON json = null; if (method != null) json = method.getAnnotation(JSON.class); - if (this.formatter == null) - this.formatter = new SimpleDateFormat(JSONUtil.RFC3339_FORMAT); + 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 void setEnumAsBean(boolean enumAsBean) { @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 String getName() { 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; diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONPopulator.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONPopulator.java index ef1ac77bdd..2a2e65b314 100644 --- a/plugins/json/src/main/java/org/apache/struts2/json/JSONPopulator.java +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONPopulator.java @@ -32,6 +32,15 @@ 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 @@ private static boolean isJSONPrimitive(Class clazz) { || 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 @@ else if (String.class.equals(clazz)) 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 @@ else if (Character.TYPE.equals(clazz) || Character.class.equals(clazz)) { return value; } + private T parseTemporalFromString(Object value, Method method, DateTimeFormatter defaultFormatter, TemporalQuery 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); + } + } + } diff --git a/plugins/json/src/test/java/org/apache/struts2/json/DefaultJSONWriterTest.java b/plugins/json/src/test/java/org/apache/struts2/json/DefaultJSONWriterTest.java index 1f25cabbc8..886e717c5e 100644 --- a/plugins/json/src/test/java/org/apache/struts2/json/DefaultJSONWriterTest.java +++ b/plugins/json/src/test/java/org/apache/struts2/json/DefaultJSONWriterTest.java @@ -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.TimeZone; public class DefaultJSONWriterTest extends StrutsTestCase { - @Test public void testWrite() throws Exception { Bean bean1 = new Bean(); bean1.setStringField("str"); @@ -52,7 +58,6 @@ public void testWrite() throws Exception { 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 void testWriteExcludeNull() throws Exception { 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 void setMap(Map map) { } } - @Test public void testWriteAnnotatedBean() throws Exception { AnnotatedBean bean1 = new AnnotatedBean(); bean1.setStringField("str"); @@ -111,7 +115,6 @@ public void testWriteAnnotatedBean() throws Exception { 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 void testWriteBeanWithList() throws Exception { TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-04.txt"), json); } - private class BeanWithList extends Bean { + private static class BeanWithList extends Bean { private List errors; public List getErrors() { @@ -146,10 +149,10 @@ public void setErrors(List errors) { } } - 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 void setUrl(URL url) { } } - @Test public void testCanSerializeADate() throws Exception { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); @@ -174,7 +176,6 @@ public void testCanSerializeADate() throws Exception { 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 void testCanSetDefaultDateFormat() throws Exception { 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()); + } + } diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java index c3a2a3bfe7..08d15e852a 100644 --- a/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java @@ -23,8 +23,18 @@ 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 void testParseBadInput() throws JSONException { // @Test(expected = JSONException.class) } } + + public void testDeserializeLocalDate() throws Exception { + JSONPopulator populator = new JSONPopulator(); + TemporalBean bean = new TemporalBean(); + Map 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 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 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 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 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 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 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 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 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 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 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 jsonMap = new HashMap<>(); + jsonMap.put("localDate", "not-a-date"); + try { + populator.populateObject(bean, jsonMap); + fail("Should have thrown JSONException"); + } catch (JSONException e) { + // expected + } + } } diff --git a/plugins/json/src/test/java/org/apache/struts2/json/TemporalBean.java b/plugins/json/src/test/java/org/apache/struts2/json/TemporalBean.java new file mode 100644 index 0000000000..a2f6ae3247 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/TemporalBean.java @@ -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; + } +}