Skip to content

Commit 13bdd47

Browse files
authored
Merge pull request #3518 from apache/CAUSEWAY-3989
Causeway 3989
2 parents 1f4cd23 + 8a7673a commit 13bdd47

14 files changed

Lines changed: 319 additions & 32 deletions

File tree

api/applib/src/main/java/module-info.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,6 @@
124124
exports org.apache.causeway.applib.value;
125125
exports org.apache.causeway.applib.value.semantics;
126126

127-
requires com.fasterxml.jackson.core;
128-
requires com.fasterxml.jackson.databind;
129127
requires transitive jakarta.activation;
130128
requires transitive java.annotation;
131129
requires transitive java.desktop;
@@ -146,7 +144,7 @@
146144
requires transitive spring.core;
147145
requires spring.tx;
148146
requires org.apache.logging.log4j.core;
149-
requires com.fasterxml.jackson.annotation;
147+
requires com.fasterxml.jackson.dataformat.yaml;
150148

151149
// JAXB viewmodels
152150
opens org.apache.causeway.applib.annotation;

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

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
package org.apache.causeway.applib.util.schema;
2020

2121
import java.io.IOException;
22+
import java.util.ArrayList;
2223
import java.util.Collections;
2324
import java.util.List;
25+
import java.util.Optional;
2426
import java.util.stream.Collectors;
2527

2628
import javax.xml.datatype.DatatypeConstants;
@@ -37,6 +39,7 @@
3739
import org.apache.causeway.commons.io.JsonUtils;
3840
import org.apache.causeway.commons.io.JsonUtils.JacksonCustomizer;
3941
import org.apache.causeway.commons.io.YamlUtils;
42+
import org.apache.causeway.commons.functional.Try;
4043
import org.apache.causeway.schema.cmd.v2.ActionDto;
4144
import org.apache.causeway.schema.cmd.v2.CommandDto;
4245
import org.apache.causeway.schema.cmd.v2.MapDto;
@@ -54,11 +57,13 @@
5457
import com.fasterxml.jackson.databind.DeserializationContext;
5558
import com.fasterxml.jackson.databind.JsonDeserializer;
5659
import com.fasterxml.jackson.databind.JsonSerializer;
60+
import com.fasterxml.jackson.databind.MappingIterator;
5761
import com.fasterxml.jackson.databind.ObjectMapper;
5862
import com.fasterxml.jackson.databind.SerializerProvider;
5963
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
6064
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
6165
import com.fasterxml.jackson.databind.jsontype.NamedType;
66+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
6267

6368
import lombok.experimental.UtilityClass;
6469

