Skip to content

Commit 187f9ca

Browse files
committed
CAUSEWAY-3989: [v2] adds test to demo bug
CAUSEWAY-3989: [v2] fixes localdate marshalling CAUSEWAY-3989: [v2] adds approval test to check marshalling of all date/time datatypes CAUSEWAY-3989: [v2] fixes marshalling of all LocalDateTime and LocalTime datatypes CAUSEWAY-3989: [v2] adds unit test for fromYaml for all datatypes CAUSEWAY-3989: [v2] minor refactoring CAUSEWAY-3989: [v2] fixes compile issue CAUSEWAY-3989: [v2] fixes existing unit tests CAUSEWAY-3989: [v2] improves unit test
1 parent feb7dfc commit 187f9ca

10 files changed

Lines changed: 650 additions & 5 deletions

File tree

api/applib/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@
107107
<scope>test</scope>
108108
</dependency>
109109

110+
<dependency>
111+
<groupId>com.approvaltests</groupId>
112+
<artifactId>approvaltests</artifactId>
113+
<scope>test</scope>
114+
</dependency>
115+
<dependency>
116+
<!-- required by com.approvaltests:test (version managed by spring boot) -->
117+
<groupId>com.google.code.gson</groupId>
118+
<artifactId>gson</artifactId>
119+
<scope>test</scope>
120+
</dependency>
121+
110122
</dependencies>
111123

112124
</project>

api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@
1818
*/
1919
package org.apache.causeway.applib.util.schema;
2020

21+
import java.io.IOException;
2122
import java.util.Collections;
2223
import java.util.List;
2324
import java.util.stream.Collectors;
2425

26+
import javax.xml.datatype.DatatypeConstants;
27+
import javax.xml.datatype.DatatypeFactory;
28+
import javax.xml.datatype.XMLGregorianCalendar;
29+
2530
import org.apache.causeway.applib.services.bookmark.Bookmark;
2631
import org.apache.causeway.commons.internal.base._Lazy;
2732
import org.apache.causeway.commons.internal.base._NullSafe;
@@ -44,7 +49,15 @@
4449

4550
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4651
import com.fasterxml.jackson.annotation.JsonTypeInfo;
52+
import com.fasterxml.jackson.core.JsonGenerator;
53+
import com.fasterxml.jackson.core.JsonParser;
54+
import com.fasterxml.jackson.databind.DeserializationContext;
55+
import com.fasterxml.jackson.databind.JsonDeserializer;
56+
import com.fasterxml.jackson.databind.JsonSerializer;
4757
import com.fasterxml.jackson.databind.ObjectMapper;
58+
import com.fasterxml.jackson.databind.SerializerProvider;
59+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
60+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
4861
import com.fasterxml.jackson.databind.jsontype.NamedType;
4962

5063
import lombok.experimental.UtilityClass;
@@ -178,12 +191,180 @@ private abstract class AbstractDtoMixIn {}
178191

179192
// Mix-in to ignore unknown properties for ValueDto
180193
@JsonIgnoreProperties(ignoreUnknown = true)
181-
private abstract class AbstractValueDtoMixIn {}
194+
private abstract class AbstractValueDtoMixIn {
195+
@JsonSerialize(using = LocalDateXmlGregorianCalendarSerializer.class)
196+
abstract XMLGregorianCalendar getLocalDate();
197+
198+
@JsonDeserialize(using = LocalDateXmlGregorianCalendarDeserializer.class)
199+
abstract void setLocalDate(XMLGregorianCalendar localDate);
200+
201+
@JsonSerialize(using = LocalDateTimeXmlGregorianCalendarSerializer.class)
202+
abstract XMLGregorianCalendar getLocalDateTime();
203+
204+
@JsonDeserialize(using = LocalDateTimeXmlGregorianCalendarDeserializer.class)
205+
abstract void setLocalDateTime(XMLGregorianCalendar localDateTime);
206+
207+
@JsonSerialize(using = LocalTimeXmlGregorianCalendarSerializer.class)
208+
abstract XMLGregorianCalendar getLocalTime();
209+
210+
@JsonDeserialize(using = LocalTimeXmlGregorianCalendarDeserializer.class)
211+
abstract void setLocalTime(XMLGregorianCalendar localTime);
212+
}
182213

