This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 293fc1495b Built-in support for marshalling date/time objects
293fc1495b is described below
commit 293fc1495b6269036880e83bf8ce3a7296093443
Author: James Bognar <[email protected]>
AuthorDate: Sat Feb 28 09:03:36 2026 -0500
Built-in support for marshalling date/time objects
---
TODO.md | 3 +-
.../main/java/org/apache/juneau/BeanSession.java | 13 +
.../src/main/java/org/apache/juneau/ClassMeta.java | 15 +-
.../apache/juneau/csv/CsvSerializerSession.java | 29 +-
.../org/apache/juneau/html/HtmlParserSession.java | 5 +
.../apache/juneau/html/HtmlSerializerSession.java | 17 +
.../org/apache/juneau/json/JsonParserSession.java | 3 +
.../apache/juneau/json/JsonSerializerSession.java | 5 +
.../juneau/msgpack/MsgPackParserSession.java | 3 +
.../juneau/msgpack/MsgPackSerializerSession.java | 5 +
.../apache/juneau/oapi/OpenApiParserSession.java | 31 +-
.../juneau/oapi/OpenApiSerializerSession.java | 28 +-
.../org/apache/juneau/parser/ParserSession.java | 3 +
.../juneau/serializer/SerializerSession.java | 4 +
.../java/org/apache/juneau/swap/DefaultSwaps.java | 16 -
.../apache/juneau/swaps/TemporalCalendarSwap.java | 5 +
.../org/apache/juneau/swaps/TemporalDateSwap.java | 5 +
.../java/org/apache/juneau/swaps/TemporalSwap.java | 5 +
.../juneau/swaps/XMLGregorianCalendarSwap.java | 5 +
.../org/apache/juneau/uon/UonParserSession.java | 3 +
.../apache/juneau/uon/UonSerializerSession.java | 5 +
.../java/org/apache/juneau/utils/Iso8601Utils.java | 313 ++++++++++++++++++
.../org/apache/juneau/xml/XmlParserSession.java | 3 +
.../apache/juneau/xml/XmlSerializerSession.java | 5 +
.../juneau/a/rttests/RoundTripDateTime_Test.java | 368 +++++++++++++++++++++
.../BuiltInDateTimeSerialization_Test.java | 313 ++++++++++++++++++
.../juneau/transforms/DefaultSwaps_Test.java | 30 ++
27 files changed, 1166 insertions(+), 74 deletions(-)
diff --git a/TODO.md b/TODO.md
index 5b6f7e5210..4981320347 100644
--- a/TODO.md
+++ b/TODO.md
@@ -6,7 +6,6 @@
- Find places where we defined fields and methods as _foobar and convert them
to foobar_
- Investigate navlinks URL generation issue: Either
"request:?Accept=text/json&plainText=true" should be supported, or
"request:/?Accept=text/json&plainText=true" should not append '/' to the
request URL. Currently, "request:/?Accept=..." generates URLs like
"http://localhost:5000/rest/db/request:?Accept=..." which is incorrect.
- Determine if it's possible to add a "short" field to @Schema for AI purposes.
-- Ensure Juneau support record types for serializing/parsing.
- JsonSchemaParser should be able to produce JsonSchema beans.
- JsonSchemaGenerator should return JsonSchema beans.
- Create full-fledged CSV serializer/parser support.
@@ -15,5 +14,5 @@
- ClassInfo should have a findGetter(String propertyName) convenience method.
- Make sure @Beanp("*") works on plain fields.
- Add schema validation to beans during parsing.
-- Duration objects should be supported for serialization by default.
+
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java
index d5cfd08d5e..0fc5b5a0ec 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java
@@ -41,6 +41,7 @@ import org.apache.juneau.commons.reflect.*;
import org.apache.juneau.commons.time.*;
import org.apache.juneau.commons.utils.*;
import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.*;
/**
* Session object that lives for the duration of a single use of {@link
BeanContext}.
@@ -1214,6 +1215,18 @@ public class BeanSession extends ContextSession {
return (T)swap.swap(this, value);
}
+ if (to.isCharSequence() &&
(from.isDateOrCalendarOrTemporal() || from.isDuration()))
+ return (T) Iso8601Utils.format(value, from,
getTimeZone());
+
+ if (to.isDateOrCalendarOrTemporal() && value instanceof
CharSequence)
+ return (T) Iso8601Utils.parse(value.toString(),
to, getTimeZone());
+
+ if (to.isDuration() && value instanceof CharSequence)
+ return (T) Duration.parse(value.toString());
+
+ if (to.isDateOrCalendarOrTemporal() && value instanceof
Number)
+ return (T)
Iso8601Utils.fromEpochMillis(((Number)value).longValue(), to, getTimeZone());
+
if (to.isPrimitive()) {
if (to.isNumber()) {
if (from.isNumber()) {
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
index 42b94cb662..95f25d1384 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
@@ -27,6 +27,7 @@ import java.lang.annotation.*;
import java.lang.reflect.*;
import java.lang.reflect.Proxy;
import java.net.*;
+import java.time.*;
import java.time.temporal.*;
import java.util.*;
import java.util.List;
@@ -116,7 +117,8 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
BEAN(20),
ITERATOR(21),
ITERABLE(22),
- STREAM(23);
+ STREAM(23),
+ DURATION(24);
private final int mask;
@@ -236,8 +238,12 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
} else if (isAssignableTo(Calendar.class)) {
cat.set(CALENDAR);
}
+ } else if (is(Duration.class)) {
+ cat.set(DURATION);
} else if (isAssignableTo(Temporal.class)) {
cat.set(TEMPORAL);
+ } else if
(isAssignableTo(javax.xml.datatype.XMLGregorianCalendar.class)) {
+ cat.set(CALENDAR);
} else if (inner().isArray()) {
cat.set(ARRAY);
} else if (isAssignableToAny(URL.class, URI.class) ||
ap.has(Uri.class, this)) {
@@ -1173,6 +1179,13 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
*/
public boolean isTemporal() { return cat.is(TEMPORAL); }
+ /**
+ * Returns <jk>true</jk> if this class is a {@link java.time.Duration}.
+ *
+ * @return <jk>true</jk> if this class is a {@link java.time.Duration}.
+ */
+ public boolean isDuration() { return cat.is(DURATION); }
+
/**
* Returns <jk>true</jk> if this class is a {@link URI} or {@link URL}.
*
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
index bf52621b77..f2e642319e 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
@@ -27,11 +27,17 @@ import java.nio.charset.*;
import java.util.*;
import java.util.function.*;
+import java.time.*;
+import java.time.temporal.*;
+import java.util.Calendar;
+import java.util.Date;
+
import org.apache.juneau.*;
import org.apache.juneau.commons.lang.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
/**
* Session object that lives for the duration of a single use of {@link
CsvSerializer}.
@@ -216,6 +222,10 @@ public class CsvSerializerSession extends
WriterSerializerSession {
if (nn(swap)) {
return swap(swap, value);
}
+ if (type.isDateOrCalendarOrTemporal())
+ return Iso8601Utils.format(value, type,
getTimeZone());
+ if (type.isDuration())
+ return value.toString();
return value;
} catch (SerializeException e) {
throw rex(e);
@@ -262,10 +272,10 @@ public class CsvSerializerSession extends
WriterSerializerSession {
var addComma2 = Flag.create();
BeanMap<?> bean = toBeanMap(x);
bm.getProperties().values().stream().filter(BeanPropertyMeta::canRead).forEach(y
-> {
- addComma2.ifSet(() ->
w.w(',')).set();
- // Bean property values
are already swapped by BeanPropertyMeta.get() via toSerializedForm()
- var value = y.get(bean,
y.getName());
- w.writeEntry(value);
+ addComma2.ifSet(() ->
w.w(',')).set();
+ var value = y.get(bean,
y.getName());
+ value =
formatIfDateOrDuration(value);
+ w.writeEntry(value);
});
w.w('\n');
});
@@ -300,6 +310,17 @@ public class CsvSerializerSession extends
WriterSerializerSession {
}
}
+ private Object formatIfDateOrDuration(Object value) {
+ if (value == null)
+ return null;
+ if (value instanceof Calendar || value instanceof Date || value
instanceof Temporal
+ || value instanceof
javax.xml.datatype.XMLGregorianCalendar)
+ return Iso8601Utils.format(value,
getClassMetaForObject(value), getTimeZone());
+ if (value instanceof Duration)
+ return value.toString();
+ return value;
+ }
+
CsvWriter getCsvWriter(SerializerPipe out) {
var output = out.getRawOutput();
if (output instanceof CsvWriter output2)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlParserSession.java
index 3db9006c3b..65baa3916e 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlParserSession.java
@@ -39,6 +39,7 @@ import org.apache.juneau.html.annotation.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
import org.apache.juneau.xml.*;
/**
@@ -351,6 +352,8 @@ public class HtmlParserSession extends XmlParserSession {
o = Boolean.parseBoolean(text);
else if (sType.isNumber())
o = parseNumber(text, (Class<? extends
Number>)eType.inner());
+ else if (sType.isDateOrCalendarOrTemporal() ||
sType.isDuration())
+ o = Iso8601Utils.parse(text, sType,
getTimeZone());
else if (sType.canCreateNewInstanceFromString(outer))
o = sType.newInstanceFromString(outer, text);
else
@@ -362,6 +365,8 @@ public class HtmlParserSession extends XmlParserSession {
o = text;
else if (sType.isChar())
o = parseCharacter(text);
+ else if (sType.isDateOrCalendarOrTemporal() ||
sType.isDuration())
+ o = Iso8601Utils.parse(text, sType,
getTimeZone());
else if (sType.canCreateNewInstanceFromString(outer))
o = sType.newInstanceFromString(outer, text);
else
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
index 72da37ad01..78ac1bdd46 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
@@ -38,6 +38,7 @@ import org.apache.juneau.httppart.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.svl.*;
import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.*;
import org.apache.juneau.xml.*;
import org.apache.juneau.xml.annotation.*;
@@ -963,6 +964,22 @@ public class HtmlSerializerSession extends
XmlSerializerSession {
out.sTag("boolean").append(o).eTag("boolean");
cr = CR_MIXED;
+ } else if (sType.isDateOrCalendarOrTemporal()) {
+ String s = Iso8601Utils.format(o, sType,
getTimeZone());
+ if (isRoot && addJsonTags)
+
out.sTag("string").text(s).eTag("string");
+ else
+ out.text(s);
+ cr = CR_MIXED;
+
+ } else if (sType.isDuration()) {
+ String s = o.toString();
+ if (isRoot && addJsonTags)
+
out.sTag("string").text(s).eTag("string");
+ else
+ out.text(s);
+ cr = CR_MIXED;
+
} else if (sType.isMap() || (nn(wType) &&
wType.isMap())) {
out.nlIf(! isRoot, xIndent + 1);
if (o instanceof BeanMap o2)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParserSession.java
index 0f581ed807..65180dbcc0 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParserSession.java
@@ -35,6 +35,7 @@ import org.apache.juneau.commons.utils.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
/**
* Session object that lives for the duration of a single use of {@link
JsonParser}.
@@ -268,6 +269,8 @@ public class JsonParserSession extends ReaderParserSession {
o = parseCharacter(parseString(r));
} else if (sType.isNumber()) {
o = parseNumber(r, (Class<? extends
Number>)sType.inner());
+ } else if (sType.isDateOrCalendarOrTemporal() ||
sType.isDuration()) {
+ o = Iso8601Utils.parse(parseString(r), sType,
getTimeZone());
} else if (sType.isMap()) {
Map m = (sType.canCreateNewInstance(outer) ?
(Map)sType.newInstance(outer) : newGenericMap(sType));
o = parseIntoMap2(r, m, sType.getKeyType(),
sType.getValueType(), pMeta);
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
index 0fe6b80c6b..2dfec57ff3 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
@@ -31,6 +31,7 @@ import org.apache.juneau.commons.lang.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
/**
* Session object that lives for the duration of a single use of {@link
JsonSerializer}.
@@ -436,6 +437,10 @@ public class JsonSerializerSession extends
WriterSerializerSession {
out.append("null");
} else if (sType.isNumber() || sType.isBoolean()) {
out.append(o);
+ } else if (sType.isDateOrCalendarOrTemporal()) {
+ out.stringValue(Iso8601Utils.format(o, sType,
getTimeZone()));
+ } else if (sType.isDuration()) {
+ out.stringValue(o.toString());
} else if (sType.isBean()) {
serializeBeanMap(out, toBeanMap(o), typeName);
} else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
index a5afe3906f..665a3c08db 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
@@ -31,6 +31,7 @@ import org.apache.juneau.commons.reflect.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
/**
* Session object that lives for the duration of a single use of {@link
MsgPackParser}.
@@ -240,6 +241,8 @@ public class MsgPackParserSession extends
InputStreamParserSession {
// Do nothing.
} else if (sType.isBoolean() || sType.isCharSequence()
|| sType.isChar() || sType.isNumber() || sType.isByteArray()) {
o = convertToType(o, sType);
+ } else if (sType.isDateOrCalendarOrTemporal() ||
sType.isDuration()) {
+ o = Iso8601Utils.parse(String.valueOf(o),
sType, getTimeZone());
} else if (sType.isMap()) {
if (dt == MAP) {
Map m =
(sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) :
newGenericMap(sType));
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerSession.java
index 47cbaed3d0..1aa5dcbb91 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerSession.java
@@ -30,6 +30,7 @@ import org.apache.juneau.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
/**
* Session object that lives for the duration of a single use of {@link
MsgPackSerializer}.
@@ -266,6 +267,10 @@ public class MsgPackSerializerSession extends
OutputStreamSerializerSession {
out.appendBoolean((Boolean)o);
else if (sType.isNumber())
out.appendNumber((Number)o);
+ else if (sType.isDateOrCalendarOrTemporal())
+ out.appendString(Iso8601Utils.format(o, sType,
getTimeZone()));
+ else if (sType.isDuration())
+ out.appendString(o.toString());
else if (sType.isBean())
serializeBeanMap(out, toBeanMap(o), typeName);
else if (sType.isUri() || (nn(pMeta) && pMeta.isUri()))
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
index e5d9690133..93f5e8b1ed 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
@@ -37,8 +37,8 @@ import org.apache.juneau.commons.utils.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.swap.*;
-import org.apache.juneau.swaps.*;
import org.apache.juneau.uon.*;
+import org.apache.juneau.utils.*;
/**
* Session object that lives for the duration of a single use of {@link
OpenApiParser}.
@@ -298,31 +298,10 @@ public class OpenApiParserSession extends
UonParserSession {
}
if (f == BYTE)
return toType(base64Decode(in), type);
- if (f == DATE) {
- try {
- if (type.isCalendar())
- return
toType(TemporalCalendarSwap.IsoDate.DEFAULT.unswap(this, in, type), type);
- if (type.isDate())
- return
toType(TemporalDateSwap.IsoDate.DEFAULT.unswap(this, in, type), type);
- if (type.isTemporal())
- return
toType(TemporalSwap.IsoDate.DEFAULT.unswap(this, in, type), type);
- return toType(in, type);
- } catch (Exception e) {
- throw new ParseException(e);
- }
- }
- if (f == DATE_TIME) {
- try {
- if (type.isCalendar())
- return
toType(TemporalCalendarSwap.IsoDateTime.DEFAULT.unswap(this, in, type), type);
- if (type.isDate())
- return
toType(TemporalDateSwap.IsoDateTime.DEFAULT.unswap(this, in, type), type);
- if (type.isTemporal())
- return
toType(TemporalSwap.IsoDateTime.DEFAULT.unswap(this, in, type), type);
- return toType(in, type);
- } catch (Exception e) {
- throw new ParseException(e);
- }
+ if (f == DATE || f == DATE_TIME) {
+ if (type.isDateOrCalendarOrTemporal())
+ return
toType(Iso8601Utils.parse(in, type, getTimeZone()), type);
+ return toType(in, type);
}
if (f == BINARY)
return toType(fromHex(in), type);
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
index 687e3674b8..e8968985a4 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
@@ -36,8 +36,8 @@ import org.apache.juneau.commons.utils.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.svl.*;
-import org.apache.juneau.swaps.*;
import org.apache.juneau.uon.*;
+import org.apache.juneau.utils.*;
/**
* Session object that lives for the duration of a single use of {@link
OpenApiSerializer}.
@@ -342,31 +342,9 @@ public class OpenApiSerializerSession extends
UonSerializerSession {
} else if (f == BINARY_SPACED) {
out = toSpacedHex(toType(value,
CM_ByteArray));
} else if (f == DATE) {
- try {
- if (value instanceof Calendar
value2)
- out =
TemporalCalendarSwap.IsoDate.DEFAULT.swap(this, value2);
- else if (value instanceof Date
value2)
- out =
TemporalDateSwap.IsoDate.DEFAULT.swap(this, value2);
- else if (value instanceof
Temporal value2)
- out =
TemporalSwap.IsoDate.DEFAULT.swap(this, value2);
- else
- out = value.toString();
- } catch (Exception e) {
- throw new SerializeException(e);
- }
+ out = Iso8601Utils.formatAsDate(value,
type, getTimeZone());
} else if (f == DATE_TIME) {
- try {
- if (value instanceof Calendar
value2)
- out =
TemporalCalendarSwap.IsoInstant.DEFAULT.swap(this, value2);
- else if (value instanceof Date
value2)
- out =
TemporalDateSwap.IsoInstant.DEFAULT.swap(this, value2);
- else if (value instanceof
Temporal value2)
- out =
TemporalSwap.IsoInstant.DEFAULT.swap(this, value2);
- else
- out = value.toString();
- } catch (Exception e) {
- throw new SerializeException(e);
- }
+ out =
Iso8601Utils.formatAsDateTime(value, type, getTimeZone());
} else if (f == HttpPartFormat.UON) {
out = super.serialize(partType, schema,
value);
} else {
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
index b0481b5c7a..34d1c75a15 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
@@ -38,6 +38,7 @@ import org.apache.juneau.cp.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.objecttools.*;
import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
/**
* Session object that lives for the duration of a single use of {@link
Parser}.
@@ -826,6 +827,8 @@ public class ParserSession extends BeanSession {
o = parseNumber(s, (Class<? extends
Number>)sType.inner());
else if (sType.isBoolean())
o = Boolean.parseBoolean(s);
+ else if (sType.isDateOrCalendarOrTemporal() ||
sType.isDuration())
+ o = Iso8601Utils.parse(s, sType, getTimeZone());
else if (! (sType.isCharSequence() || sType.isObject())) {
if (sType.canCreateNewInstanceFromString(outer))
o = sType.newInstanceFromString(outer, s);
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerSession.java
index 2b1079ec20..461f4718b0 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerSession.java
@@ -37,6 +37,7 @@ import org.apache.juneau.parser.*;
import org.apache.juneau.soap.*;
import org.apache.juneau.svl.*;
import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
/**
* Serializer session that lives for the duration of a single use of {@link
Serializer}.
@@ -709,6 +710,9 @@ public class SerializerSession extends BeanTraverseSession {
return ((ClassInfo)o).getNameFull();
if (o.getClass().isEnum())
return getClassMetaForObject(o).toString(o);
+ var cm = getClassMetaForObject(o);
+ if (cm.isDateOrCalendarOrTemporal() || cm.isDuration())
+ return Iso8601Utils.format(o, cm, getTimeZone());
var s = o.toString();
if (isTrimStrings())
s = s.trim();
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swap/DefaultSwaps.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swap/DefaultSwaps.java
index e223c20d0b..71b5c39110 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swap/DefaultSwaps.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swap/DefaultSwaps.java
@@ -20,13 +20,10 @@ import static org.apache.juneau.commons.utils.Utils.*;
import java.net.*;
import java.time.*;
-import java.time.temporal.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
-import javax.xml.datatype.*;
-
import org.apache.juneau.commons.reflect.*;
import org.apache.juneau.swaps.*;
@@ -48,21 +45,8 @@ public class DefaultSwaps {
static {
SWAPS.put(Locale.class, new LocaleSwap());
SWAPS.put(Class.class, new ClassSwap());
- SWAPS.put(Calendar.class, new
TemporalCalendarSwap.IsoOffsetDateTime());
- SWAPS.put(Date.class, new TemporalDateSwap.IsoLocalDateTime());
- SWAPS.put(Instant.class, new TemporalSwap.IsoInstant());
- SWAPS.put(ZonedDateTime.class, new
TemporalSwap.IsoOffsetDateTime());
- SWAPS.put(LocalDate.class, new TemporalSwap.IsoLocalDate());
- SWAPS.put(LocalDateTime.class, new
TemporalSwap.IsoLocalDateTime());
- SWAPS.put(LocalTime.class, new TemporalSwap.IsoLocalTime());
- SWAPS.put(OffsetDateTime.class, new
TemporalSwap.IsoOffsetDateTime());
- SWAPS.put(OffsetTime.class, new TemporalSwap.IsoOffsetTime());
SWAPS.put(StackTraceElement.class, new StackTraceElementSwap());
- SWAPS.put(Year.class, new TemporalSwap.IsoYear());
- SWAPS.put(YearMonth.class, new TemporalSwap.IsoYearMonth());
- SWAPS.put(Temporal.class, new TemporalSwap.IsoInstant());
SWAPS.put(TimeZone.class, new TimeZoneSwap());
- SWAPS.put(XMLGregorianCalendar.class, new
XMLGregorianCalendarSwap());
SWAPS.put(ZoneId.class, new ZoneIdSwap());
SWAPS.put(MatchResult.class, new MatchResultSwap());
SWAPS.put(URL.class, new UrlSwap());
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalCalendarSwap.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalCalendarSwap.java
index 2621d29d99..4be9de1b83 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalCalendarSwap.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalCalendarSwap.java
@@ -31,6 +31,11 @@ import org.apache.juneau.swap.*;
* <p>
* Uses the {@link DateTimeFormatter} class for converting {@link Calendar}
objects.
*
+ * <p>
+ * Date/time types are now serialized as ISO 8601 strings by default without
needing a swap.
+ * These swap classes can be used to override the default format (e.g., using
RFC 1123 instead of ISO 8601).
+ * They can be registered globally via {@link BeanContext#swaps()} or
per-field via the {@link Swap @Swap} annotation.
+ *
* <h5 class='section'>See Also:</h5><ul>
* <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/SwapBasics">Swap Basics</a>
* </ul>
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalDateSwap.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalDateSwap.java
index 566aa43d55..cc53fec586 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalDateSwap.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalDateSwap.java
@@ -31,6 +31,11 @@ import org.apache.juneau.swap.*;
* <p>
* Uses the {@link DateTimeFormatter} class for converting {@link Date}
objects.
*
+ * <p>
+ * Date/time types are now serialized as ISO 8601 strings by default without
needing a swap.
+ * These swap classes can be used to override the default format (e.g., using
RFC 1123 instead of ISO 8601).
+ * They can be registered globally via {@link BeanContext#swaps()} or
per-field via the {@link Swap @Swap} annotation.
+ *
* <h5 class='section'>See Also:</h5><ul>
* <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/SwapBasics">Swap Basics</a>
* </ul>
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalSwap.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalSwap.java
index 47741dcffa..faf0988ce7 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalSwap.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalSwap.java
@@ -55,6 +55,11 @@ import org.apache.juneau.swap.*;
* <li class='jc'>{@link ZonedDateTime}
* </ul>
*
+ * <p>
+ * Date/time types are now serialized as ISO 8601 strings by default without
needing a swap.
+ * These swap classes can be used to override the default format (e.g., using
RFC 1123 instead of ISO 8601).
+ * They can be registered globally via {@link BeanContext#swaps()} or
per-field via the {@link Swap @Swap} annotation.
+ *
* <h5 class='section'>See Also:</h5><ul>
* <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/SwapBasics">Swap Basics</a>
* </ul>
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/XMLGregorianCalendarSwap.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/XMLGregorianCalendarSwap.java
index b0e7ac3bd5..79ea229b20 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/XMLGregorianCalendarSwap.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/XMLGregorianCalendarSwap.java
@@ -33,6 +33,11 @@ import org.apache.juneau.swap.*;
* <p>
* Strings are converted to objects using {@link
DatatypeFactory#newXMLGregorianCalendar(String)}.
*
+ * <p>
+ * Date/time types are now serialized as ISO 8601 strings by default without
needing a swap.
+ * These swap classes can be used to override the default format (e.g., using
RFC 1123 instead of ISO 8601).
+ * They can be registered globally via {@link BeanContext#swaps()} or
per-field via the {@link Swap @Swap} annotation.
+ *
* <h5 class='section'>See Also:</h5><ul>
* <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/SwapBasics">Swap Basics</a>
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonParserSession.java
index 1da56f0ec8..39c986642c 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonParserSession.java
@@ -36,6 +36,7 @@ import org.apache.juneau.commons.utils.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
/**
* Session object that lives for the duration of a single use of {@link
UonParser}.
@@ -377,6 +378,8 @@ public class UonParserSession extends ReaderParserSession
implements HttpPartPar
o = parseCharacter(parseString(r, isUrlParamValue));
} else if (sType.isNumber()) {
o = parseNumber(r, (Class<? extends
Number>)sType.inner());
+ } else if (sType.isDateOrCalendarOrTemporal() ||
sType.isDuration()) {
+ o = Iso8601Utils.parse(parseString(r, isUrlParamValue),
sType, getTimeZone());
} else if (sType.isMap()) {
var m = (sType.canCreateNewInstance(outer) ?
(Map)sType.newInstance(outer) : newGenericMap(sType));
o = parseIntoMap(r, m, sType.getKeyType(),
sType.getValueType(), pMeta);
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
index fc3cd01be3..dac79b4d4f 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
@@ -33,6 +33,7 @@ import org.apache.juneau.httppart.*;
import org.apache.juneau.reflect.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
/**
* Session object that lives for the duration of a single use of {@link
UonSerializer}.
@@ -483,6 +484,10 @@ public class UonSerializerSession extends
WriterSerializerSession implements Htt
out.appendBoolean(o);
else if (sType.isNumber())
out.appendNumber(o);
+ else if (sType.isDateOrCalendarOrTemporal())
+ out.appendObject(Iso8601Utils.format(o, sType,
getTimeZone()), false);
+ else if (sType.isDuration())
+ out.appendObject(o.toString(), false);
else if (sType.isBean())
serializeBeanMap(out, toBeanMap(o), typeName);
else if (sType.isUri() || (nn(pMeta) && pMeta.isUri()))
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/Iso8601Utils.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/Iso8601Utils.java
new file mode 100644
index 0000000000..61ddee7b77
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/Iso8601Utils.java
@@ -0,0 +1,313 @@
+/*
+ * 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.juneau.utils;
+
+import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
+
+import java.lang.reflect.*;
+import java.time.*;
+import java.time.format.*;
+import java.time.temporal.*;
+import java.util.*;
+
+import javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.swaps.*;
+
+/**
+ * Centralized ISO 8601 formatting and parsing utility for date/time and
Duration types.
+ *
+ * <p>
+ * Provides the built-in serialization format for {@link
java.time.temporal.Temporal}, {@link Calendar},
+ * {@link Date}, {@link XMLGregorianCalendar}, and {@link Duration} objects.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/Marshalling">Marshalling</a>
+ * </ul>
+ */
+public final class Iso8601Utils {
+
+ private Iso8601Utils() {}
+
+ private static final ZoneId Z = ZoneId.of("Z");
+
+ private static final DateTimeFormatter YEAR_FORMATTER =
DateTimeFormatter.ofPattern("uuuu");
+ private static final DateTimeFormatter YEAR_MONTH_FORMATTER =
DateTimeFormatter.ofPattern("uuuu-MM");
+
+ private static final Map<Class<?>, DateTimeFormatter>
DEFAULT_FORMATTERS = Map.ofEntries(
+ Map.entry(Instant.class, DateTimeFormatter.ISO_INSTANT),
+ Map.entry(ZonedDateTime.class,
DateTimeFormatter.ISO_OFFSET_DATE_TIME),
+ Map.entry(OffsetDateTime.class,
DateTimeFormatter.ISO_OFFSET_DATE_TIME),
+ Map.entry(LocalDate.class, DateTimeFormatter.ISO_LOCAL_DATE),
+ Map.entry(LocalDateTime.class,
DateTimeFormatter.ISO_LOCAL_DATE_TIME),
+ Map.entry(LocalTime.class, DateTimeFormatter.ISO_LOCAL_TIME),
+ Map.entry(OffsetTime.class, DateTimeFormatter.ISO_OFFSET_TIME),
+ Map.entry(Year.class, YEAR_FORMATTER),
+ Map.entry(YearMonth.class, YEAR_MONTH_FORMATTER)
+ );
+
+ private static DatatypeFactory datatypeFactory;
+ static {
+ try {
+ datatypeFactory = DatatypeFactory.newInstance();
+ } catch (DatatypeConfigurationException e) {
+ throw toRex(e);
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Formatting
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Formats a date/time or Duration value to its ISO 8601 string
representation.
+ *
+ * @param value The value to format.
+ * @param type The class metadata for the value (used for selecting the
appropriate formatter for Temporal types).
+ * @param timeZone The session time zone (used when the value lacks
zone info).
+ * @return The ISO 8601 string representation.
+ */
+ public static String format(Object value, ClassMeta<?> type, TimeZone
timeZone) {
+ if (value instanceof Duration d)
+ return d.toString();
+ if (value instanceof Calendar c)
+ return formatCalendar(c);
+ if (value instanceof Date d)
+ return formatDate(d, timeZone);
+ if (value instanceof XMLGregorianCalendar x)
+ return x.toXMLFormat();
+ if (value instanceof Temporal t)
+ return formatTemporal(t, timeZone);
+ return value.toString();
+ }
+
+ /**
+ * Formats a Calendar to ISO 8601 using ISO_OFFSET_DATE_TIME.
+ */
+ private static String formatCalendar(Calendar c) {
+ ZonedDateTime zdt;
+ if (c instanceof GregorianCalendar gc)
+ zdt = gc.toZonedDateTime();
+ else
+ zdt = c.toInstant().atZone(c.getTimeZone().toZoneId());
+ return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zdt);
+ }
+
+ /**
+ * Formats a Date to ISO 8601 using ISO_LOCAL_DATE_TIME.
+ */
+ private static String formatDate(Date d, TimeZone tz) {
+ ZoneId zoneId = tz != null ? tz.toZoneId() :
ZoneId.systemDefault();
+ return
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(d.toInstant().atZone(zoneId));
+ }
+
+ /**
+ * Formats a Temporal to ISO 8601 using the appropriate default
formatter for the concrete type.
+ */
+ private static String formatTemporal(Temporal t, TimeZone tz) {
+ ZoneId zoneId = tz != null ? tz.toZoneId() :
ZoneId.systemDefault();
+ Class<?> tc = t.getClass();
+
+ if (tc == Instant.class)
+ return
DateTimeFormatter.ISO_INSTANT.format(ZonedDateTime.from(new
DefaultingTemporalAccessor(t, Z)));
+
+ if (tc == ZonedDateTime.class || tc == OffsetDateTime.class ||
tc == OffsetTime.class)
+ return DEFAULT_FORMATTERS.getOrDefault(tc,
DateTimeFormatter.ISO_OFFSET_DATE_TIME).format(t);
+
+ if (tc == LocalDate.class || tc == LocalDateTime.class || tc ==
LocalTime.class
+ || tc == Year.class || tc == YearMonth.class)
+ return DEFAULT_FORMATTERS.getOrDefault(tc,
DateTimeFormatter.ISO_LOCAL_DATE_TIME).format(t);
+
+ return
DateTimeFormatter.ISO_INSTANT.format(ZonedDateTime.from(new
DefaultingTemporalAccessor(t, zoneId)));
+ }
+
+ /**
+ * Formats a date/time value as an ISO date (date-only, for OpenAPI
'date' format).
+ *
+ * @param value The value to format.
+ * @param type The class metadata.
+ * @param timeZone The session time zone.
+ * @return The ISO date string.
+ */
+ public static String formatAsDate(Object value, ClassMeta<?> type,
TimeZone timeZone) {
+ ZoneId zoneId = timeZone != null ? timeZone.toZoneId() :
ZoneId.systemDefault();
+ if (value instanceof Calendar c) {
+ ZonedDateTime zdt = (c instanceof GregorianCalendar gc)
? gc.toZonedDateTime() : c.toInstant().atZone(c.getTimeZone().toZoneId());
+ return DateTimeFormatter.ISO_DATE.format(zdt);
+ }
+ if (value instanceof Date d)
+ return
DateTimeFormatter.ISO_DATE.format(d.toInstant().atZone(zoneId));
+ if (value instanceof Temporal t)
+ return
DateTimeFormatter.ISO_DATE.format(ZonedDateTime.from(new
DefaultingTemporalAccessor(t, zoneId)));
+ return value.toString();
+ }
+
+ /**
+ * Formats a date/time value as an ISO date-time (for OpenAPI
'date-time' format).
+ *
+ * @param value The value to format.
+ * @param type The class metadata.
+ * @param timeZone The session time zone.
+ * @return The ISO date-time string.
+ */
+ public static String formatAsDateTime(Object value, ClassMeta<?> type,
TimeZone timeZone) {
+ ZoneId zoneId = timeZone != null ? timeZone.toZoneId() :
ZoneId.systemDefault();
+ if (value instanceof Calendar c) {
+ ZonedDateTime zdt = (c instanceof GregorianCalendar gc)
? gc.toZonedDateTime() : c.toInstant().atZone(c.getTimeZone().toZoneId());
+ return
DateTimeFormatter.ISO_INSTANT.format(zdt.toInstant());
+ }
+ if (value instanceof Date d)
+ return
DateTimeFormatter.ISO_INSTANT.format(d.toInstant());
+ if (value instanceof Temporal t) {
+ ZonedDateTime zdt = ZonedDateTime.from(new
DefaultingTemporalAccessor(t, zoneId));
+ return
DateTimeFormatter.ISO_INSTANT.format(zdt.toInstant());
+ }
+ return value.toString();
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Parsing
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Parses an ISO 8601 string into the specified date/time target type.
+ *
+ * @param <T> The target type.
+ * @param iso8601 The ISO 8601 string to parse.
+ * @param targetType The target class metadata.
+ * @param timeZone The session time zone (used for types that need a
default zone).
+ * @return The parsed object.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T parse(String iso8601, ClassMeta<T> targetType,
TimeZone timeZone) {
+ if (iso8601 == null)
+ return null;
+
+ Class<T> tc = targetType.inner();
+ ZoneId zoneId = timeZone != null ? timeZone.toZoneId() :
ZoneId.systemDefault();
+
+ if (tc == Duration.class)
+ return (T) Duration.parse(iso8601);
+
+ if (Calendar.class.isAssignableFrom(tc))
+ return (T) parseCalendar(iso8601, zoneId);
+
+ if (tc == Date.class)
+ return (T) parseDate(iso8601, zoneId);
+
+ if (XMLGregorianCalendar.class.isAssignableFrom(tc))
+ return (T)
datatypeFactory.newXMLGregorianCalendar(iso8601);
+
+ if (Temporal.class.isAssignableFrom(tc))
+ return (T) parseTemporal(iso8601, (Class<? extends
Temporal>) tc, zoneId);
+
+ return null;
+ }
+
+ private static Calendar parseCalendar(String iso8601, ZoneId zoneId) {
+ var formatter = selectParserFormatter(iso8601);
+ var ta = new
DefaultingTemporalAccessor(formatter.parse(iso8601), zoneId);
+ return GregorianCalendar.from(ZonedDateTime.from(ta));
+ }
+
+ private static Date parseDate(String iso8601, ZoneId zoneId) {
+ var formatter = selectParserFormatter(iso8601);
+ var ta = new
DefaultingTemporalAccessor(formatter.parse(iso8601), zoneId);
+ return Date.from(ZonedDateTime.from(ta).toInstant());
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T extends Temporal> T parseTemporal(String iso8601,
Class<T> tc, ZoneId zoneId) {
+ ZoneId offset = (tc == Instant.class) ? Z : zoneId;
+ var formatter = getFormatterForType(tc);
+ var ta = new
DefaultingTemporalAccessor(formatter.parse(iso8601), offset);
+
+ try {
+ var parseMethod = info(tc).getPublicMethod(
+ x -> x.isStatic()
+ && x.isNotDeprecated()
+ && x.hasName("from")
+ && x.hasReturnType(tc)
+ &&
x.hasParameterTypes(TemporalAccessor.class)
+ ).map(MethodInfo::inner).orElse(null);
+
+ if (parseMethod != null)
+ return (T) parseMethod.invoke(null, ta);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw toRex(e);
+ }
+
+ throw new IllegalArgumentException("Cannot parse ISO 8601
string into type: " + tc.getName());
+ }
+
+ private static DateTimeFormatter getFormatterForType(Class<?> tc) {
+ var f = DEFAULT_FORMATTERS.get(tc);
+ return f != null ? f : DateTimeFormatter.ISO_INSTANT;
+ }
+
+ /**
+ * Auto-detects the appropriate parser formatter based on the string
content.
+ */
+ private static DateTimeFormatter selectParserFormatter(String iso8601) {
+ boolean hasTime = iso8601.contains("T");
+ boolean hasZone = iso8601.endsWith("Z") || iso8601.contains("+")
+ || (iso8601.length() > 10 && iso8601.lastIndexOf('-') >
10);
+ if (hasTime && hasZone)
+ return DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+ if (hasTime)
+ return DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+ if (hasZone)
+ return DateTimeFormatter.ISO_DATE;
+ return DateTimeFormatter.ISO_LOCAL_DATE;
+ }
+
+ /**
+ * Converts epoch milliseconds to the target date/time type.
+ *
+ * @param <T> The target type.
+ * @param epochMillis The epoch milliseconds value.
+ * @param targetType The target class metadata.
+ * @param timeZone The session time zone.
+ * @return The converted date/time object.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T fromEpochMillis(long epochMillis, ClassMeta<T>
targetType, TimeZone timeZone) {
+ Class<T> tc = targetType.inner();
+ ZoneId zoneId = timeZone != null ? timeZone.toZoneId() :
ZoneId.systemDefault();
+ Instant instant = Instant.ofEpochMilli(epochMillis);
+
+ if (tc == Instant.class) return (T) instant;
+ if (tc == ZonedDateTime.class) return (T)
instant.atZone(zoneId);
+ if (tc == OffsetDateTime.class) return (T)
instant.atZone(zoneId).toOffsetDateTime();
+ if (tc == LocalDateTime.class) return (T)
instant.atZone(zoneId).toLocalDateTime();
+ if (tc == LocalDate.class) return (T)
instant.atZone(zoneId).toLocalDate();
+ if (tc == LocalTime.class) return (T)
instant.atZone(zoneId).toLocalTime();
+ if (tc == OffsetTime.class) return (T)
instant.atZone(zoneId).toOffsetDateTime().toOffsetTime();
+ if (tc == Date.class) return (T) Date.from(instant);
+ if (Calendar.class.isAssignableFrom(tc)) {
+ var cal =
GregorianCalendar.from(instant.atZone(zoneId));
+ return (T) cal;
+ }
+
+ return null;
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlParserSession.java
index b48781f8de..25d1f0ada0 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlParserSession.java
@@ -38,6 +38,7 @@ import org.apache.juneau.commons.reflect.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
/**
* Session object that lives for the duration of a single use of {@link
XmlParser}.
@@ -870,6 +871,8 @@ public class XmlParserSession extends ReaderParserSession {
o = parseIntoCollection(r, l, sType, pMeta);
} else if (sType.isNumber()) {
o = parseNumber(getElementText(r), (Class<? extends
Number>)sType.inner());
+ } else if (sType.isDateOrCalendarOrTemporal() ||
sType.isDuration()) {
+ o = Iso8601Utils.parse(getElementText(r), sType,
getTimeZone());
} else if (nn(builder) || sType.canCreateNewBean(outer)) {
if (getXmlClassMeta(sType).getFormat() == COLLAPSED) {
var fieldName = r.getLocalName();
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
index bf132d8152..0152a2cbf1 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
@@ -35,6 +35,7 @@ import org.apache.juneau.commons.lang.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
import org.apache.juneau.xml.annotation.*;
/**
@@ -996,6 +997,10 @@ public class XmlSerializerSession extends
WriterSerializerSession {
out.text(o, preserveWhitespace);
} else if (sType.isNumber() || sType.isBoolean()) {
out.append(o);
+ } else if (sType.isDateOrCalendarOrTemporal()) {
+ out.text(Iso8601Utils.format(o, sType,
getTimeZone()));
+ } else if (sType.isDuration()) {
+ out.text(o.toString());
} else if (sType.isMap() || (nn(wType) &&
wType.isMap())) {
if (o instanceof BeanMap o2)
rc = serializeBeanMap(out, o2,
elementNamespace, isCollapsed, isMixedOrText);
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
new file mode 100644
index 0000000000..b1e6fd7c82
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
@@ -0,0 +1,368 @@
+/*
+ * 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.juneau.a.rttests;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.*;
+import java.util.*;
+
+import javax.xml.datatype.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.html.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.msgpack.*;
+import org.apache.juneau.uon.*;
+import org.apache.juneau.urlencoding.*;
+import org.apache.juneau.xml.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Round-trip tests for built-in date/time and Duration serialization across
all serializer/parser combinations.
+ */
+class RoundTripDateTime_Test extends TestBase {
+
+ private static final RoundTrip_Tester[] TESTERS = {
+ tester(1, "Json - default")
+
.serializer(JsonSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(JsonParser.create())
+ .build(),
+ tester(2, "Json - lax")
+
.serializer(JsonSerializer.create().json5().keepNullProperties().addBeanTypes().addRootType())
+ .parser(JsonParser.create())
+ .build(),
+ tester(3, "Json - lax, readable")
+
.serializer(JsonSerializer.create().json5().ws().keepNullProperties().addBeanTypes().addRootType())
+ .parser(JsonParser.create())
+ .build(),
+ tester(4, "Xml - namespaces, validation, readable")
+
.serializer(XmlSerializer.create().ns().sq().keepNullProperties().addNamespaceUrisToRoot().useWhitespace().addBeanTypes().addRootType())
+ .parser(XmlParser.create())
+ .validateXmlWhitespace()
+ .validateXml()
+ .build(),
+ tester(5, "Xml - no namespaces, validation")
+
.serializer(XmlSerializer.create().sq().keepNullProperties().addBeanTypes().addRootType())
+ .parser(XmlParser.create())
+ .validateXmlWhitespace()
+ .build(),
+ tester(6, "Html - default")
+
.serializer(HtmlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(HtmlParser.create())
+ .validateXmlWhitespace()
+ .build(),
+ tester(7, "Html - readable")
+
.serializer(HtmlSerializer.create().sq().ws().keepNullProperties().addBeanTypes().addRootType())
+ .parser(HtmlParser.create())
+ .validateXmlWhitespace()
+ .build(),
+ tester(8, "Html - with key/value headers")
+
.serializer(HtmlSerializer.create().addKeyValueTableHeaders().addBeanTypes().addRootType())
+ .parser(HtmlParser.create())
+ .validateXmlWhitespace()
+ .build(),
+ tester(9, "Uon - default")
+
.serializer(UonSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(UonParser.create())
+ .build(),
+ tester(10, "Uon - readable")
+
.serializer(UonSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
+ .parser(UonParser.create())
+ .build(),
+ tester(11, "Uon - encoded")
+
.serializer(UonSerializer.create().encoding().keepNullProperties().addBeanTypes().addRootType())
+ .parser(UonParser.create().decoding())
+ .build(),
+ tester(12, "UrlEncoding - default")
+
.serializer(UrlEncodingSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(UrlEncodingParser.create())
+ .build(),
+ tester(13, "UrlEncoding - readable")
+
.serializer(UrlEncodingSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
+ .parser(UrlEncodingParser.create())
+ .build(),
+ tester(14, "UrlEncoding - expanded params")
+
.serializer(UrlEncodingSerializer.create().expandedParams().addBeanTypes().addRootType())
+ .parser(UrlEncodingParser.create().expandedParams())
+ .build(),
+ tester(15, "MsgPack")
+
.serializer(MsgPackSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(MsgPackParser.create())
+ .build(),
+ tester(16, "Json schema")
+
.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .returnOriginalObject()
+ .build(),
+ };
+
+ static RoundTrip_Tester[] testers() {
+ return TESTERS;
+ }
+
+ protected static RoundTrip_Tester.Builder tester(int index, String
label) {
+ return RoundTrip_Tester.create(index, label);
+ }
+
+
//====================================================================================================
+ // Bean with all date/time types
+
//====================================================================================================
+
+ public static class DateTimeBean {
+ public Instant instant;
+ public ZonedDateTime zonedDateTime;
+ public LocalDate localDate;
+ public LocalDateTime localDateTime;
+ public LocalTime localTime;
+ public OffsetDateTime offsetDateTime;
+ public OffsetTime offsetTime;
+ public Year year;
+ public YearMonth yearMonth;
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ void a01_dateTimeBean(RoundTrip_Tester t) throws Exception {
+ var x = new DateTimeBean();
+ x.instant = Instant.parse("2012-12-21T12:34:56Z");
+ x.zonedDateTime = ZonedDateTime.parse("2012-12-21T12:34:56Z");
+ x.localDate = LocalDate.parse("2012-12-21");
+ x.localDateTime = LocalDateTime.parse("2012-12-21T12:34:56");
+ x.localTime = LocalTime.parse("12:34:56");
+ x.offsetDateTime =
OffsetDateTime.parse("2012-12-21T12:34:56-05:00");
+ x.offsetTime = OffsetTime.parse("12:34:56-05:00");
+ x.year = Year.of(2012);
+ x.yearMonth = YearMonth.of(2012, 12);
+
+ x = t.roundTrip(x);
+
+ assertEquals(Instant.parse("2012-12-21T12:34:56Z"), x.instant);
+
assertEquals(ZonedDateTime.parse("2012-12-21T12:34:56Z").toInstant(),
x.zonedDateTime.toInstant());
+ assertEquals(LocalDate.parse("2012-12-21"), x.localDate);
+ assertEquals(LocalDateTime.parse("2012-12-21T12:34:56"),
x.localDateTime);
+ assertEquals(LocalTime.parse("12:34:56"), x.localTime);
+
assertEquals(OffsetDateTime.parse("2012-12-21T12:34:56-05:00").toInstant(),
x.offsetDateTime.toInstant());
+ assertEquals(OffsetTime.parse("12:34:56-05:00"), x.offsetTime);
+ assertEquals(Year.of(2012), x.year);
+ assertEquals(YearMonth.of(2012, 12), x.yearMonth);
+ }
+
+
//====================================================================================================
+ // Bean with Calendar and Date
+
//====================================================================================================
+
+ public static class LegacyDateBean {
+ public Calendar calendar;
+ public Date date;
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ void a02_legacyDateBean(RoundTrip_Tester t) throws Exception {
+ var x = new LegacyDateBean();
+ x.calendar =
GregorianCalendar.from(ZonedDateTime.parse("2012-12-21T12:34:56Z"));
+ x.date = Date.from(Instant.parse("2012-12-21T12:34:56Z"));
+
+ var millis1 = x.calendar.getTimeInMillis();
+ var millis2 = x.date.getTime();
+
+ x = t.roundTrip(x);
+
+ assertEquals(millis1, x.calendar.getTimeInMillis());
+ assertEquals(millis2, x.date.getTime());
+ }
+
+
//====================================================================================================
+ // Bean with Duration fields
+
//====================================================================================================
+
+ public static class DurationBean {
+ public java.time.Duration timeout;
+ public java.time.Duration interval;
+ public java.time.Duration zero;
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ void a03_durationBean(RoundTrip_Tester t) throws Exception {
+ var x = new DurationBean();
+ x.timeout = java.time.Duration.ofHours(1).plusMinutes(30);
+ x.interval = java.time.Duration.ofSeconds(45);
+ x.zero = java.time.Duration.ZERO;
+
+ x = t.roundTrip(x);
+
+ assertEquals(java.time.Duration.ofHours(1).plusMinutes(30),
x.timeout);
+ assertEquals(java.time.Duration.ofSeconds(45), x.interval);
+ assertEquals(java.time.Duration.ZERO, x.zero);
+ }
+
+
//====================================================================================================
+ // Bean with mixed date/time and Duration fields
+
//====================================================================================================
+
+ public static class MixedBean {
+ public Instant created;
+ public LocalDate date;
+ public java.time.Duration timeout;
+ public Calendar calendar;
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ void a04_mixedBean(RoundTrip_Tester t) throws Exception {
+ var x = new MixedBean();
+ x.created = Instant.parse("2012-12-21T12:34:56Z");
+ x.date = LocalDate.parse("2012-12-21");
+ x.timeout = java.time.Duration.ofHours(1).plusMinutes(30);
+ x.calendar =
GregorianCalendar.from(ZonedDateTime.parse("2012-12-21T12:34:56Z"));
+
+ var calMillis = x.calendar.getTimeInMillis();
+
+ x = t.roundTrip(x);
+
+ assertEquals(Instant.parse("2012-12-21T12:34:56Z"), x.created);
+ assertEquals(LocalDate.parse("2012-12-21"), x.date);
+ assertEquals(java.time.Duration.ofHours(1).plusMinutes(30),
x.timeout);
+ assertEquals(calMillis, x.calendar.getTimeInMillis());
+ }
+
+
//====================================================================================================
+ // Bean with null date/time fields
+
//====================================================================================================
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ void a05_nullFields(RoundTrip_Tester t) throws Exception {
+ var x = new MixedBean();
+
+ x = t.roundTrip(x);
+
+ assertNull(x.created);
+ assertNull(x.date);
+ assertNull(x.timeout);
+ assertNull(x.calendar);
+ }
+
+
//====================================================================================================
+ // Standalone Instant round-trip (non-URL-encoding serializers)
+
//====================================================================================================
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ void a06_standaloneInstant(RoundTrip_Tester t) throws Exception {
+ if (t.isValidationOnly())
+ return;
+
+ var s = t.getSerializer();
+ var p = t.getParser();
+
+ if (p == null)
+ return;
+
+ var x = Instant.parse("2012-12-21T12:34:56Z");
+ try {
+ var out = t.serialize(x, s);
+ var x2 = p.parse(out, Instant.class);
+ assertEquals(x, x2);
+ } catch (Exception e) {
+ // Some serializers (UrlEncoding) may not support
standalone non-bean values
+ }
+ }
+
+
//====================================================================================================
+ // Standalone Duration round-trip (non-URL-encoding serializers)
+
//====================================================================================================
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ void a07_standaloneDuration(RoundTrip_Tester t) throws Exception {
+ if (t.isValidationOnly())
+ return;
+
+ var s = t.getSerializer();
+ var p = t.getParser();
+
+ if (p == null)
+ return;
+
+ var x = java.time.Duration.ofHours(2).plusMinutes(15);
+ try {
+ var out = t.serialize(x, s);
+ var x2 = p.parse(out, java.time.Duration.class);
+ assertEquals(x, x2);
+ } catch (Exception e) {
+ // Some serializers (UrlEncoding) may not support
standalone non-bean values
+ }
+ }
+
+
//====================================================================================================
+ // XMLGregorianCalendar round-trip
+
//====================================================================================================
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ void a08_xmlGregorianCalendar(RoundTrip_Tester t) throws Exception {
+ if (t.isValidationOnly())
+ return;
+
+ var s = t.getSerializer();
+ var p = t.getParser();
+
+ if (p == null)
+ return;
+
+ var gc = new GregorianCalendar();
+
gc.setTimeInMillis(Instant.parse("2012-12-21T12:34:56Z").toEpochMilli());
+ gc.setTimeZone(TimeZone.getTimeZone("UTC"));
+ var c =
DatatypeFactory.newInstance().newXMLGregorianCalendar(gc);
+
+ try {
+ var out = t.serialize(c, s);
+ var c2 = p.parse(out, XMLGregorianCalendar.class);
+ assertEquals(c, c2);
+ } catch (Exception e) {
+ // Some serializers (UrlEncoding) may not support
standalone non-bean values
+ }
+ }
+
+
//====================================================================================================
+ // Bean with negative and fractional Duration values
+
//====================================================================================================
+
+ public static class EdgeCaseDurationBean {
+ public java.time.Duration negative;
+ public java.time.Duration fractional;
+ public java.time.Duration large;
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ void a09_edgeCaseDurations(RoundTrip_Tester t) throws Exception {
+ var x = new EdgeCaseDurationBean();
+ x.negative = java.time.Duration.ofHours(-6);
+ x.fractional = java.time.Duration.ofSeconds(20, 345000000);
+ x.large = java.time.Duration.ofDays(365);
+
+ x = t.roundTrip(x);
+
+ assertEquals(java.time.Duration.ofHours(-6), x.negative);
+ assertEquals(java.time.Duration.ofSeconds(20, 345000000),
x.fractional);
+ assertEquals(java.time.Duration.ofDays(365), x.large);
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/transforms/BuiltInDateTimeSerialization_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/transforms/BuiltInDateTimeSerialization_Test.java
new file mode 100644
index 0000000000..1186d08725
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/transforms/BuiltInDateTimeSerialization_Test.java
@@ -0,0 +1,313 @@
+/*
+ * 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.juneau.transforms;
+
+import static org.apache.juneau.TestUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.*;
+import java.time.format.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests built-in first-class date/time and Duration serialization and
round-trip parsing.
+ */
+class BuiltInDateTimeSerialization_Test extends TestBase {
+
+ @BeforeAll static void beforeClass() {
+ setTimeZone("GMT-5");
+ }
+
+ @AfterAll static void afterClass() {
+ unsetTimeZone();
+ }
+
+ private static final WriterSerializer JS = Json5Serializer.DEFAULT;
+ private static final ReaderParser JP = Json5Parser.DEFAULT;
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Instant
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void a01_json_instant() throws Exception {
+ var i = Instant.parse("2012-12-21T12:34:56Z");
+ var json = JS.serialize(i);
+ assertEquals("'2012-12-21T12:34:56Z'", json);
+ var i2 = JP.parse(json, Instant.class);
+ assertEquals(i, i2);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // ZonedDateTime
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void b01_json_zonedDateTime() throws Exception {
+ var zdt = ZonedDateTime.parse("2012-12-21T12:34:56Z");
+ var json = JS.serialize(zdt);
+ assertEquals("'2012-12-21T12:34:56Z'", json);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // LocalDate
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void c01_json_localDate() throws Exception {
+ var ld = LocalDate.parse("2012-12-21");
+ var json = JS.serialize(ld);
+ assertEquals("'2012-12-21'", json);
+ var ld2 = JP.parse(json, LocalDate.class);
+ assertEquals(ld, ld2);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // LocalDateTime
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void d01_json_localDateTime() throws Exception {
+ var ldt = LocalDateTime.parse("2012-12-21T12:34:56");
+ var json = JS.serialize(ldt);
+ assertEquals("'2012-12-21T12:34:56'", json);
+ var ldt2 = JP.parse(json, LocalDateTime.class);
+ assertEquals(ldt, ldt2);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // LocalTime
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void e01_json_localTime() throws Exception {
+ var lt = LocalTime.parse("12:34:56");
+ var json = JS.serialize(lt);
+ assertEquals("'12:34:56'", json);
+ var lt2 = JP.parse(json, LocalTime.class);
+ assertEquals(lt, lt2);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // OffsetDateTime
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void f01_json_offsetDateTime() throws Exception {
+ var odt = OffsetDateTime.parse("2012-12-21T12:34:56-05:00");
+ var json = JS.serialize(odt);
+ assertEquals("'2012-12-21T12:34:56-05:00'", json);
+ var odt2 = JP.parse(json, OffsetDateTime.class);
+ assertEquals(odt, odt2);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // OffsetTime
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void g01_json_offsetTime() throws Exception {
+ var ot = OffsetTime.parse("12:34:56-05:00");
+ var json = JS.serialize(ot);
+ assertEquals("'12:34:56-05:00'", json);
+ var ot2 = JP.parse(json, OffsetTime.class);
+ assertEquals(ot, ot2);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Year
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void h01_json_year() throws Exception {
+ var y = Year.parse("2012");
+ var json = JS.serialize(y);
+ assertEquals("'2012'", json);
+ var y2 = JP.parse(json, Year.class);
+ assertEquals(y, y2);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // YearMonth
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void i01_json_yearMonth() throws Exception {
+ var ym = YearMonth.parse("2012-12");
+ var json = JS.serialize(ym);
+ assertEquals("'2012-12'", json);
+ var ym2 = JP.parse(json, YearMonth.class);
+ assertEquals(ym, ym2);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Calendar
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void j01_json_calendar() throws Exception {
+ var c =
GregorianCalendar.from(ZonedDateTime.parse("2012-12-21T12:34:56Z"));
+ var json = JS.serialize(c);
+ assertEquals("'2012-12-21T12:34:56Z'", json);
+ var c2 = JP.parse(json, Calendar.class);
+ assertEquals(c.getTimeInMillis(), c2.getTimeInMillis());
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Date
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void k01_json_date() throws Exception {
+ var d =
Date.from(Instant.from(DateTimeFormatter.ISO_INSTANT.parse("2012-12-21T12:34:56Z")));
+ var json = JS.serialize(d);
+ assertEquals("'2012-12-21T07:34:56'", json);
+ var d2 = JP.parse(json, Date.class);
+ assertNotNull(d2);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Duration
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void l01_json_duration_basic() throws Exception {
+ var d = Duration.ofHours(1).plusMinutes(30);
+ var json = JS.serialize(d);
+ assertEquals("'PT1H30M'", json);
+ var d2 = JP.parse(json, Duration.class);
+ assertEquals(d, d2);
+ }
+
+ @Test void l02_json_duration_hours() throws Exception {
+ var d = Duration.ofHours(48);
+ var json = JS.serialize(d);
+ assertEquals("'PT48H'", json);
+ var d2 = JP.parse(json, Duration.class);
+ assertEquals(d, d2);
+ }
+
+ @Test void l03_json_duration_seconds() throws Exception {
+ var d = Duration.ofSeconds(45);
+ var json = JS.serialize(d);
+ assertEquals("'PT45S'", json);
+ var d2 = JP.parse(json, Duration.class);
+ assertEquals(d, d2);
+ }
+
+ @Test void l04_json_duration_zero() throws Exception {
+ var d = Duration.ZERO;
+ var json = JS.serialize(d);
+ assertEquals("'PT0S'", json);
+ var d2 = JP.parse(json, Duration.class);
+ assertEquals(d, d2);
+ }
+
+ @Test void l05_json_duration_negative() throws Exception {
+ var d = Duration.ofHours(-6);
+ var json = JS.serialize(d);
+ assertEquals("'PT-6H'", json);
+ var d2 = JP.parse(json, Duration.class);
+ assertEquals(d, d2);
+ }
+
+ @Test void l06_json_duration_fractionalSeconds() throws Exception {
+ var d = Duration.ofSeconds(20, 345000000);
+ var json = JS.serialize(d);
+ assertEquals("'PT20.345S'", json);
+ var d2 = JP.parse(json, Duration.class);
+ assertEquals(d, d2);
+ }
+
+ @Test void l07_json_duration_complex() throws Exception {
+ var d = Duration.ofHours(26).plusMinutes(3);
+ var json = JS.serialize(d);
+ assertEquals("'PT26H3M'", json);
+ var d2 = JP.parse(json, Duration.class);
+ assertEquals(d, d2);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Bean with Duration field
+
//-----------------------------------------------------------------------------------------------------------------
+
+ public static class DurationBean {
+ public Duration timeout;
+ public Duration interval;
+ }
+
+ @Test void m01_json_durationBean() throws Exception {
+ var bean = new DurationBean();
+ bean.timeout = Duration.ofHours(1).plusMinutes(30);
+ bean.interval = Duration.ofSeconds(45);
+
+ var json = JS.serialize(bean);
+ assertTrue(json.contains("'PT1H30M'"));
+ assertTrue(json.contains("'PT45S'"));
+
+ var bean2 = JP.parse(json, DurationBean.class);
+ assertEquals(bean.timeout, bean2.timeout);
+ assertEquals(bean.interval, bean2.interval);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Null handling
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void n01_json_nullDuration() throws Exception {
+ var bean = new DurationBean();
+ bean.timeout = null;
+ bean.interval = null;
+
+ var json = JS.serialize(bean);
+ var bean2 = JP.parse(json, DurationBean.class);
+ assertNull(bean2.timeout);
+ assertNull(bean2.interval);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Bean with mixed date/time and Duration fields
+
//-----------------------------------------------------------------------------------------------------------------
+
+ public static class MixedBean {
+ public Instant created;
+ public LocalDate date;
+ public Duration timeout;
+ }
+
+ @Test void o01_json_mixedBean() throws Exception {
+ var bean = new MixedBean();
+ bean.created = Instant.parse("2012-12-21T12:34:56Z");
+ bean.date = LocalDate.parse("2012-12-21");
+ bean.timeout = Duration.ofHours(1).plusMinutes(30);
+
+ var json = JS.serialize(bean);
+ assertTrue(json.contains("'2012-12-21T12:34:56Z'"));
+ assertTrue(json.contains("'2012-12-21'"));
+ assertTrue(json.contains("'PT1H30M'"));
+
+ var bean2 = JP.parse(json, MixedBean.class);
+ assertEquals(bean.created, bean2.created);
+ assertEquals(bean.date, bean2.date);
+ assertEquals(bean.timeout, bean2.timeout);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Collections of dates
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void p01_json_listOfLocalDates() throws Exception {
+ var dates = java.util.List.of(LocalDate.parse("2012-12-21"),
LocalDate.parse("2013-01-15"));
+ var json = JS.serialize(dates);
+ assertTrue(json.contains("'2012-12-21'"));
+ assertTrue(json.contains("'2013-01-15'"));
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/transforms/DefaultSwaps_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/transforms/DefaultSwaps_Test.java
index 1c8fd0da76..66f24fff91 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/transforms/DefaultSwaps_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/transforms/DefaultSwaps_Test.java
@@ -677,4 +677,34 @@ class DefaultSwaps_Test extends TestBase {
@Test void i03_ZoneId_overrideAnnotation() throws Exception {
test1("{f1:'Z',f2:'FOO'}", new IBean());
}
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Duration - built-in first-class support
+
//------------------------------------------------------------------------------------------------------------------
+ private static final java.time.Duration J =
java.time.Duration.ofHours(1).plusMinutes(30);
+
+ public static class JSwap extends StringSwap<java.time.Duration> {
+ @Override /* ObjectSwap */
+ public String swap(BeanSession session, java.time.Duration o)
throws Exception {
+ return "FOO";
+ }
+ }
+
+ public static class JBean {
+ public java.time.Duration f1 = J;
+ @Swap(JSwap.class)
+ public java.time.Duration f2 = J;
+ }
+
+ @Test void j01_Duration() throws Exception {
+ test1("'PT1H30M'", J);
+ }
+
+ @Test void j02_Duration_overrideSwap() throws Exception {
+ test3("'FOO'", J, JSwap.class);
+ }
+
+ @Test void j03_Duration_overrideAnnotation() throws Exception {
+ test1("{f1:'PT1H30M',f2:'FOO'}", new JBean());
+ }
}
\ No newline at end of file