@@ -150,7 +155,7 @@ private MapDto userDataFor(final CommandDto commandDto) {
150155
// -- YAML SUPPORT
151156

152157
public String toYaml(final Iterable<CommandDto> commandDtos) {
153-
final JsonUtils.JacksonCustomizer customizer = new JacksonCustomizer() {
158+
final var customizer = new JacksonCustomizer() {
154159
@Override
155160
public ObjectMapper apply(ObjectMapper mapper) {
156161
JsonUtils.jaxbAnnotationSupport(mapper);
@@ -160,26 +165,68 @@ public ObjectMapper apply(ObjectMapper mapper) {
160165
return mapper;
161166
}
162167
};
163-
return YamlUtils.toStringUtf8(
164-
_NullSafe.stream(commandDtos)
165-
.collect(Collectors.toList()),
166-
customizer);
168+
final var commandDtoList = _NullSafe.stream(commandDtos)
169+
.collect(Collectors.toList());
170+
return YamlUtils.toStringUtf8ForList(
171+
commandDtoList, YamlUtils.Marshalling.MULTI_DOC,
172+
customizer);
167173
}
168174

169175
public List<CommandDto> fromYaml(final DataSource commandDtosYaml) {
170-
final JsonUtils.JacksonCustomizer customizer = new JacksonCustomizer() {
171-
@Override
172-
public ObjectMapper apply(ObjectMapper mapper) {
173-
JsonUtils.jaxbAnnotationSupport(mapper);
174-
CommandDtoUtils.memberDtoSupport(mapper);
175-
CommandDtoUtils.valueDtoSupport(mapper);
176-
return mapper;
177-
}
178-
};
179-
return YamlUtils.tryReadAsList(CommandDto.class, commandDtosYaml, customizer)
180-
.ifFailureFail()
181-
.getValue()
182-
.orElseGet(Collections::emptyList);
176+
final var customizer = yamlCommandDtoCustomizer();
177+
178+
final Try<List<CommandDto>> asList = YamlUtils.tryReadAsList(CommandDto.class, commandDtosYaml, customizer);
179+
if (asList.isSuccess()) {
180+
return asList.getValue().orElseGet(Collections::emptyList);
181+
}
182+
183+
final Try<List<CommandDto>> asMultiDocument = tryReadAsMultiDocument(CommandDto.class, commandDtosYaml, customizer);
184+
asMultiDocument.getFailure().ifPresent(multiDocFailure ->
185+
asList.getFailure().ifPresent(multiDocFailure::addSuppressed));
186+
187+
return asMultiDocument
188+
.ifFailureFail()
189+
.getValue()
190+
.orElseGet(Collections::emptyList);
191+
}
192+
193+
private JsonUtils.JacksonCustomizer yamlCommandDtoCustomizer() {
194+
return mapper -> {
195+
JsonUtils.jaxbAnnotationSupport(mapper);
196+
CommandDtoUtils.memberDtoSupport(mapper);
197+
CommandDtoUtils.valueDtoSupport(mapper);
198+
return mapper;
199+
};
200+
}
201+
202+
private <T> Try<List<T>> tryReadAsMultiDocument(
203+
final Class<T> elementType,
204+
final DataSource source,
205+
final JsonUtils.JacksonCustomizer ... customizers) {
206+
return source.tryReadAll(is -> Try.call(() -> {
207+
final var mapper = createYamlReader(customizers);
208+
final MappingIterator<T> documentReader = mapper.readerFor(elementType).readValues(is);
209+
final List<T> elements = new ArrayList<>();
210+
while (documentReader.hasNextValue()) {
211+
final T next = documentReader.nextValue();
212+
if (next != null) {
213+
elements.add(next);
214+
}
215+
}
216+
return elements;
217+
}));
218+
}
219+
220+
private ObjectMapper createYamlReader(final JsonUtils.JacksonCustomizer ... customizers) {
221+
var mapper = new ObjectMapper(new YAMLFactory());
222+
mapper = JsonUtils.jdk8Support(mapper);
223+
mapper = JsonUtils.readingJavaTimeSupport(mapper);
224+
mapper = JsonUtils.readingCanSupport(mapper);
225+
for (final var customizer : customizers) {
226+
mapper = Optional.ofNullable(customizer.apply(mapper))
227+
.orElse(mapper);
228+
}
229+
return mapper;
183230
}
184231

185232
// Mix-in to add type metadata to MemberDto
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
majorVersion: "2"
2+
minorVersion: "0"
3+
interactionId: "08c210e1-55c0-4c45-83db-491f6581e621"
4+
timestamp: "2026-04-21T09:43:58.169+00:00"
5+
username: "estatio-admin"
6+
targets:
7+
oid:
8+
- type: "outgoing.invoiceforlease.InvoiceForLease"
9+
id: "419264"
10+
member: !<ACT>
11+
logicalMemberIdentifier: "outgoing.invoiceforlease.InvoiceForLease#approve"
12+
interactionType: "action_invocation"
13+
---
14+
majorVersion: "2"
15+
minorVersion: "0"
16+
interactionId: "0e2ad15e-c1d8-48c5-8b63-b8cdde673715"
17+
timestamp: "2026-04-21T09:45:07.409+00:00"
18+
username: "estatio-admin"
19+
targets:
20+
oid:
21+
- type: "outgoing.invoiceforlease.InvoiceForLease"
22+
id: "419264"
23+
member: !<ACT>
24+
parameters:
25+
parameter:
26+
- localDate: "2026-04-20"
27+
type: "localDate"
28+
name: "Invoice Date"
29+
- boolean: false
30+
type: "boolean"
31+
name: "Allow Invoice Date In Future"
32+
logicalMemberIdentifier: "outgoing.invoiceforlease.InvoiceForLease#invoice"
33+
interactionType: "action_invocation"
34+
---
35+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
majorVersion: "2"
2+
minorVersion: "0"
3+
interactionId: "08c210e1-55c0-4c45-83db-491f6581e621"
4+
timestamp: "2026-04-21T09:43:58.169+00:00"
5+
username: "estatio-admin"
6+
targets:
7+
oid:
8+
- type: "outgoing.invoiceforlease.InvoiceForLease"
9+
id: "419264"
10+
member: !<ACT>
11+
logicalMemberIdentifier: "outgoing.invoiceforlease.InvoiceForLease#approve"
12+
interactionType: "action_invocation"
13+
---
14+
majorVersion: "2"
15+
minorVersion: "0"
16+
interactionId: "0e2ad15e-c1d8-48c5-8b63-b8cdde673715"
17+
timestamp: "2026-04-21T09:45:07.409+00:00"
18+
username: "estatio-admin"
19+
targets:
20+
oid:
21+
- type: "outgoing.invoiceforlease.InvoiceForLease"
22+
id: "419264"
23+
member: !<ACT>
24+
parameters:
25+
parameter:
26+
- localDate: "2026-04-20"
27+
type: "localDate"
28+
name: "Invoice Date"
29+
- boolean: false
30+
type: "boolean"
31+
name: "Allow Invoice Date In Future"
32+
logicalMemberIdentifier: "outgoing.invoiceforlease.InvoiceForLease#invoice"
33+
interactionType: "action_invocation"
34+

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ public class CommandDtoUtils_fromYaml_Test {
4141
public void scalarValues() throws IOException {
4242
var commandDtos = loadCommands("commands-with-scalar-params.yaml");
4343

44+
assertScalarCommands(commandDtos);
45+
}
46+
47+
@Test
48+
public void scalarValuesAsMultiDocument() throws IOException {
49+
var commandDtos = loadCommands("commands-with-scalar-params-multi-document.yaml");
50+
51+
assertScalarCommands(commandDtos);
52+
}
53+
54+
@Test
55+
public void scalarValuesAsMultiDocumentWithTrailingEmptyDocument() throws IOException {
56+
var commandDtos = loadCommands("commands-with-scalar-params-multi-document-trailing-empty.yaml");
57+
58+
assertScalarCommands(commandDtos);
59+
}
60+
61+
private static void assertScalarCommands(final List<CommandDto> commandDtos) {
62+
4463
Assertions.assertThat(commandDtos).hasSize(2);
4564

4665
ActionDto firstAction = (ActionDto) commandDtos.get(0).getMember();

commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.causeway.commons.io;
2020

2121
import java.io.InputStream;
22+
import java.util.ArrayList;
2223
import java.util.List;
2324
import java.util.Optional;
2425

@@ -42,6 +43,11 @@
4243
@UtilityClass
4344
public class YamlUtils {
4445

46+
public enum Marshalling {
47+
YAML_LIST,
48+
MULTI_DOC
49+
}
50+
4551
// -- READING
4652

4753
/**
@@ -111,6 +117,40 @@ public static String toStringUtf8(
111117
? createJacksonWriter(customizers).writeValueAsString(pojo)
112118
: null;
113119
}
120+
121+
/**
122+
* Converts given list to UTF8 encoded YAML using the requested marshalling mode.
123+
* @return <code>null</code> if list is <code>null</code>
124+
*/
125+
@SneakyThrows
126+
@Nullable
127+
public static String toStringUtf8ForList(
128+
final @Nullable List<?> list,
129+
final @NonNull Marshalling marshalling,
130+
final JsonUtils.JacksonCustomizer ... customizers) {
131+
if (list == null) {
132+
return null;
133+
}
134+
if (marshalling == Marshalling.YAML_LIST) {
135+
return toStringUtf8(list, customizers);
136+
}
137+
return toStringUtf8AsMultiDocument(list, customizers);
138+
}
139+
140+
@SneakyThrows
141+
private static String toStringUtf8AsMultiDocument(
142+
final List<?> list,
143+
final JsonUtils.JacksonCustomizer ... customizers) {
144+
if (list.isEmpty()) {
145+
return "";
146+
}
147+
final var mapper = createJacksonWriter(customizers);
148+
final List<String> serializedDocuments = new ArrayList<>();
149+
for (Object element : list) {
150+
serializedDocuments.add(mapper.writeValueAsString(element));
151+
}
152+
return String.join("---\n", serializedDocuments);
153+
}
114154
// -- CUSTOMIZERS
115155

116156
/**

commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.apache.causeway.commons.io;
2020

21+
import java.util.List;
22+
2123
import org.approvaltests.Approvals;
2224
import org.junit.jupiter.api.BeforeEach;
2325
import org.junit.jupiter.api.Test;
@@ -43,6 +45,28 @@ void toStringUtf8() {
4345
Approvals.verify(yaml);
4446
}
4547

48+
@Test
49+
void toStringUtf8ForList_yamlList() {
50+
val person2 = _TestDomain.samplePerson();
51+
person2.setName("fred");
52+
53+
val yaml = YamlUtils.toStringUtf8ForList(
54+
List.of(person, person2),
55+
YamlUtils.Marshalling.YAML_LIST);
56+
Approvals.verify(yaml);
57+
}
58+
59+
@Test
60+
void toStringUtf8ForList_multiDoc() {
61+
val person2 = _TestDomain.samplePerson();
62+
person2.setName("fred");
63+
64+
val yaml = YamlUtils.toStringUtf8ForList(
65+
List.of(person, person2),
66+
YamlUtils.Marshalling.MULTI_DOC);
67+
Approvals.verify(yaml);
68+
}
69+
4670
@Test
4771
void parseRecord() {
4872
var yamlTemplate = ""
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: "sven"
2+
address:
3+
zip: 1234
4+
street: "backerstreet"
5+
additionalAddresses:
6+
- zip: 23
7+
street: "brownstreet"
8+
- zip: 34
9+
street: "bluestreet"
10+
java8Time:
11+
localTime: "17:33:45"
12+
localDate: "2007-11-21"
13+
localDateTime: "2007-11-21T17:33:45"
14+
offsetTime: "17:33:45-02:00"
15+
offsetDateTime: "2007-11-21T17:33:45-02:00"
16+
zonedDateTime: "2007-11-21T17:33:45+01:00"
17+
phone:
18+
home: "+99 1234"
19+
work: null
20+
---
21+
name: "fred"
22+
address:
23+
zip: 1234
24+
street: "backerstreet"
25+
additionalAddresses:
26+
- zip: 23
27+
street: "brownstreet"
28+
- zip: 34
29+
street: "bluestreet"
30+
java8Time:
31+
localTime: "17:33:45"
32+
localDate: "2007-11-21"
33+
localDateTime: "2007-11-21T17:33:45"
34+
offsetTime: "17:33:45-02:00"
35+
offsetDateTime: "2007-11-21T17:33:45-02:00"
36+
zonedDateTime: "2007-11-21T17:33:45+01:00"
37+
phone:
38+
home: "+99 1234"
39+
work: null

0 commit comments

Comments
 (0)