183214
private void valueDtoSupport(final ObjectMapper mb) {
184215
mb.addMixIn(ValueDto.class, AbstractValueDtoMixIn.class);
185216
}
186217

218+
private static final DatatypeFactory DATATYPE_FACTORY = datatypeFactory();
219+
220+
private static DatatypeFactory datatypeFactory() {
221+
try {
222+
return DatatypeFactory.newInstance();
223+
} catch (Exception ex) {
224+
throw new RuntimeException("Failed to initialize DatatypeFactory", ex);
225+
}
226+
}
227+
228+
private static final class LocalDateXmlGregorianCalendarSerializer
229+
extends JsonSerializer<XMLGregorianCalendar> {
230+
231+
@Override
232+
public void serialize(
233+
final XMLGregorianCalendar value,
234+
final JsonGenerator gen,
235+
final SerializerProvider serializers) throws IOException {
236+
if (value == null) {
237+
gen.writeNull();
238+
return;
239+
}
240+
final XMLGregorianCalendar dateOnly = DATATYPE_FACTORY.newXMLGregorianCalendarDate(
241+
value.getYear(),
242+
value.getMonth(),
243+
value.getDay(),
244+
DatatypeConstants.FIELD_UNDEFINED);
245+
gen.writeString(dateOnly.toXMLFormat());
246+
}
247+
}
248+
249+
private static final class LocalDateXmlGregorianCalendarDeserializer
250+
extends JsonDeserializer<XMLGregorianCalendar> {
251+
252+
@Override
253+
public XMLGregorianCalendar deserialize(
254+
final JsonParser p,
255+
final DeserializationContext ctxt) throws IOException {
256+
final String text = p.getValueAsString();
257+
if (_Strings.isNullOrEmpty(text)) {
258+
return null;
259+
}
260+
final XMLGregorianCalendar parsed = DATATYPE_FACTORY.newXMLGregorianCalendar(text);
261+
return DATATYPE_FACTORY.newXMLGregorianCalendarDate(
262+
parsed.getYear(),
263+
parsed.getMonth(),
264+
parsed.getDay(),
265+
DatatypeConstants.FIELD_UNDEFINED);
266+
}
267+
}
268+
269+
private static final class LocalDateTimeXmlGregorianCalendarSerializer
270+
extends JsonSerializer<XMLGregorianCalendar> {
271+
272+
@Override
273+
public void serialize(
274+
final XMLGregorianCalendar value,
275+
final JsonGenerator gen,
276+
final SerializerProvider serializers) throws IOException {
277+
if (value == null) {
278+
gen.writeNull();
279+
return;
280+
}
281+
final XMLGregorianCalendar localDateTime = DATATYPE_FACTORY.newXMLGregorianCalendar(
282+
value.getYear(),
283+
value.getMonth(),
284+
value.getDay(),
285+
value.getHour(),
286+
value.getMinute(),
287+
value.getSecond(),
288+
millisecondsOf(value),
289+
DatatypeConstants.FIELD_UNDEFINED);
290+
gen.writeString(localDateTime.toXMLFormat());
291+
}
292+
}
293+
294+
private static final class LocalDateTimeXmlGregorianCalendarDeserializer
295+
extends JsonDeserializer<XMLGregorianCalendar> {
296+
297+
@Override
298+
public XMLGregorianCalendar deserialize(
299+
final JsonParser p,
300+
final DeserializationContext ctxt) throws IOException {
301+
final String text = p.getValueAsString();
302+
if (_Strings.isNullOrEmpty(text)) {
303+
return null;
304+
}
305+
final XMLGregorianCalendar parsed = DATATYPE_FACTORY.newXMLGregorianCalendar(text);
306+
return DATATYPE_FACTORY.newXMLGregorianCalendar(
307+
parsed.getYear(),
308+
parsed.getMonth(),
309+
parsed.getDay(),
310+
parsed.getHour(),
311+
parsed.getMinute(),
312+
parsed.getSecond(),
313+
millisecondsOf(parsed),
314+
DatatypeConstants.FIELD_UNDEFINED);
315+
}
316+
}
317+
318+
private static final class LocalTimeXmlGregorianCalendarSerializer
319+
extends JsonSerializer<XMLGregorianCalendar> {
320+
321+
@Override
322+
public void serialize(
323+
final XMLGregorianCalendar value,
324+
final JsonGenerator gen,
325+
final SerializerProvider serializers) throws IOException {
326+
if (value == null) {
327+
gen.writeNull();
328+
return;
329+
}
330+
final XMLGregorianCalendar localTime = DATATYPE_FACTORY.newXMLGregorianCalendarTime(
331+
value.getHour(),
332+
value.getMinute(),
333+
value.getSecond(),
334+
millisecondsOf(value),
335+
DatatypeConstants.FIELD_UNDEFINED);
336+
gen.writeString(localTime.toXMLFormat());
337+
}
338+
}
339+
340+
private static final class LocalTimeXmlGregorianCalendarDeserializer
341+
extends JsonDeserializer<XMLGregorianCalendar> {
342+
343+
@Override
344+
public XMLGregorianCalendar deserialize(
345+
final JsonParser p,
346+
final DeserializationContext ctxt) throws IOException {
347+
final String text = p.getValueAsString();
348+
if (_Strings.isNullOrEmpty(text)) {
349+
return null;
350+
}
351+
final XMLGregorianCalendar parsed = DATATYPE_FACTORY.newXMLGregorianCalendar(text);
352+
return DATATYPE_FACTORY.newXMLGregorianCalendarTime(
353+
parsed.getHour(),
354+
parsed.getMinute(),
355+
parsed.getSecond(),
356+
millisecondsOf(parsed),
357+
DatatypeConstants.FIELD_UNDEFINED);
358+
}
359+
}
360+
361+
private static int millisecondsOf(final XMLGregorianCalendar value) {
362+
final int millis = value.getMillisecond();
363+
return millis == DatatypeConstants.FIELD_UNDEFINED
364+
? DatatypeConstants.FIELD_UNDEFINED
365+
: millis;
366+
}
367+
187368
private void memberDtoSupport(final ObjectMapper mb) {
188369
// add mix-in so MemberDto carries @JsonTypeInfo without modifying source
189370
mb.addMixIn(MemberDto.class, AbstractDtoMixIn.class);
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.causeway.applib.util.schema;
20+
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.util.List;
24+
25+
import javax.xml.datatype.DatatypeConstants;
26+
27+
import org.assertj.core.api.Assertions;
28+
import org.junit.jupiter.api.Test;
29+
30+
import org.springframework.util.StreamUtils;
31+
32+
import org.apache.causeway.commons.io.DataSource;
33+
import org.apache.causeway.schema.cmd.v2.ActionDto;
34+
import org.apache.causeway.schema.cmd.v2.CommandDto;
35+
import org.apache.causeway.schema.cmd.v2.ParamDto;
36+
import org.apache.causeway.schema.common.v2.ValueType;
37+
38+
class CommandDtoUtils_fromYaml_Approval_Test {
39+
40+
@Test
41+
void unmarshals_all_date_time_datatypes_from_approved_toYaml_snapshot() throws IOException {
42+
String yaml = readApprovalSnapshot();
43+
44+
List<CommandDto> commands = CommandDtoUtils.fromYaml(DataSource.ofStringUtf8(yaml));
45+
46+
Assertions.assertThat(commands).singleElement().satisfies(command -> {
47+
Assertions.assertThat(command.getInteractionId()).isEqualTo("approval-datetime-marshalling");
48+
49+
ActionDto action = (ActionDto) command.getMember();
50+
Assertions.assertThat(action.getLogicalMemberIdentifier())
51+
.isEqualTo("demo.Customer#allDateTimeTypes");
52+
53+
List<ParamDto> params = action.getParameters().getParameter();
54+
Assertions.assertThat(params).hasSize(6);
55+
56+
ParamDto localDate = params.get(0);
57+
Assertions.assertThat(localDate.getType()).isEqualTo(ValueType.LOCAL_DATE);
58+
Assertions.assertThat(localDate.getLocalDate().toXMLFormat()).isEqualTo("2026-07-01");
59+
60+
ParamDto localDateTime = params.get(1);
61+
Assertions.assertThat(localDateTime.getType()).isEqualTo(ValueType.LOCAL_DATE_TIME);
62+
Assertions.assertThat(localDateTime.getLocalDateTime().toXMLFormat()).isEqualTo("2026-07-01T10:15:30");
63+
64+
ParamDto localTime = params.get(2);
65+
Assertions.assertThat(localTime.getType()).isEqualTo(ValueType.LOCAL_TIME);
66+
Assertions.assertThat(localTime.getLocalTime().toXMLFormat()).isEqualTo("10:15:30");
67+
68+
ParamDto offsetDateTime = params.get(3);
69+
Assertions.assertThat(offsetDateTime.getType()).isEqualTo(ValueType.OFFSET_DATE_TIME);
70+
Assertions.assertThat(offsetDateTime.getOffsetDateTime().getYear()).isEqualTo(2026);
71+
Assertions.assertThat(offsetDateTime.getOffsetDateTime().getMonth()).isEqualTo(7);
72+
Assertions.assertThat(offsetDateTime.getOffsetDateTime().getDay()).isEqualTo(1);
73+
Assertions.assertThat(offsetDateTime.getOffsetDateTime().getHour()).isEqualTo(8);
74+
Assertions.assertThat(offsetDateTime.getOffsetDateTime().getMinute()).isEqualTo(15);
75+
Assertions.assertThat(offsetDateTime.getOffsetDateTime().getSecond()).isEqualTo(30);
76+
Assertions.assertThat(offsetDateTime.getOffsetDateTime().getTimezone()).isEqualTo(0);
77+
78+
ParamDto offsetTime = params.get(4);
79+
Assertions.assertThat(offsetTime.getType()).isEqualTo(ValueType.OFFSET_TIME);
80+
Assertions.assertThat(offsetTime.getOffsetTime()).isNotNull();
81+
Assertions.assertThat(offsetTime.getOffsetTime().getHour()).isEqualTo(8);
82+
Assertions.assertThat(offsetTime.getOffsetTime().getMinute()).isEqualTo(15);
83+
Assertions.assertThat(offsetTime.getOffsetTime().getSecond()).isEqualTo(30);
84+
Assertions.assertThat(offsetTime.getOffsetTime().getTimezone()).isEqualTo(0);
85+
86+
ParamDto zonedDateTime = params.get(5);
87+
Assertions.assertThat(zonedDateTime.getType()).isEqualTo(ValueType.ZONED_DATE_TIME);
88+
Assertions.assertThat(zonedDateTime.getZonedDateTime().getYear()).isEqualTo(2026);
89+
Assertions.assertThat(zonedDateTime.getZonedDateTime().getMonth()).isEqualTo(7);
90+
Assertions.assertThat(zonedDateTime.getZonedDateTime().getDay()).isEqualTo(1);
91+
Assertions.assertThat(zonedDateTime.getZonedDateTime().getHour()).isEqualTo(8);
92+
Assertions.assertThat(zonedDateTime.getZonedDateTime().getMinute()).isEqualTo(15);
93+
Assertions.assertThat(zonedDateTime.getZonedDateTime().getSecond()).isEqualTo(30);
94+
Assertions.assertThat(zonedDateTime.getZonedDateTime().getTimezone())
95+
.isEqualTo(0);
96+
});
97+
}
98+
99+
private String readApprovalSnapshot() throws IOException {
100+
String path = CommandDtoUtils_toYaml_Approval_Test.class.getSimpleName() + ".marshals_all_date_time_datatypes.approved.txt";
101+
InputStream stream = CommandDtoUtils_toYaml_Approval_Test.class.getResourceAsStream(path);
102+
return StreamUtils.copyToString(stream, java.nio.charset.StandardCharsets.UTF_8);
103+
}
104+
}
105+

api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@
7373
type: "collection"
7474
"null": false
7575
name: "Lease Item Types"
76-
- localDate: "2026-06-30T22:00:00.000+00:00"
76+
- localDate: "2026-06-30"
7777
type: "localDate"
7878
name: "Invoice Due Date"
79-
- localDate: "2026-06-30T22:00:00.000+00:00"
79+
- localDate: "2026-06-30"
8080
type: "localDate"
8181
name: "Start Due Date"
82-
- localDate: "2026-07-01T22:00:00.000+00:00"
82+
- localDate: "2026-07-01"
8383
type: "localDate"
8484
name: "Next Due Date"
8585
- type: "string"

api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
member: !<ACT>
2323
parameters:
2424
parameter:
25-
- localDate: "2026-04-20T22:00:00.000+00:00"
25+
- localDate: "2026-04-20"
2626
type: "localDate"
2727
name: "Invoice Date"
2828
- boolean: false

api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public void scalarValues() throws IOException {
6868
Assertions.assertThat(invoiceDate.getName()).isEqualTo("Invoice Date");
6969
Assertions.assertThat(invoiceDate.getType()).isEqualTo(ValueType.LOCAL_DATE);
7070
Assertions.assertThat(invoiceDate.getLocalDate()).isNotNull();
71+
Assertions.assertThat(invoiceDate.getLocalDate().toXMLFormat()).isEqualTo("2026-04-20");
7172

7273
ParamDto allowFuture = scalarParams.get(1);
7374
Assertions.assertThat(allowFuture.getName()).isEqualTo("Allow Invoice Date In Future");
@@ -113,6 +114,21 @@ public void collectionValues() throws IOException {
113114
Assertions.assertThat(nullableTagName.getType()).isEqualTo(ValueType.STRING);
114115
Assertions.assertThat(nullableTagName.isNull()).isTrue();
115116

117+
ParamDto invoiceDueDate = params.get(2);
118+
Assertions.assertThat(invoiceDueDate.getName()).isEqualTo("Invoice Due Date");
119+
Assertions.assertThat(invoiceDueDate.getType()).isEqualTo(ValueType.LOCAL_DATE);
120+
Assertions.assertThat(invoiceDueDate.getLocalDate().toXMLFormat()).isEqualTo("2026-06-30");
121+
122+
ParamDto startDueDate = params.get(3);
123+
Assertions.assertThat(startDueDate.getName()).isEqualTo("Start Due Date");
124+
Assertions.assertThat(startDueDate.getType()).isEqualTo(ValueType.LOCAL_DATE);
125+
Assertions.assertThat(startDueDate.getLocalDate().toXMLFormat()).isEqualTo("2026-06-30");
126+
127+
ParamDto nextDueDate = params.get(4);
128+
Assertions.assertThat(nextDueDate.getName()).isEqualTo("Next Due Date");
129+
Assertions.assertThat(nextDueDate.getType()).isEqualTo(ValueType.LOCAL_DATE);
130+
Assertions.assertThat(nextDueDate.getLocalDate().toXMLFormat()).isEqualTo("2026-07-01");
131+
116132
ParamDto newTagName = params.get(6);
117133
Assertions.assertThat(newTagName.getName()).isEqualTo("New Tag Name");
118134
Assertions.assertThat(newTagName.getType()).isEqualTo(ValueType.STRING);

0 commit comments

Comments
 (0)