From dd45d8c06e089fcf1f64413c2f2bfbfae4875bbc Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 26 Apr 2026 08:43:37 +0200 Subject: [PATCH 1/9] CAUSEWAY-3989: [CommandLog] I/O v2->v4 forward porting --- .../apache/causeway/commons/io/YamlUtils.java | 30 ++++++++++++-- .../causeway/commons/io/YamlUtilsTest.java | 36 +++++++++++++++++ ....toStringUtf8ForList_multiDoc.approved.txt | 39 +++++++++++++++++++ ....toStringUtf8ForList_yamlList.approved.txt | 38 ++++++++++++++++++ .../causeway/commons/io/_TestDomain.java | 6 ++- 5 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_multiDoc.approved.txt create mode 100644 commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_yamlList.approved.txt diff --git a/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java index ce0c4ec5159..6305629f691 100644 --- a/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java +++ b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -32,6 +34,7 @@ import org.yaml.snakeyaml.DumperOptions; import org.apache.causeway.commons.functional.Try; +import org.apache.causeway.commons.internal.base._NullSafe; import lombok.SneakyThrows; import lombok.experimental.UtilityClass; @@ -49,6 +52,8 @@ @UtilityClass public class YamlUtils { + public static final String MULTI_DOC_DELIMITER = "---\n"; + @FunctionalInterface public interface YamlDumpCustomizer extends Consumer {} @FunctionalInterface @@ -144,7 +149,9 @@ public void write( final @Nullable Object pojo, final @NonNull DataSink sink, final JsonUtils.JacksonCustomizer ... customizers) { - if(pojo==null) return; + if(pojo==null) { + return; + } sink.writeAll(os-> Try.run(()->createJacksonWriter(Optional.empty(), customizers).writeValue(os, pojo))); } @@ -171,7 +178,9 @@ public void writeCustomized( final @NonNull DataSink sink, final @NonNull YamlDumpCustomizer dumpCustomizer, final JsonUtils.JacksonCustomizer ... customizers) { - if(pojo==null) return; + if(pojo==null) { + return; + } sink.writeAll(os-> Try.run(()->createJacksonWriter(Optional.of(dumpCustomizer), customizers).writeValue(os, pojo))); } @@ -182,7 +191,7 @@ public void writeCustomized( */ @SneakyThrows @Nullable - public static String toStringUtf8Customized( + public String toStringUtf8Customized( final @Nullable Object pojo, final @NonNull YamlDumpCustomizer dumpCustomizer, final JsonUtils.JacksonCustomizer ... customizers) { @@ -191,6 +200,21 @@ public static String toStringUtf8Customized( : null; } + /** + * Concatenates given {@link Iterable} into a multi-doc YAML. + */ + public String writeMultiDoc(@Nullable Iterable yamlDocuments) { + return _NullSafe.stream(yamlDocuments) + .collect(Collectors.joining(YamlUtils.MULTI_DOC_DELIMITER)); + } + /** + * Concatenates given {@link Stream} into a multi-doc YAML. + */ + public String writeMultiDoc(@Nullable Stream yamlDocumentStream) { + return _NullSafe.stream(yamlDocumentStream) + .collect(Collectors.joining(YamlUtils.MULTI_DOC_DELIMITER)); + } + // -- CUSTOMIZERS /** diff --git a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java b/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java index f15406b08bc..f0a5f5ae25b 100644 --- a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java +++ b/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java @@ -18,7 +18,13 @@ */ package org.apache.causeway.commons.io; +import java.util.List; + import org.approvaltests.Approvals; +import org.approvaltests.core.Options; +import org.approvaltests.reporters.DiffReporter; +import org.approvaltests.reporters.UseReporter; +import org.approvaltests.reporters.linux.ReportWithMeldMergeLinux; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,10 +35,14 @@ class YamlUtilsTest { private Person person; + private List persons; @BeforeEach void setup() { this.person = _TestDomain.samplePerson(); + this.persons = List.of( + person, + _TestDomain.samplePerson("fred")); } @Test @@ -72,5 +82,31 @@ void parseRecord() { .valueAsNonNullElseFail(); assertEquals(this.person, person); } + + @Test + @UseReporter(DiffReporter.class) + void toStringUtf8ForList_yamlList() { + var yaml = YamlUtils.toStringUtf8(persons); + Approvals.verify(yaml, defaultOptions()); + } + + @Test + @UseReporter(DiffReporter.class) + void toStringUtf8ForList_multiDoc() { + var yaml = YamlUtils.writeMultiDoc( + persons.stream() + .map(YamlUtils::toStringUtf8)); + Approvals.verify(yaml, defaultOptions()); + } + //TODO de-duplicate (from internaltestsupport) + static Options defaultOptions() { + var opts = new Options(); + // on Linux, at time of writing, the default reporter find mechanism throws an exception while evaluating Windows Diff Reporters; + // this is a workaround, provided you are on Linux and have Meld installed + return ReportWithMeldMergeLinux.INSTANCE.checkFileExists() + ? opts.withReporter(ReportWithMeldMergeLinux.INSTANCE) + : opts; + } + } diff --git a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_multiDoc.approved.txt b/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_multiDoc.approved.txt new file mode 100644 index 00000000000..3cac21f215c --- /dev/null +++ b/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_multiDoc.approved.txt @@ -0,0 +1,39 @@ +name: "sven" +address: + zip: 1234 + street: "backerstreet" +additionalAddresses: +- zip: 23 + street: "brownstreet" +- zip: 34 + street: "bluestreet" +java8Time: + localTime: "17:33:45" + localDate: "2007-11-21" + localDateTime: "2007-11-21T17:33:45" + offsetTime: "17:33:45-02:00" + offsetDateTime: "2007-11-21T17:33:45-02:00" + zonedDateTime: "2007-11-21T17:33:45+01:00" +phone: + home: "+99 1234" + work: null +--- +name: "fred" +address: + zip: 1234 + street: "backerstreet" +additionalAddresses: +- zip: 23 + street: "brownstreet" +- zip: 34 + street: "bluestreet" +java8Time: + localTime: "17:33:45" + localDate: "2007-11-21" + localDateTime: "2007-11-21T17:33:45" + offsetTime: "17:33:45-02:00" + offsetDateTime: "2007-11-21T17:33:45-02:00" + zonedDateTime: "2007-11-21T17:33:45+01:00" +phone: + home: "+99 1234" + work: null diff --git a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_yamlList.approved.txt b/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_yamlList.approved.txt new file mode 100644 index 00000000000..935533db17e --- /dev/null +++ b/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_yamlList.approved.txt @@ -0,0 +1,38 @@ +- name: "sven" + address: + zip: 1234 + street: "backerstreet" + additionalAddresses: + - zip: 23 + street: "brownstreet" + - zip: 34 + street: "bluestreet" + java8Time: + localTime: "17:33:45" + localDate: "2007-11-21" + localDateTime: "2007-11-21T17:33:45" + offsetTime: "17:33:45-02:00" + offsetDateTime: "2007-11-21T17:33:45-02:00" + zonedDateTime: "2007-11-21T17:33:45+01:00" + phone: + home: "+99 1234" + work: null +- name: "fred" + address: + zip: 1234 + street: "backerstreet" + additionalAddresses: + - zip: 23 + street: "brownstreet" + - zip: 34 + street: "bluestreet" + java8Time: + localTime: "17:33:45" + localDate: "2007-11-21" + localDateTime: "2007-11-21T17:33:45" + offsetTime: "17:33:45-02:00" + offsetDateTime: "2007-11-21T17:33:45-02:00" + zonedDateTime: "2007-11-21T17:33:45+01:00" + phone: + home: "+99 1234" + work: null diff --git a/commons/src/test/java/org/apache/causeway/commons/io/_TestDomain.java b/commons/src/test/java/org/apache/causeway/commons/io/_TestDomain.java index 1181e75ce8f..084396ef686 100644 --- a/commons/src/test/java/org/apache/causeway/commons/io/_TestDomain.java +++ b/commons/src/test/java/org/apache/causeway/commons/io/_TestDomain.java @@ -96,7 +96,11 @@ public static record Java8TimeStringified( } Person samplePerson() { - return new Person("sven", new Address(1234, "backerstreet"), + return samplePerson("sven"); + } + + Person samplePerson(String name) { + return new Person(name, new Address(1234, "backerstreet"), Can.of(new Address(23, "brownstreet"), new Address(34, "bluestreet")), new Java8Time( From 751c453e02b71fafe04995c6e06e48670805edc0 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 26 Apr 2026 09:52:48 +0200 Subject: [PATCH 2/9] CAUSEWAY-3989: moves approval test (commons, applib) to mmtest - instead of duplicating approval test support code all over the place, move all the tests to mmtest, where we have all the required infrastructure --- api/applib/pom.xml | 1 - commons/pom.xml | 12 -- .../org/apache/causeway/commons/io/.gitignore | 1 - core/internaltestsupport/pom.xml | 12 ++ core/mmtest/.gitignore | 1 + core/mmtest/pom.xml | 76 +++++--- ...ommandDtoUtils_fromYaml_Approval_Test.java | 105 +++++++++++ ...l_Test.commands-with-collection-param.yaml | 92 ++++++++++ ...nds-with-scalar-params-multi-document.yaml | 34 ++++ ...Yaml_Test.commands-with-scalar-params.yaml | 32 ++++ .../schema/CommandDtoUtils_fromYaml_Test.java | 164 +++++++++++++++++ .../CommandDtoUtils_toYaml_Approval_Test.java | 169 ++++++++++++++++++ ...shals_all_date_time_datatypes.approved.txt | 31 ++++ .../CommandDtoUtils_toYaml_fromYaml_Test.java | 135 ++++++++++++++ .../causeway/commons/io/DataSourceTest.java | 0 .../causeway/commons/io/JaxbUtilsTest.java | 0 ...with_indent_number_overridden.approved.txt | 0 ...Utf8_with_no_formatted_output.approved.txt | 0 ....toStringUtf8_with_no_options.approved.txt | 0 .../causeway/commons/io/JsonUtilsTest.java | 0 ...t.toStringUtf8_indentedOutput.approved.txt | 0 .../causeway/commons/io/YamlUtilsTest.java | 17 +- .../YamlUtilsTest.toStringUtf8.approved.txt | 0 ....toStringUtf8ForList_multiDoc.approved.txt | 0 ....toStringUtf8ForList_yamlList.approved.txt | 0 .../causeway/commons/io/ZipUtilsTest.java | 0 .../causeway/commons/io/_TestDomain.java | 0 27 files changed, 826 insertions(+), 56 deletions(-) delete mode 100644 commons/src/test/java/org/apache/causeway/commons/io/.gitignore create mode 100644 core/mmtest/.gitignore create mode 100644 core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java create mode 100644 core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml create mode 100644 core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params-multi-document.yaml create mode 100644 core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml create mode 100644 core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java create mode 100644 core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java create mode 100644 core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt create mode 100644 core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/DataSourceTest.java (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_indent_number_overridden.approved.txt (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_no_formatted_output.approved.txt (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_no_options.approved.txt (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.java (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.toStringUtf8_indentedOutput.approved.txt (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java (82%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8.approved.txt (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_multiDoc.approved.txt (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_yamlList.approved.txt (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/ZipUtilsTest.java (100%) rename {commons => core/mmtest}/src/test/java/org/apache/causeway/commons/io/_TestDomain.java (100%) diff --git a/api/applib/pom.xml b/api/applib/pom.xml index 754bdbaadb6..569f0e4dbe2 100644 --- a/api/applib/pom.xml +++ b/api/applib/pom.xml @@ -76,7 +76,6 @@ causeway-core-internaltestsupport test - diff --git a/commons/pom.xml b/commons/pom.xml index 18546295a83..4354bbab845 100644 --- a/commons/pom.xml +++ b/commons/pom.xml @@ -166,18 +166,6 @@ hamcrest-library test - - com.approvaltests - approvaltests - test - - - - com.google.code.gson - gson - test - diff --git a/commons/src/test/java/org/apache/causeway/commons/io/.gitignore b/commons/src/test/java/org/apache/causeway/commons/io/.gitignore deleted file mode 100644 index e8bd71526ca..00000000000 --- a/commons/src/test/java/org/apache/causeway/commons/io/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/*.received.txt diff --git a/core/internaltestsupport/pom.xml b/core/internaltestsupport/pom.xml index a7425ef1e55..a752f1140a1 100644 --- a/core/internaltestsupport/pom.xml +++ b/core/internaltestsupport/pom.xml @@ -112,6 +112,18 @@ additional org.slf4j slf4j-api + + + + + com.approvaltests + approvaltests + + + + com.google.code.gson + gson diff --git a/core/mmtest/.gitignore b/core/mmtest/.gitignore new file mode 100644 index 00000000000..e66bc565654 --- /dev/null +++ b/core/mmtest/.gitignore @@ -0,0 +1 @@ +*.received.txt \ No newline at end of file diff --git a/core/mmtest/pom.xml b/core/mmtest/pom.xml index 1f9627c8c6c..9c0ed7b6869 100644 --- a/core/mmtest/pom.xml +++ b/core/mmtest/pom.xml @@ -12,41 +12,61 @@ additional OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> - 4.0.0 - - - org.apache.causeway.core - causeway-core - 4.0.0-SNAPSHOT - - - causeway-core-mmtest - Apache Causeway Core - Metamodel Test - - - org.apache.causeway.core.mmtest - org/apache/causeway/core/mmtest - true - + - - Tests the causeway-core-metamodel artifact (using a custom - MetaModelContext for testing, as provided by - causeway-core-mmtestsupport). - Introduced to avoid a circular artifact dependency. - + + Tests the causeway-core-metamodel artifact (using a custom + MetaModelContext for testing, as provided by + causeway-core-mmtestsupport). + Introduced to avoid a circular artifact dependency. + - + + + org.apache.causeway.core + causeway-core-mmtestsupport + test + + + org.apache.causeway.testing + causeway-testing-unittestsupport-applib + test + + + org.apache.causeway.core + causeway-core-internaltestsupport + test + + + org.apache.causeway.testing + causeway-testing-integtestsupport-applib + - org.apache.causeway.core - causeway-core-mmtestsupport + + com.google.code.gson + gson test - + diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java new file mode 100644 index 00000000000..812a74ed974 --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java @@ -0,0 +1,105 @@ +/* + * 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.causeway.applib.util.schema; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.util.StreamUtils; + +import org.apache.causeway.commons.io.DataSource; +import org.apache.causeway.schema.cmd.v2.ActionDto; +import org.apache.causeway.schema.cmd.v2.CommandDto; +import org.apache.causeway.schema.cmd.v2.ParamDto; +import org.apache.causeway.schema.common.v2.ValueType; + +@Disabled //FIXME +class CommandDtoUtils_fromYaml_Approval_Test { + + @Test + void unmarshals_all_date_time_datatypes_from_approved_toYaml_snapshot() throws IOException { + String yaml = readApprovalSnapshot(); + + List commands = CommandDtoUtils.fromYaml(DataSource.ofStringUtf8(yaml)); + + Assertions.assertThat(commands).singleElement().satisfies(command -> { + Assertions.assertThat(command.getInteractionId()).isEqualTo("approval-datetime-marshalling"); + + ActionDto action = (ActionDto) command.getMember(); + Assertions.assertThat(action.getLogicalMemberIdentifier()) + .isEqualTo("demo.Customer#allDateTimeTypes"); + + List params = action.getParameters().getParameter(); + Assertions.assertThat(params).hasSize(6); + + ParamDto localDate = params.get(0); + Assertions.assertThat(localDate.getType()).isEqualTo(ValueType.LOCAL_DATE); + Assertions.assertThat(localDate.getLocalDate().toXMLFormat()).isEqualTo("2026-07-01"); + + ParamDto localDateTime = params.get(1); + Assertions.assertThat(localDateTime.getType()).isEqualTo(ValueType.LOCAL_DATE_TIME); + Assertions.assertThat(localDateTime.getLocalDateTime().toXMLFormat()).isEqualTo("2026-07-01T10:15:30"); + + ParamDto localTime = params.get(2); + Assertions.assertThat(localTime.getType()).isEqualTo(ValueType.LOCAL_TIME); + Assertions.assertThat(localTime.getLocalTime().toXMLFormat()).isEqualTo("10:15:30"); + + ParamDto offsetDateTime = params.get(3); + Assertions.assertThat(offsetDateTime.getType()).isEqualTo(ValueType.OFFSET_DATE_TIME); + Assertions.assertThat(offsetDateTime.getOffsetDateTime().getYear()).isEqualTo(2026); + Assertions.assertThat(offsetDateTime.getOffsetDateTime().getMonth()).isEqualTo(7); + Assertions.assertThat(offsetDateTime.getOffsetDateTime().getDay()).isEqualTo(1); + Assertions.assertThat(offsetDateTime.getOffsetDateTime().getHour()).isEqualTo(8); + Assertions.assertThat(offsetDateTime.getOffsetDateTime().getMinute()).isEqualTo(15); + Assertions.assertThat(offsetDateTime.getOffsetDateTime().getSecond()).isEqualTo(30); + Assertions.assertThat(offsetDateTime.getOffsetDateTime().getTimezone()).isEqualTo(0); + + ParamDto offsetTime = params.get(4); + Assertions.assertThat(offsetTime.getType()).isEqualTo(ValueType.OFFSET_TIME); + Assertions.assertThat(offsetTime.getOffsetTime()).isNotNull(); + Assertions.assertThat(offsetTime.getOffsetTime().getHour()).isEqualTo(8); + Assertions.assertThat(offsetTime.getOffsetTime().getMinute()).isEqualTo(15); + Assertions.assertThat(offsetTime.getOffsetTime().getSecond()).isEqualTo(30); + Assertions.assertThat(offsetTime.getOffsetTime().getTimezone()).isEqualTo(0); + + ParamDto zonedDateTime = params.get(5); + Assertions.assertThat(zonedDateTime.getType()).isEqualTo(ValueType.ZONED_DATE_TIME); + Assertions.assertThat(zonedDateTime.getZonedDateTime().getYear()).isEqualTo(2026); + Assertions.assertThat(zonedDateTime.getZonedDateTime().getMonth()).isEqualTo(7); + Assertions.assertThat(zonedDateTime.getZonedDateTime().getDay()).isEqualTo(1); + Assertions.assertThat(zonedDateTime.getZonedDateTime().getHour()).isEqualTo(8); + Assertions.assertThat(zonedDateTime.getZonedDateTime().getMinute()).isEqualTo(15); + Assertions.assertThat(zonedDateTime.getZonedDateTime().getSecond()).isEqualTo(30); + Assertions.assertThat(zonedDateTime.getZonedDateTime().getTimezone()) + .isEqualTo(0); + }); + } + + private String readApprovalSnapshot() throws IOException { + String path = CommandDtoUtils_toYaml_Approval_Test.class.getSimpleName() + ".marshals_all_date_time_datatypes.approved.txt"; + InputStream stream = CommandDtoUtils_toYaml_Approval_Test.class.getResourceAsStream(path); + return StreamUtils.copyToString(stream, java.nio.charset.StandardCharsets.UTF_8); + } + +} diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml new file mode 100644 index 00000000000..4175c718be9 --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml @@ -0,0 +1,92 @@ +- majorVersion: "2" + minorVersion: "0" + interactionId: "87ded048-530d-41b4-a431-59df0a77899d" + timestamp: "2026-04-21T09:09:06.882+00:00" + username: "estatio-admin" + targets: + oid: + - type: "outgoing.lease.Lease" + id: "32846" + member: ! + parameters: + parameter: + - enum: + enumType: "org.estatio.module.invoice.dom.InvoiceRunType" + enumName: "NORMAL_RUN" + type: "enum" + name: "Run Type" + - collection: + value: + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "RENT" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "RENT_FIXED" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "RENT_DISCOUNT" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "RENT_DISCOUNT_FIXED" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "SERVICE_CHARGE" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "SERVICE_CHARGE_INDEXABLE" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "SERVICE_CHARGE_DISCOUNT_FIXED" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "DEPOSIT" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "TAX" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "MARKETING" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "PROPERTY_TAX" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "OFFICE_TAX" + type: "enum" + - enum: + enumType: "org.estatio.module.lease.dom.LeaseItemType" + enumName: "RETAIL_TAX" + type: "enum" + type: "enum" + type: "collection" + "null": false + name: "Lease Item Types" + - localDate: "2026-06-30" + type: "localDate" + name: "Invoice Due Date" + - localDate: "2026-06-30" + type: "localDate" + name: "Start Due Date" + - localDate: "2026-07-01" + type: "localDate" + name: "Next Due Date" + - type: "string" + "null": true + name: "Tag Name" + - string: "JDOJPA-T1" + type: "string" + name: "New Tag Name" + logicalMemberIdentifier: "outgoing.lease.Lease#calculate" + interactionType: "action_invocation" \ No newline at end of file diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params-multi-document.yaml b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params-multi-document.yaml new file mode 100644 index 00000000000..8ad1657ae59 --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params-multi-document.yaml @@ -0,0 +1,34 @@ +majorVersion: "2" +minorVersion: "0" +interactionId: "08c210e1-55c0-4c45-83db-491f6581e621" +timestamp: "2026-04-21T09:43:58.169+00:00" +username: "estatio-admin" +targets: + oid: + - type: "outgoing.invoiceforlease.InvoiceForLease" + id: "419264" +member: ! + logicalMemberIdentifier: "outgoing.invoiceforlease.InvoiceForLease#approve" + interactionType: "action_invocation" +--- +majorVersion: "2" +minorVersion: "0" +interactionId: "0e2ad15e-c1d8-48c5-8b63-b8cdde673715" +timestamp: "2026-04-21T09:45:07.409+00:00" +username: "estatio-admin" +targets: + oid: + - type: "outgoing.invoiceforlease.InvoiceForLease" + id: "419264" +member: ! + parameters: + parameter: + - localDate: "2026-04-20" + type: "localDate" + name: "Invoice Date" + - boolean: false + type: "boolean" + name: "Allow Invoice Date In Future" + logicalMemberIdentifier: "outgoing.invoiceforlease.InvoiceForLease#invoice" + interactionType: "action_invocation" + diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml new file mode 100644 index 00000000000..81222d72ad7 --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml @@ -0,0 +1,32 @@ +- majorVersion: "2" + minorVersion: "0" + interactionId: "08c210e1-55c0-4c45-83db-491f6581e621" + timestamp: "2026-04-21T09:43:58.169+00:00" + username: "estatio-admin" + targets: + oid: + - type: "outgoing.invoiceforlease.InvoiceForLease" + id: "419264" + member: ! + logicalMemberIdentifier: "outgoing.invoiceforlease.InvoiceForLease#approve" + interactionType: "action_invocation" +- majorVersion: "2" + minorVersion: "0" + interactionId: "0e2ad15e-c1d8-48c5-8b63-b8cdde673715" + timestamp: "2026-04-21T09:45:07.409+00:00" + username: "estatio-admin" + targets: + oid: + - type: "outgoing.invoiceforlease.InvoiceForLease" + id: "419264" + member: ! + parameters: + parameter: + - localDate: "2026-04-20" + type: "localDate" + name: "Invoice Date" + - boolean: false + type: "boolean" + name: "Allow Invoice Date In Future" + logicalMemberIdentifier: "outgoing.invoiceforlease.InvoiceForLease#invoice" + interactionType: "action_invocation" diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java new file mode 100644 index 00000000000..4bf98dfa899 --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java @@ -0,0 +1,164 @@ +/* + * 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.causeway.applib.util.schema; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.util.StreamUtils; + +import org.apache.causeway.applib.value.Blob; +import org.apache.causeway.applib.value.NamedWithMimeType; +import org.apache.causeway.schema.cmd.v2.ActionDto; +import org.apache.causeway.schema.cmd.v2.CommandDto; +import org.apache.causeway.schema.cmd.v2.ParamDto; +import org.apache.causeway.schema.common.v2.InteractionType; +import org.apache.causeway.schema.common.v2.ValueType; + +@Disabled //FIXME +class CommandDtoUtils_fromYaml_Test { + + @Test + void scalarValues() throws IOException { + var commandDtos = loadCommands("commands-with-scalar-params.yaml"); + + assertScalarCommands(commandDtos); + } + + @Test + void scalarValuesAsMultiDocument() throws IOException { + var commandDtos = loadCommands("commands-with-scalar-params-multi-document.yaml"); + + assertScalarCommands(commandDtos); + } + + private static void assertScalarCommands(final List commandDtos) { + + Assertions.assertThat(commandDtos).hasSize(2); + + ActionDto firstAction = (ActionDto) commandDtos.get(0).getMember(); + Assertions.assertThat(firstAction.getLogicalMemberIdentifier()) + .isEqualTo("outgoing.invoiceforlease.InvoiceForLease#approve"); + Assertions.assertThat(firstAction.getInteractionType()) + .isEqualTo(InteractionType.ACTION_INVOCATION); + Assertions.assertThat(commandDtos.get(0).getTargets().getOid()) + .singleElement() + .satisfies(oid -> { + Assertions.assertThat(oid.getType()).isEqualTo("outgoing.invoiceforlease.InvoiceForLease"); + Assertions.assertThat(oid.getId()).isEqualTo("419264"); + }); + + ActionDto secondAction = (ActionDto) commandDtos.get(1).getMember(); + Assertions.assertThat(secondAction.getLogicalMemberIdentifier()) + .isEqualTo("outgoing.invoiceforlease.InvoiceForLease#invoice"); + Assertions.assertThat(secondAction.getInteractionType()) + .isEqualTo(InteractionType.ACTION_INVOCATION); + + List scalarParams = secondAction.getParameters().getParameter(); + Assertions.assertThat(scalarParams).hasSize(2); + + ParamDto invoiceDate = scalarParams.get(0); + Assertions.assertThat(invoiceDate.getName()).isEqualTo("Invoice Date"); + Assertions.assertThat(invoiceDate.getType()).isEqualTo(ValueType.LOCAL_DATE); + Assertions.assertThat(invoiceDate.getLocalDate()).isNotNull(); + Assertions.assertThat(invoiceDate.getLocalDate().toXMLFormat()).isEqualTo("2026-04-20"); + + ParamDto allowFuture = scalarParams.get(1); + Assertions.assertThat(allowFuture.getName()).isEqualTo("Allow Invoice Date In Future"); + Assertions.assertThat(allowFuture.getType()).isEqualTo(ValueType.BOOLEAN); + Assertions.assertThat(allowFuture.isBoolean()).isFalse(); + } + + @Test + void collectionValues() throws IOException { + var commandDtos = loadCommands("commands-with-collection-param.yaml"); + + Assertions.assertThat(commandDtos).hasSize(1); + + ActionDto action = (ActionDto) commandDtos.get(0).getMember(); + Assertions.assertThat(action.getLogicalMemberIdentifier()) + .isEqualTo("outgoing.lease.Lease#calculate"); + Assertions.assertThat(action.getInteractionType()) + .isEqualTo(InteractionType.ACTION_INVOCATION); + + List params = action.getParameters().getParameter(); + Assertions.assertThat(params).hasSize(7); + + ParamDto leaseItemTypes = params.get(1); + Assertions.assertThat(leaseItemTypes.getName()).isEqualTo("Lease Item Types"); + Assertions.assertThat(leaseItemTypes.getType()).isEqualTo(ValueType.COLLECTION); + Assertions.assertThat(leaseItemTypes.isNull()).isFalse(); + Assertions.assertThat(leaseItemTypes.getCollection()).isNotNull(); + Assertions.assertThat(leaseItemTypes.getCollection().getType()).isEqualTo(ValueType.ENUM); + + var items = leaseItemTypes.getCollection().getValue(); + Assertions.assertThat(items).hasSize(13); + Assertions.assertThat(items).allSatisfy(item -> Assertions.assertThat(item.getEnum()).isNotNull()); + + Assertions.assertThat(items.get(0).getEnum().getEnumType()) + .isEqualTo("org.estatio.module.lease.dom.LeaseItemType"); + Assertions.assertThat(items.get(0).getEnum().getEnumName()).isEqualTo("RENT"); + + Assertions.assertThat(items.get(6).getEnum().getEnumName()).isEqualTo("SERVICE_CHARGE_DISCOUNT_FIXED"); + Assertions.assertThat(items.get(12).getEnum().getEnumName()).isEqualTo("RETAIL_TAX"); + + ParamDto nullableTagName = params.get(5); + Assertions.assertThat(nullableTagName.getName()).isEqualTo("Tag Name"); + Assertions.assertThat(nullableTagName.getType()).isEqualTo(ValueType.STRING); + Assertions.assertThat(nullableTagName.isNull()).isTrue(); + + ParamDto invoiceDueDate = params.get(2); + Assertions.assertThat(invoiceDueDate.getName()).isEqualTo("Invoice Due Date"); + Assertions.assertThat(invoiceDueDate.getType()).isEqualTo(ValueType.LOCAL_DATE); + Assertions.assertThat(invoiceDueDate.getLocalDate().toXMLFormat()).isEqualTo("2026-06-30"); + + ParamDto startDueDate = params.get(3); + Assertions.assertThat(startDueDate.getName()).isEqualTo("Start Due Date"); + Assertions.assertThat(startDueDate.getType()).isEqualTo(ValueType.LOCAL_DATE); + Assertions.assertThat(startDueDate.getLocalDate().toXMLFormat()).isEqualTo("2026-06-30"); + + ParamDto nextDueDate = params.get(4); + Assertions.assertThat(nextDueDate.getName()).isEqualTo("Next Due Date"); + Assertions.assertThat(nextDueDate.getType()).isEqualTo(ValueType.LOCAL_DATE); + Assertions.assertThat(nextDueDate.getLocalDate().toXMLFormat()).isEqualTo("2026-07-01"); + + ParamDto newTagName = params.get(6); + Assertions.assertThat(newTagName.getName()).isEqualTo("New Tag Name"); + Assertions.assertThat(newTagName.getType()).isEqualTo(ValueType.STRING); + Assertions.assertThat(newTagName.getString()).isEqualTo("JDOJPA-T1"); + } + + private List loadCommands(final String yamlFileName) throws IOException { + InputStream resourceAsStream = getClass().getResourceAsStream(getClass().getSimpleName() + "." + yamlFileName); + byte[] bytes = StreamUtils.copyToByteArray(resourceAsStream); + + Blob commandsYaml = new Blob( + yamlFileName, + NamedWithMimeType.CommonMimeType.YAML.mimeType(), + bytes); + + return CommandDtoUtils.fromYaml(commandsYaml.asDataSource()); + } + +} diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java new file mode 100644 index 00000000000..583a1784caa --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java @@ -0,0 +1,169 @@ +/* + * 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.causeway.applib.util.schema; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.TimeZone; + +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; + +import org.approvaltests.Approvals; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.util.StreamUtils; + +import org.apache.causeway.schema.cmd.v2.ActionDto; +import org.apache.causeway.schema.cmd.v2.CommandDto; +import org.apache.causeway.schema.cmd.v2.ParamDto; +import org.apache.causeway.schema.cmd.v2.ParamsDto; +import org.apache.causeway.schema.common.v2.InteractionType; +import org.apache.causeway.schema.common.v2.OidDto; +import org.apache.causeway.schema.common.v2.OidsDto; +import org.apache.causeway.schema.common.v2.ValueType; +import org.apache.causeway.testing.integtestsupport.applib.ApprovalsOptions; + +@Disabled //FIXME +class CommandDtoUtils_toYaml_Approval_Test { + + private static final DatatypeFactory DATATYPE_FACTORY = datatypeFactory(); + + @Test + void marshals_all_date_time_datatypes() { + withDefaultTimeZone("UTC", () -> { + String yaml = CommandDtoUtils.toYaml(List.of(commandWithAllDateTimeParams())); + Approvals.verify(yaml, ApprovalsOptions.defaultOptions()); + }); + } + + @Test + void marshals_all_date_time_datatypes_when_default_timezone_is_cest() { + withDefaultTimeZone("Europe/Paris", () -> { + String yaml = CommandDtoUtils.toYaml(List.of(commandWithAllDateTimeParams())); + try { + Assertions.assertThat(yaml).isEqualTo(readApprovalSnapshot()); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + } + + private static CommandDto commandWithAllDateTimeParams() { + CommandDto command = new CommandDto(); + command.setMajorVersion("2"); + command.setMinorVersion("0"); + command.setInteractionId("approval-datetime-marshalling"); + command.setUsername("approval-user"); + command.setTargets(targets("demo.Customer", "123")); + command.setMember(actionWithAllDateTimeParams()); + return command; + } + + private static void withDefaultTimeZone(final String zoneId, final _Runnable runnable) { + TimeZone originalDefault = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone(zoneId)); + try { + runnable.run(); + } finally { + TimeZone.setDefault(originalDefault); + } + } + + @FunctionalInterface + private interface _Runnable { + void run(); + } + + private String readApprovalSnapshot() throws IOException { + String path = getClass().getSimpleName() + ".marshals_all_date_time_datatypes.approved.txt"; + InputStream stream = getClass().getResourceAsStream(path); + return StreamUtils.copyToString(stream, StandardCharsets.UTF_8); + } + + private static ActionDto actionWithAllDateTimeParams() { + ActionDto action = new ActionDto(); + action.setLogicalMemberIdentifier("demo.Customer#allDateTimeTypes"); + action.setInteractionType(InteractionType.ACTION_INVOCATION); + + ParamsDto params = new ParamsDto(); + params.getParameter().add(param("Local Date", ValueType.LOCAL_DATE, "2026-07-01")); + params.getParameter().add(param("Local Date Time", ValueType.LOCAL_DATE_TIME, "2026-07-01T10:15:30")); + params.getParameter().add(param("Local Time", ValueType.LOCAL_TIME, "10:15:30")); + params.getParameter().add(param("Offset Date Time", ValueType.OFFSET_DATE_TIME, "2026-07-01T10:15:30+02:00")); + params.getParameter().add(param("Offset Time", ValueType.OFFSET_TIME, "10:15:30+02:00")); + params.getParameter().add(param("Zoned Date Time", ValueType.ZONED_DATE_TIME, "2026-07-01T10:15:30+02:00")); + action.setParameters(params); + return action; + } + + private static ParamDto param(final String name, final ValueType type, final String lexicalValue) { + ParamDto param = new ParamDto(); + param.setName(name); + param.setType(type); + + XMLGregorianCalendar value = DATATYPE_FACTORY.newXMLGregorianCalendar(lexicalValue); + switch (type) { + case LOCAL_DATE: + param.setLocalDate(value); + break; + case LOCAL_DATE_TIME: + param.setLocalDateTime(value); + break; + case LOCAL_TIME: + param.setLocalTime(value); + break; + case OFFSET_DATE_TIME: + param.setOffsetDateTime(value); + break; + case OFFSET_TIME: + param.setOffsetTime(value); + break; + case ZONED_DATE_TIME: + param.setZonedDateTime(value); + break; + default: + throw new IllegalArgumentException("Unsupported type: " + type); + } + return param; + } + + private static OidsDto targets(final String type, final String id) { + OidDto oid = new OidDto(); + oid.setType(type); + oid.setId(id); + + OidsDto targets = new OidsDto(); + targets.getOid().add(oid); + return targets; + } + + private static DatatypeFactory datatypeFactory() { + try { + return DatatypeFactory.newInstance(); + } catch (Exception ex) { + throw new RuntimeException("Failed to initialize DatatypeFactory", ex); + } + } + +} diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt new file mode 100644 index 00000000000..0d7479ee511 --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt @@ -0,0 +1,31 @@ +majorVersion: "2" +minorVersion: "0" +interactionId: "approval-datetime-marshalling" +username: "approval-user" +targets: + oid: + - type: "demo.Customer" + id: "123" +member: ! + parameters: + parameter: + - localDate: "2026-07-01" + type: "localDate" + name: "Local Date" + - localDateTime: "2026-07-01T10:15:30" + type: "localDateTime" + name: "Local Date Time" + - localTime: "10:15:30" + type: "localTime" + name: "Local Time" + - offsetDateTime: "2026-07-01T08:15:30.000+00:00" + type: "offsetDateTime" + name: "Offset Date Time" + - offsetTime: "1970-01-01T08:15:30.000+00:00" + type: "offsetTime" + name: "Offset Time" + - zonedDateTime: "2026-07-01T08:15:30.000+00:00" + type: "zonedDateTime" + name: "Zoned Date Time" + logicalMemberIdentifier: "demo.Customer#allDateTimeTypes" + interactionType: "action_invocation" diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java new file mode 100644 index 00000000000..e7517a0ed54 --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java @@ -0,0 +1,135 @@ +/* + * 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.causeway.applib.util.schema; + +import java.util.List; +import java.util.TimeZone; + +import javax.xml.datatype.DatatypeConstants; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.apache.causeway.commons.io.DataSource; +import org.apache.causeway.schema.cmd.v2.ActionDto; +import org.apache.causeway.schema.cmd.v2.CommandDto; +import org.apache.causeway.schema.cmd.v2.ParamDto; +import org.apache.causeway.schema.cmd.v2.ParamsDto; +import org.apache.causeway.schema.common.v2.InteractionType; +import org.apache.causeway.schema.common.v2.OidDto; +import org.apache.causeway.schema.common.v2.OidsDto; +import org.apache.causeway.schema.common.v2.ValueType; + +@Disabled //FIXME +class CommandDtoUtils_toYaml_fromYaml_Test { + + @Test + void localDate_roundtrips_as_date_only_without_timezone() throws Exception { + TimeZone originalDefault = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Amsterdam")); + try { + DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); + XMLGregorianCalendar originalDate = datatypeFactory + .newXMLGregorianCalendarDate(2026, 7, 1, DatatypeConstants.FIELD_UNDEFINED); + XMLGregorianCalendar originalDateTime = datatypeFactory + .newXMLGregorianCalendar("2026-07-01T10:15:30"); + XMLGregorianCalendar originalTime = datatypeFactory + .newXMLGregorianCalendar("10:15:30"); + + CommandDto command = new CommandDto(); + command.setMajorVersion("2"); + command.setMinorVersion("0"); + command.setInteractionId("localdate-roundtrip-bug"); + command.setUsername("sven"); + + OidDto oid = new OidDto(); + oid.setType("demo.Customer"); + oid.setId("123"); + OidsDto targets = new OidsDto(); + targets.getOid().add(oid); + command.setTargets(targets); + + ParamDto localDateParam = new ParamDto(); + localDateParam.setName("Invoice Date"); + localDateParam.setType(ValueType.LOCAL_DATE); + localDateParam.setLocalDate(originalDate); + + ParamDto localDateTimeParam = new ParamDto(); + localDateTimeParam.setName("Invoice Date Time"); + localDateTimeParam.setType(ValueType.LOCAL_DATE_TIME); + localDateTimeParam.setLocalDateTime(originalDateTime); + + ParamDto localTimeParam = new ParamDto(); + localTimeParam.setName("Invoice Time"); + localTimeParam.setType(ValueType.LOCAL_TIME); + localTimeParam.setLocalTime(originalTime); + + ParamsDto params = new ParamsDto(); + params.getParameter().add(localDateParam); + params.getParameter().add(localDateTimeParam); + params.getParameter().add(localTimeParam); + + ActionDto action = new ActionDto(); + action.setLogicalMemberIdentifier("demo.Customer#invoice"); + action.setInteractionType(InteractionType.ACTION_INVOCATION); + action.setParameters(params); + command.setMember(action); + + String yaml = CommandDtoUtils.toYaml(List.of(command)); + List roundtripped = CommandDtoUtils.fromYaml(DataSource.ofStringUtf8(yaml)); + + XMLGregorianCalendar roundtrippedDate = ((ActionDto) roundtripped.get(0).getMember()) + .getParameters() + .getParameter() + .get(0) + .getLocalDate(); + XMLGregorianCalendar roundtrippedDateTime = ((ActionDto) roundtripped.get(0).getMember()) + .getParameters() + .getParameter() + .get(1) + .getLocalDateTime(); + XMLGregorianCalendar roundtrippedTime = ((ActionDto) roundtripped.get(0).getMember()) + .getParameters() + .getParameter() + .get(2) + .getLocalTime(); + + // Verify fixed behavior: local date/time values are emitted and roundtripped without timezone. + Assertions.assertThat(yaml) + .contains("localDate: \"2026-07-01\"") + .contains("localDateTime: \"2026-07-01T10:15:30\"") + .contains("localTime: \"10:15:30\"") + .doesNotContain("localDate: \"2026-06-30T22:00:00.000+00:00\"") + .doesNotContain("localDateTime: \"2026-07-01T10:15:30.000+00:00\"") + .doesNotContain("localTime: \"1970-01-01T10:15:30.000+00:00\""); + Assertions.assertThat(roundtrippedDate.toXMLFormat()) + .isEqualTo(originalDate.toXMLFormat()); + Assertions.assertThat(roundtrippedDateTime.toXMLFormat()) + .isEqualTo(originalDateTime.toXMLFormat()); + Assertions.assertThat(roundtrippedTime.toXMLFormat()) + .isEqualTo(originalTime.toXMLFormat()); + } finally { + TimeZone.setDefault(originalDefault); + } + } + +} diff --git a/commons/src/test/java/org/apache/causeway/commons/io/DataSourceTest.java b/core/mmtest/src/test/java/org/apache/causeway/commons/io/DataSourceTest.java similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/DataSourceTest.java rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/DataSourceTest.java diff --git a/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java b/core/mmtest/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java diff --git a/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_indent_number_overridden.approved.txt b/core/mmtest/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_indent_number_overridden.approved.txt similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_indent_number_overridden.approved.txt rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_indent_number_overridden.approved.txt diff --git a/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_no_formatted_output.approved.txt b/core/mmtest/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_no_formatted_output.approved.txt similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_no_formatted_output.approved.txt rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_no_formatted_output.approved.txt diff --git a/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_no_options.approved.txt b/core/mmtest/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_no_options.approved.txt similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_no_options.approved.txt rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.toStringUtf8_with_no_options.approved.txt diff --git a/commons/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.java b/core/mmtest/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.java similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.java rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.java diff --git a/commons/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.toStringUtf8_indentedOutput.approved.txt b/core/mmtest/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.toStringUtf8_indentedOutput.approved.txt similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.toStringUtf8_indentedOutput.approved.txt rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.toStringUtf8_indentedOutput.approved.txt diff --git a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java b/core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java similarity index 82% rename from commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java index f0a5f5ae25b..99fd2fd8954 100644 --- a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java +++ b/core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java @@ -21,16 +21,15 @@ import java.util.List; import org.approvaltests.Approvals; -import org.approvaltests.core.Options; import org.approvaltests.reporters.DiffReporter; import org.approvaltests.reporters.UseReporter; -import org.approvaltests.reporters.linux.ReportWithMeldMergeLinux; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import org.apache.causeway.commons.io._TestDomain.Person; +import org.apache.causeway.testing.integtestsupport.applib.ApprovalsOptions; class YamlUtilsTest { @@ -87,7 +86,7 @@ void parseRecord() { @UseReporter(DiffReporter.class) void toStringUtf8ForList_yamlList() { var yaml = YamlUtils.toStringUtf8(persons); - Approvals.verify(yaml, defaultOptions()); + Approvals.verify(yaml, ApprovalsOptions.defaultOptions()); } @Test @@ -96,17 +95,7 @@ void toStringUtf8ForList_multiDoc() { var yaml = YamlUtils.writeMultiDoc( persons.stream() .map(YamlUtils::toStringUtf8)); - Approvals.verify(yaml, defaultOptions()); + Approvals.verify(yaml, ApprovalsOptions.defaultOptions()); } - - //TODO de-duplicate (from internaltestsupport) - static Options defaultOptions() { - var opts = new Options(); - // on Linux, at time of writing, the default reporter find mechanism throws an exception while evaluating Windows Diff Reporters; - // this is a workaround, provided you are on Linux and have Meld installed - return ReportWithMeldMergeLinux.INSTANCE.checkFileExists() - ? opts.withReporter(ReportWithMeldMergeLinux.INSTANCE) - : opts; - } } diff --git a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8.approved.txt b/core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8.approved.txt similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8.approved.txt rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8.approved.txt diff --git a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_multiDoc.approved.txt b/core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_multiDoc.approved.txt similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_multiDoc.approved.txt rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_multiDoc.approved.txt diff --git a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_yamlList.approved.txt b/core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_yamlList.approved.txt similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_yamlList.approved.txt rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8ForList_yamlList.approved.txt diff --git a/commons/src/test/java/org/apache/causeway/commons/io/ZipUtilsTest.java b/core/mmtest/src/test/java/org/apache/causeway/commons/io/ZipUtilsTest.java similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/ZipUtilsTest.java rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/ZipUtilsTest.java diff --git a/commons/src/test/java/org/apache/causeway/commons/io/_TestDomain.java b/core/mmtest/src/test/java/org/apache/causeway/commons/io/_TestDomain.java similarity index 100% rename from commons/src/test/java/org/apache/causeway/commons/io/_TestDomain.java rename to core/mmtest/src/test/java/org/apache/causeway/commons/io/_TestDomain.java From ee19e9555c4815b2099ca72c694a152e2bcff0c3 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 26 Apr 2026 10:48:20 +0200 Subject: [PATCH 3/9] CAUSEWAY-3989: generic multi-doc reading --- .../apache/causeway/commons/io/YamlUtils.java | 30 ++++++++++++++++++- .../causeway/commons/io/YamlUtilsTest.java | 14 +++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java index 6305629f691..bfbb89a479a 100644 --- a/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java +++ b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java @@ -19,6 +19,7 @@ package org.apache.causeway.commons.io; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Consumer; @@ -33,6 +34,7 @@ import org.snakeyaml.engine.v2.api.LoadSettingsBuilder; import org.yaml.snakeyaml.DumperOptions; +import org.apache.causeway.commons.collections.Can; import org.apache.causeway.commons.functional.Try; import org.apache.causeway.commons.internal.base._NullSafe; @@ -139,7 +141,33 @@ public Try> tryReadAsListCustomized( return mapper.readValue(is, collectionType); })); } - + + /** + * Returns a {@link Stream} of (arbitrary) YAML Document junks from provided {@link DataSource}, + * where each junk typically needs further parsing individually. + */ + public Try> tryReadMultiDoc(final @Nullable DataSource source) { + return source == null + ? Try.success(Stream.empty()) + : source.tryReadAsStringUtf8() + .mapSuccessAsNullable(TextUtils::readLines) + .mapSuccessAsNullable(YamlUtils::linesToDocs); + } + private static Stream linesToDocs(final Can lineStream) { + var buffer = new ArrayList(); + buffer.add(new StringBuilder()); + for(var line : lineStream) { + if(line.equals("---")) { + buffer.add(new StringBuilder()); + continue; + } + buffer.get(buffer.size()-1).append(line).append("\n"); + } + return buffer.stream() + .map(StringBuilder::toString) + .filter(str->!str.isEmpty()); + } + // -- WRITING /** diff --git a/core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java b/core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java index 99fd2fd8954..8596d72fb58 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java +++ b/core/mmtest/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java @@ -98,4 +98,18 @@ void toStringUtf8ForList_multiDoc() { Approvals.verify(yaml, ApprovalsOptions.defaultOptions()); } + @Test + void multiDocRoundtrip() { + var multiDocYaml = YamlUtils.writeMultiDoc( + persons.stream() + .map(YamlUtils::toStringUtf8)); + var personsAfterRoundtrip = YamlUtils.tryReadMultiDoc(DataSource.ofStringUtf8(multiDocYaml)) + .valueAsNonNullElseFail() + .map(yaml->YamlUtils.tryRead(Person.class, yaml) + .valueAsNonNullElseFail()) + .toList(); + + assertEquals(this.persons, personsAfterRoundtrip); + } + } From 8a265276b43893d7f337db151c40b8a45f141219 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 26 Apr 2026 10:54:44 +0200 Subject: [PATCH 4/9] CAUSEWAY-3989: polish YamlUtils --- .../java/org/apache/causeway/commons/io/YamlUtils.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java index bfbb89a479a..99d2d8265e8 100644 --- a/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java +++ b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java @@ -54,7 +54,7 @@ @UtilityClass public class YamlUtils { - public static final String MULTI_DOC_DELIMITER = "---\n"; + private static final String MULTI_DOC_DELIMITER = "---"; @FunctionalInterface public interface YamlDumpCustomizer extends Consumer {} @@ -157,7 +157,7 @@ private static Stream linesToDocs(final Can lineStream) { var buffer = new ArrayList(); buffer.add(new StringBuilder()); for(var line : lineStream) { - if(line.equals("---")) { + if(line.equals(MULTI_DOC_DELIMITER)) { buffer.add(new StringBuilder()); continue; } @@ -233,14 +233,14 @@ public String toStringUtf8Customized( */ public String writeMultiDoc(@Nullable Iterable yamlDocuments) { return _NullSafe.stream(yamlDocuments) - .collect(Collectors.joining(YamlUtils.MULTI_DOC_DELIMITER)); + .collect(Collectors.joining(MULTI_DOC_DELIMITER + "\n")); } /** * Concatenates given {@link Stream} into a multi-doc YAML. */ public String writeMultiDoc(@Nullable Stream yamlDocumentStream) { return _NullSafe.stream(yamlDocumentStream) - .collect(Collectors.joining(YamlUtils.MULTI_DOC_DELIMITER)); + .collect(Collectors.joining(MULTI_DOC_DELIMITER + "\n")); } // -- CUSTOMIZERS From c2c45b70d13a1ea8741c017dbb1536778e259b23 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 26 Apr 2026 11:47:25 +0200 Subject: [PATCH 5/9] CAUSEWAY-3989: CommandDto multi-doc i/o support --- .../util/schema/CommandDtoJacksonSupport.java | 66 ++++++++++++++ .../applib/util/schema/CommandDtoUtils.java | 90 ++++++++++--------- 2 files changed, 114 insertions(+), 42 deletions(-) create mode 100644 api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoJacksonSupport.java diff --git a/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoJacksonSupport.java b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoJacksonSupport.java new file mode 100644 index 00000000000..24778d574c0 --- /dev/null +++ b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoJacksonSupport.java @@ -0,0 +1,66 @@ +/* + * 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.causeway.applib.util.schema; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.apache.causeway.commons.io.JsonUtils; +import org.apache.causeway.commons.io.JsonUtils.JacksonCustomizer; +import org.apache.causeway.schema.cmd.v2.ActionDto; +import org.apache.causeway.schema.cmd.v2.MemberDto; +import org.apache.causeway.schema.cmd.v2.PropertyDto; + +import lombok.experimental.UtilityClass; + +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.jsontype.NamedType; + +@UtilityClass +class CommandDtoJacksonSupport { + + JsonUtils.JacksonCustomizer yamlWriteCustomizer() { + return ((JacksonCustomizer) JsonUtils::jaxbAnnotationSupport) + .andThen((JacksonCustomizer) CommandDtoJacksonSupport::memberDtoSupport) + .andThen((JacksonCustomizer) JsonUtils::onlyIncludeNonNull) + ::accept; + } + JsonUtils.JacksonCustomizer yamlReadCustomizer() { + return ((JacksonCustomizer) JsonUtils::jaxbAnnotationSupport) + .andThen((JacksonCustomizer) CommandDtoJacksonSupport::memberDtoSupport) + ::accept; + } + + // -- HELPER + + // Mix-in to add type metadata to MemberDto + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") + private abstract class AbstractDtoMixIn {} + + private void memberDtoSupport(final MapperBuilder mb) { + // add mix-in so MemberDto carries @JsonTypeInfo without modifying source + mb.addMixIn(MemberDto.class, AbstractDtoMixIn.class); + // register concrete sub-types with logical names + mb.registerSubtypes(new NamedType(ActionDto.class, "ACT")); + mb.registerSubtypes(new NamedType(PropertyDto.class, "PROP")); + } + +} diff --git a/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java index f97b639aff0..e60b40ee2ba 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java @@ -20,9 +20,6 @@ import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.apache.causeway.applib.services.bookmark.Bookmark; import org.apache.causeway.commons.internal.base._Lazy; @@ -31,23 +28,16 @@ import org.apache.causeway.commons.io.DataSource; import org.apache.causeway.commons.io.DtoMapper; import org.apache.causeway.commons.io.JaxbUtils; -import org.apache.causeway.commons.io.JsonUtils; -import org.apache.causeway.commons.io.JsonUtils.JacksonCustomizer; import org.apache.causeway.commons.io.YamlUtils; import org.apache.causeway.schema.cmd.v2.ActionDto; import org.apache.causeway.schema.cmd.v2.CommandDto; import org.apache.causeway.schema.cmd.v2.MapDto; -import org.apache.causeway.schema.cmd.v2.MemberDto; import org.apache.causeway.schema.cmd.v2.ParamsDto; -import org.apache.causeway.schema.cmd.v2.PropertyDto; import org.apache.causeway.schema.common.v2.OidsDto; import org.apache.causeway.schema.common.v2.PeriodDto; import lombok.experimental.UtilityClass; -import tools.jackson.databind.cfg.MapperBuilder; -import tools.jackson.databind.jsontype.NamedType; - /** * @since 1.x {@index} */ @@ -93,30 +83,34 @@ public PeriodDto timingsFor(final CommandDto commandDto) { } public String getUserData(final CommandDto dto, final String key) { - if(dto == null || key == null) - return null; + if(dto == null || key == null) { + return null; + } return CommonDtoUtils.getMapValue(dto.getUserData(), key); } public void setUserData( final CommandDto dto, final String key, final String value) { - if(dto == null || key == null || _Strings.isNullOrEmpty(value)) - return; + if(dto == null || key == null || _Strings.isNullOrEmpty(value)) { + return; + } final MapDto userData = userDataFor(dto); CommonDtoUtils.putMapKeyValue(userData, key, value); } public void setUserData( final CommandDto dto, final String key, final Bookmark bookmark) { - if(dto == null || key == null || bookmark == null) - return; + if(dto == null || key == null || bookmark == null) { + return; + } setUserData(dto, key, bookmark.toString()); } public void clearUserData( final CommandDto dto, final String key) { - if(dto == null || key == null) - return; + if(dto == null || key == null) { + return; + } userDataFor(dto).getEntry().removeIf(x -> x.getKey().equals(key)); } @@ -131,39 +125,51 @@ private MapDto userDataFor(final CommandDto commandDto) { // -- YAML SUPPORT + /** + * Uses (regular) YAML-list format to represent a collection of {@link CommandDto} entries. + */ public String toYaml(final Iterable commandDtos) { + var yamlWriteCustomizer = CommandDtoJacksonSupport.yamlWriteCustomizer(); return YamlUtils.toStringUtf8( _NullSafe.stream(commandDtos) - .collect(Collectors.toList()), - ((JacksonCustomizer) JsonUtils::jaxbAnnotationSupport) - .andThen((JacksonCustomizer) CommandDtoUtils::memberDtoSupport) - .andThen((JacksonCustomizer) JsonUtils::onlyIncludeNonNull) - ::accept); + .toList(), + yamlWriteCustomizer); + } + + /** + * Uses multi-doc YAML format to represent a collection of {@link CommandDto} entries. + */ + public String toMultiDocYaml(final Iterable commandDtos) { + var yamlWriteCustomizer = CommandDtoJacksonSupport.yamlWriteCustomizer(); + return YamlUtils.writeMultiDoc(_NullSafe.stream(commandDtos) + .map(commandDto->YamlUtils.toStringUtf8(commandDto, yamlWriteCustomizer))); } + /** + * Parses from (regular) YAML-list format that represent a collection of {@link CommandDto} entries. + */ public List fromYaml(final DataSource commandDtosYaml) { - return YamlUtils.tryReadAsList(CommandDto.class, commandDtosYaml, - ((JacksonCustomizer) JsonUtils::jaxbAnnotationSupport) - .andThen((JacksonCustomizer) CommandDtoUtils::memberDtoSupport) - ::accept) + var yamlReadCustomizer = CommandDtoJacksonSupport.yamlReadCustomizer(); + return YamlUtils.tryReadAsList(CommandDto.class, commandDtosYaml, yamlReadCustomizer) .ifFailureFail() .getValue() .orElseGet(Collections::emptyList); } - - // Mix-in to add type metadata to MemberDto - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "type") - private abstract class AbstractDtoMixIn {} - - private void memberDtoSupport(final MapperBuilder mb) { - // add mix-in so MemberDto carries @JsonTypeInfo without modifying source - mb.addMixIn(MemberDto.class, AbstractDtoMixIn.class); - // register concrete sub-types with logical names - mb.registerSubtypes(new NamedType(ActionDto.class, "ACT")); - mb.registerSubtypes(new NamedType(PropertyDto.class, "PROP")); + + /** + * Parses from multi-doc YAML format that represent a collection of {@link CommandDto} entries. + */ + public List fromMultiDocYaml(final DataSource commandDtosYaml) { + var yamlReadCustomizer = CommandDtoJacksonSupport.yamlReadCustomizer(); + return YamlUtils.tryReadMultiDoc(commandDtosYaml) + .mapSuccessWhenPresent(stream->stream + .map(yaml->YamlUtils + .tryRead(CommandDto.class, yaml, yamlReadCustomizer) + .valueAsNonNullElseFail()) + .toList()) + .ifFailureFail() + .getValue() + .orElseGet(Collections::emptyList); } - + } From dfe38619f726de1b4baf44ec54aa8e99939d765d Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 26 Apr 2026 16:47:06 +0200 Subject: [PATCH 6/9] CAUSEWAY-3989: list parsing multi-doc support --- api/applib/src/main/java/module-info.java | 1 + .../util/schema/CommandDtoJacksonSupport.java | 188 +++++++++++++++++- .../applib/util/schema/CommandDtoUtils.java | 26 +-- .../apache/causeway/commons/io/YamlUtils.java | 31 ++- ...ommandDtoUtils_fromYaml_Approval_Test.java | 2 - .../schema/CommandDtoUtils_fromYaml_Test.java | 2 - .../CommandDtoUtils_toYaml_Approval_Test.java | 4 +- .../CommandDtoUtils_toYaml_fromYaml_Test.java | 4 +- .../schema/CommandDtoYamlRoundtripTest.java | 4 +- .../dom/replay/CommandExportManager.java | 2 +- .../dom/replay/CommandReplayManager.java | 5 +- 11 files changed, 219 insertions(+), 50 deletions(-) diff --git a/api/applib/src/main/java/module-info.java b/api/applib/src/main/java/module-info.java index bc34e9bf119..0962e777c65 100644 --- a/api/applib/src/main/java/module-info.java +++ b/api/applib/src/main/java/module-info.java @@ -146,6 +146,7 @@ requires spring.tx; requires org.slf4j; requires micrometer.observation; + requires com.fasterxml.jackson.annotation; // JAXB viewmodels opens org.apache.causeway.applib.annotation; diff --git a/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoJacksonSupport.java b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoJacksonSupport.java index 24778d574c0..a0d4400520b 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoJacksonSupport.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoJacksonSupport.java @@ -18,16 +18,32 @@ */ package org.apache.causeway.applib.util.schema; +import javax.xml.datatype.DatatypeConstants; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apache.causeway.commons.internal.base._Strings; import org.apache.causeway.commons.io.JsonUtils; import org.apache.causeway.commons.io.JsonUtils.JacksonCustomizer; import org.apache.causeway.schema.cmd.v2.ActionDto; import org.apache.causeway.schema.cmd.v2.MemberDto; import org.apache.causeway.schema.cmd.v2.PropertyDto; +import org.apache.causeway.schema.common.v2.ValueDto; import lombok.experimental.UtilityClass; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; import tools.jackson.databind.cfg.MapperBuilder; import tools.jackson.databind.jsontype.NamedType; @@ -36,14 +52,16 @@ class CommandDtoJacksonSupport { JsonUtils.JacksonCustomizer yamlWriteCustomizer() { return ((JacksonCustomizer) JsonUtils::jaxbAnnotationSupport) - .andThen((JacksonCustomizer) CommandDtoJacksonSupport::memberDtoSupport) - .andThen((JacksonCustomizer) JsonUtils::onlyIncludeNonNull) - ::accept; + .andThen((JacksonCustomizer) CommandDtoJacksonSupport::memberDtoSupport) + .andThen((JacksonCustomizer) CommandDtoJacksonSupport::valueDtoSupport) + .andThen((JacksonCustomizer) JsonUtils::onlyIncludeNonNull) + ::accept; } JsonUtils.JacksonCustomizer yamlReadCustomizer() { return ((JacksonCustomizer) JsonUtils::jaxbAnnotationSupport) - .andThen((JacksonCustomizer) CommandDtoJacksonSupport::memberDtoSupport) - ::accept; + .andThen((JacksonCustomizer) CommandDtoJacksonSupport::memberDtoSupport) + .andThen((JacksonCustomizer) CommandDtoJacksonSupport::valueDtoSupport) + ::accept; } // -- HELPER @@ -63,4 +81,164 @@ private void memberDtoSupport(final MapperBuilder mb) { mb.registerSubtypes(new NamedType(PropertyDto.class, "PROP")); } + // Mix-in to ignore unknown properties for ValueDto + @JsonIgnoreProperties(ignoreUnknown = true) + private abstract class AbstractValueDtoMixIn { + @JsonSerialize(using = LocalDateXmlGregorianCalendarSerializer.class) + abstract XMLGregorianCalendar getLocalDate(); + + @JsonDeserialize(using = LocalDateXmlGregorianCalendarDeserializer.class) + abstract void setLocalDate(XMLGregorianCalendar localDate); + + @JsonSerialize(using = LocalDateTimeXmlGregorianCalendarSerializer.class) + abstract XMLGregorianCalendar getLocalDateTime(); + + @JsonDeserialize(using = LocalDateTimeXmlGregorianCalendarDeserializer.class) + abstract void setLocalDateTime(XMLGregorianCalendar localDateTime); + + @JsonSerialize(using = LocalTimeXmlGregorianCalendarSerializer.class) + abstract XMLGregorianCalendar getLocalTime(); + + @JsonDeserialize(using = LocalTimeXmlGregorianCalendarDeserializer.class) + abstract void setLocalTime(XMLGregorianCalendar localTime); + } + + private void valueDtoSupport(final MapperBuilder mb) { + mb.addMixIn(ValueDto.class, AbstractValueDtoMixIn.class); + } + + private static final DatatypeFactory DATATYPE_FACTORY = datatypeFactory(); + + private static DatatypeFactory datatypeFactory() { + try { + return DatatypeFactory.newInstance(); + } catch (Exception ex) { + throw new RuntimeException("Failed to initialize DatatypeFactory", ex); + } + } + + private static final class LocalDateXmlGregorianCalendarSerializer + extends ValueSerializer { + @Override + public void serialize(XMLGregorianCalendar value, JsonGenerator gen, SerializationContext ctxt) + throws JacksonException { + if (value == null) { + gen.writeNull(); + return; + } + final XMLGregorianCalendar dateOnly = DATATYPE_FACTORY.newXMLGregorianCalendarDate( + value.getYear(), + value.getMonth(), + value.getDay(), + DatatypeConstants.FIELD_UNDEFINED); + gen.writeString(dateOnly.toXMLFormat()); + } + } + + private static final class LocalDateXmlGregorianCalendarDeserializer + extends ValueDeserializer { + @Override + public XMLGregorianCalendar deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + final String text = p.getValueAsString(); + if (_Strings.isNullOrEmpty(text)) { + return null; + } + final XMLGregorianCalendar parsed = DATATYPE_FACTORY.newXMLGregorianCalendar(text); + return DATATYPE_FACTORY.newXMLGregorianCalendarDate( + parsed.getYear(), + parsed.getMonth(), + parsed.getDay(), + DatatypeConstants.FIELD_UNDEFINED); + } + } + + private static final class LocalDateTimeXmlGregorianCalendarSerializer + extends ValueSerializer { + + @Override + public void serialize(XMLGregorianCalendar value, JsonGenerator gen, SerializationContext ctxt) + throws JacksonException { + if (value == null) { + gen.writeNull(); + return; + } + final XMLGregorianCalendar localDateTime = DATATYPE_FACTORY.newXMLGregorianCalendar( + value.getYear(), + value.getMonth(), + value.getDay(), + value.getHour(), + value.getMinute(), + value.getSecond(), + millisecondsOf(value), + DatatypeConstants.FIELD_UNDEFINED); + gen.writeString(localDateTime.toXMLFormat()); + } + } + + private static final class LocalDateTimeXmlGregorianCalendarDeserializer + extends ValueDeserializer { + + @Override + public XMLGregorianCalendar deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + final String text = p.getValueAsString(); + if (_Strings.isNullOrEmpty(text)) { + return null; + } + final XMLGregorianCalendar parsed = DATATYPE_FACTORY.newXMLGregorianCalendar(text); + return DATATYPE_FACTORY.newXMLGregorianCalendar( + parsed.getYear(), + parsed.getMonth(), + parsed.getDay(), + parsed.getHour(), + parsed.getMinute(), + parsed.getSecond(), + millisecondsOf(parsed), + DatatypeConstants.FIELD_UNDEFINED); + } + } + + private static final class LocalTimeXmlGregorianCalendarSerializer + extends ValueSerializer { + + @Override + public void serialize(XMLGregorianCalendar value, JsonGenerator gen, SerializationContext ctxt) + throws JacksonException { + if (value == null) { + gen.writeNull(); + return; + } + final XMLGregorianCalendar localTime = DATATYPE_FACTORY.newXMLGregorianCalendarTime( + value.getHour(), + value.getMinute(), + value.getSecond(), + millisecondsOf(value), + DatatypeConstants.FIELD_UNDEFINED); + gen.writeString(localTime.toXMLFormat()); + } + } + + private static final class LocalTimeXmlGregorianCalendarDeserializer + extends ValueDeserializer { + @Override + public XMLGregorianCalendar deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + final String text = p.getValueAsString(); + if (_Strings.isNullOrEmpty(text)) { + return null; + } + final XMLGregorianCalendar parsed = DATATYPE_FACTORY.newXMLGregorianCalendar(text); + return DATATYPE_FACTORY.newXMLGregorianCalendarTime( + parsed.getHour(), + parsed.getMinute(), + parsed.getSecond(), + millisecondsOf(parsed), + DatatypeConstants.FIELD_UNDEFINED); + } + } + + private static int millisecondsOf(final XMLGregorianCalendar value) { + final int millis = value.getMillisecond(); + return millis == DatatypeConstants.FIELD_UNDEFINED + ? DatatypeConstants.FIELD_UNDEFINED + : millis; + } } diff --git a/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java index e60b40ee2ba..eb6b48bf49b 100644 --- a/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java +++ b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoUtils.java @@ -146,30 +146,14 @@ public String toMultiDocYaml(final Iterable commandDtos) { } /** - * Parses from (regular) YAML-list format that represent a collection of {@link CommandDto} entries. + * Either parses from (regular) YAML-list format or from multi-doc YAML format, + * any representing a collection of {@link CommandDto} entries. */ public List fromYaml(final DataSource commandDtosYaml) { var yamlReadCustomizer = CommandDtoJacksonSupport.yamlReadCustomizer(); - return YamlUtils.tryReadAsList(CommandDto.class, commandDtosYaml, yamlReadCustomizer) - .ifFailureFail() - .getValue() - .orElseGet(Collections::emptyList); - } - - /** - * Parses from multi-doc YAML format that represent a collection of {@link CommandDto} entries. - */ - public List fromMultiDocYaml(final DataSource commandDtosYaml) { - var yamlReadCustomizer = CommandDtoJacksonSupport.yamlReadCustomizer(); - return YamlUtils.tryReadMultiDoc(commandDtosYaml) - .mapSuccessWhenPresent(stream->stream - .map(yaml->YamlUtils - .tryRead(CommandDto.class, yaml, yamlReadCustomizer) - .valueAsNonNullElseFail()) - .toList()) - .ifFailureFail() - .getValue() - .orElseGet(Collections::emptyList); + return YamlUtils.tryReadAsList(CommandDto.class, commandDtosYaml, yamlReadCustomizer) + .getValue() + .orElseGet(Collections::emptyList); } } diff --git a/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java index 99d2d8265e8..c7f33cc6c6f 100644 --- a/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java +++ b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java @@ -41,6 +41,7 @@ import lombok.SneakyThrows; import lombok.experimental.UtilityClass; +import tools.jackson.databind.MappingIterator; import tools.jackson.dataformat.yaml.YAMLFactory; import tools.jackson.dataformat.yaml.YAMLFactoryBuilder; import tools.jackson.dataformat.yaml.YAMLMapper; @@ -89,16 +90,14 @@ public Try tryRead( /** * Tries to deserialize YAML content from given {@link DataSource} into a {@link List} * with given {@code elementType}. + * + *

Either parses (regular) YAML-list format or multi-doc YAML format. */ public Try> tryReadAsList( final @NonNull Class elementType, final @NonNull DataSource source, final JsonUtils.JacksonCustomizer ... customizers) { - return source.tryReadAll((final InputStream is) -> Try.call(()->{ - var mapper = createJacksonReader(Optional.empty(), customizers); - var collectionType = mapper.getTypeFactory().constructCollectionType(List.class, elementType); - return mapper.readValue(is, collectionType); - })); + return tryReadAsListCustomized(elementType, source, null, customizers); } /** @@ -128,17 +127,29 @@ public Try tryReadCustomized( /** * Tries to deserialize YAML content from given {@link DataSource} into a {@link List} - * with given {@code elementType}. + * with given {@code elementType}. + * + *

Either parses (regular) YAML-list format or multi-doc YAML format. */ public Try> tryReadAsListCustomized( final @NonNull Class elementType, final @NonNull DataSource source, - final @NonNull YamlLoadCustomizer loadCustomizer, + final @Nullable YamlLoadCustomizer loadCustomizer, final JsonUtils.JacksonCustomizer ... customizers) { return source.tryReadAll((final InputStream is) -> Try.call(()->{ - var mapper = createJacksonReader(Optional.of(loadCustomizer), customizers); - var collectionType = mapper.getTypeFactory().constructCollectionType(List.class, elementType); - return mapper.readValue(is, collectionType); + var mapper = createJacksonReader(Optional.ofNullable(loadCustomizer), customizers); + final MappingIterator documentReader = mapper.readerFor(elementType).readValues(is); + final List elements = new ArrayList<>(); + while (documentReader.hasNextValue()) { + final T next = documentReader.nextValue(); + if (next != null) { + elements.add(next); + } + } + return elements; // no need wrap unmodifiable, as callers can do what ever they want with the list +//former code without support for multi-doc format... +// var collectionType = mapper.getTypeFactory().constructCollectionType(List.class, elementType); +// return mapper.readValue(is, collectionType); })); } diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java index 812a74ed974..26ae6cb2493 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java @@ -23,7 +23,6 @@ import java.util.List; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.util.StreamUtils; @@ -34,7 +33,6 @@ import org.apache.causeway.schema.cmd.v2.ParamDto; import org.apache.causeway.schema.common.v2.ValueType; -@Disabled //FIXME class CommandDtoUtils_fromYaml_Approval_Test { @Test diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java index 4bf98dfa899..9faa5a3c43b 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java @@ -23,7 +23,6 @@ import java.util.List; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.util.StreamUtils; @@ -36,7 +35,6 @@ import org.apache.causeway.schema.common.v2.InteractionType; import org.apache.causeway.schema.common.v2.ValueType; -@Disabled //FIXME class CommandDtoUtils_fromYaml_Test { @Test diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java index 583a1784caa..9bfbb6fb7cd 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java @@ -52,7 +52,7 @@ class CommandDtoUtils_toYaml_Approval_Test { @Test void marshals_all_date_time_datatypes() { withDefaultTimeZone("UTC", () -> { - String yaml = CommandDtoUtils.toYaml(List.of(commandWithAllDateTimeParams())); + String yaml = CommandDtoUtils.toMultiDocYaml(List.of(commandWithAllDateTimeParams())); Approvals.verify(yaml, ApprovalsOptions.defaultOptions()); }); } @@ -60,7 +60,7 @@ void marshals_all_date_time_datatypes() { @Test void marshals_all_date_time_datatypes_when_default_timezone_is_cest() { withDefaultTimeZone("Europe/Paris", () -> { - String yaml = CommandDtoUtils.toYaml(List.of(commandWithAllDateTimeParams())); + String yaml = CommandDtoUtils.toMultiDocYaml(List.of(commandWithAllDateTimeParams())); try { Assertions.assertThat(yaml).isEqualTo(readApprovalSnapshot()); } catch (IOException ex) { diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java index e7517a0ed54..f4b68ec1197 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java @@ -26,7 +26,6 @@ import javax.xml.datatype.XMLGregorianCalendar; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.apache.causeway.commons.io.DataSource; @@ -39,7 +38,6 @@ import org.apache.causeway.schema.common.v2.OidsDto; import org.apache.causeway.schema.common.v2.ValueType; -@Disabled //FIXME class CommandDtoUtils_toYaml_fromYaml_Test { @Test @@ -94,7 +92,7 @@ void localDate_roundtrips_as_date_only_without_timezone() throws Exception { action.setParameters(params); command.setMember(action); - String yaml = CommandDtoUtils.toYaml(List.of(command)); + String yaml = CommandDtoUtils.toMultiDocYaml(List.of(command)); List roundtripped = CommandDtoUtils.fromYaml(DataSource.ofStringUtf8(yaml)); XMLGregorianCalendar roundtrippedDate = ((ActionDto) roundtripped.get(0).getMember()) diff --git a/core/mmtest/src/test/java/org/apache/causeway/mmtest/schema/CommandDtoYamlRoundtripTest.java b/core/mmtest/src/test/java/org/apache/causeway/mmtest/schema/CommandDtoYamlRoundtripTest.java index 382bff2f850..83243cfd3b2 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/mmtest/schema/CommandDtoYamlRoundtripTest.java +++ b/core/mmtest/src/test/java/org/apache/causeway/mmtest/schema/CommandDtoYamlRoundtripTest.java @@ -43,9 +43,9 @@ class CommandDtoYamlRoundtripTest { @Test void test() { - var yaml = CommandDtoUtils.toYaml(List.of(commandDtoSample(), commandDtoSample())); + var yaml = CommandDtoUtils.toMultiDocYaml(List.of(commandDtoSample(), commandDtoSample())); var afterRoundtrip = CommandDtoUtils.fromYaml(DataSource.ofStringUtf8(yaml)); - assertEquals(yaml, CommandDtoUtils.toYaml(afterRoundtrip)); + assertEquals(yaml, CommandDtoUtils.toMultiDocYaml(afterRoundtrip)); } private CommandDto commandDtoSample() { diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java index df351356b5e..878f545f81d 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java @@ -102,7 +102,7 @@ public Blob exportSelected( .sorted() .toList(); - var yaml = CommandDtoUtils.toYaml( + var yaml = CommandDtoUtils.toMultiDocYaml( selectedCommandLogEntries.stream() .filter(entry->!ReplayState.isExported(entry.getReplayState())) .map(CommandLogEntry::getCommandDto) diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java index 0868182867c..f734e487f96 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java @@ -114,8 +114,9 @@ public CommandReplayManager replayOrRetrySelected(final List .toList(); for(var replayableCommand : replayables) { var tryReplayOrRetry = replayableCommand.tryReplayOrRetry(); // filtered on its own responsibility - if(tryReplayOrRetry.isFailure()) - return this; // stop further execution + if(tryReplayOrRetry.isFailure()) { + return this; // stop further execution + } } return this; } From a1ec1d73df1a3aa940edfe85bca068d9e487bc43 Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 26 Apr 2026 16:53:52 +0200 Subject: [PATCH 7/9] CAUSEWAY-3989: relocate and improve roundtrip test --- .../util}/schema/CommandDtoYamlRoundtripTest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) rename core/mmtest/src/test/java/org/apache/causeway/{mmtest => applib/util}/schema/CommandDtoYamlRoundtripTest.java (91%) diff --git a/core/mmtest/src/test/java/org/apache/causeway/mmtest/schema/CommandDtoYamlRoundtripTest.java b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoYamlRoundtripTest.java similarity index 91% rename from core/mmtest/src/test/java/org/apache/causeway/mmtest/schema/CommandDtoYamlRoundtripTest.java rename to core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoYamlRoundtripTest.java index 83243cfd3b2..f29fa262396 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/mmtest/schema/CommandDtoYamlRoundtripTest.java +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoYamlRoundtripTest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.causeway.mmtest.schema; +package org.apache.causeway.applib.util.schema; import java.sql.Timestamp; import java.time.Instant; @@ -42,7 +42,14 @@ class CommandDtoYamlRoundtripTest { @Test - void test() { + void list() { + var yaml = CommandDtoUtils.toYaml(List.of(commandDtoSample(), commandDtoSample())); + var afterRoundtrip = CommandDtoUtils.fromYaml(DataSource.ofStringUtf8(yaml)); + assertEquals(yaml, CommandDtoUtils.toYaml(afterRoundtrip)); + } + + @Test + void multiDoc() { var yaml = CommandDtoUtils.toMultiDocYaml(List.of(commandDtoSample(), commandDtoSample())); var afterRoundtrip = CommandDtoUtils.fromYaml(DataSource.ofStringUtf8(yaml)); assertEquals(yaml, CommandDtoUtils.toMultiDocYaml(afterRoundtrip)); From f6a9cd26917d4959acf5724798a2cc871d5e5e2b Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 26 Apr 2026 16:58:22 +0200 Subject: [PATCH 8/9] CAUSEWAY-3989: aprrovals: yaml fields in alphabetic order --- ...shals_all_date_time_datatypes.approved.txt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt index 0d7479ee511..d12adc45c08 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt @@ -4,28 +4,28 @@ interactionId: "approval-datetime-marshalling" username: "approval-user" targets: oid: - - type: "demo.Customer" - id: "123" + - id: "123" + type: "demo.Customer" member: ! parameters: parameter: - localDate: "2026-07-01" - type: "localDate" name: "Local Date" + type: "localDate" - localDateTime: "2026-07-01T10:15:30" - type: "localDateTime" name: "Local Date Time" + type: "localDateTime" - localTime: "10:15:30" - type: "localTime" name: "Local Time" - - offsetDateTime: "2026-07-01T08:15:30.000+00:00" + type: "localTime" + - name: "Offset Date Time" + offsetDateTime: "2026-07-01T08:15:30.000+00:00" type: "offsetDateTime" - name: "Offset Date Time" - - offsetTime: "1970-01-01T08:15:30.000+00:00" + - name: "Offset Time" + offsetTime: "1970-01-01T08:15:30.000+00:00" type: "offsetTime" - name: "Offset Time" - - zonedDateTime: "2026-07-01T08:15:30.000+00:00" + - name: "Zoned Date Time" type: "zonedDateTime" - name: "Zoned Date Time" - logicalMemberIdentifier: "demo.Customer#allDateTimeTypes" + zonedDateTime: "2026-07-01T08:15:30.000+00:00" interactionType: "action_invocation" + logicalMemberIdentifier: "demo.Customer#allDateTimeTypes" From b1858fbcb3aa04b114aa3de059c2b0857ed585fa Mon Sep 17 00:00:00 2001 From: andi-huber Date: Sun, 26 Apr 2026 17:41:32 +0200 Subject: [PATCH 9/9] CAUSEWAY-3989: test approvals --- .../util/schema/CommandDtoUtils_toYaml_Approval_Test.java | 7 +++++-- ...oval_Test.marshals_all_date_time_datatypes.approved.txt | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java index 9bfbb6fb7cd..e77583704d5 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java @@ -29,7 +29,6 @@ import org.approvaltests.Approvals; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.util.StreamUtils; @@ -44,7 +43,11 @@ import org.apache.causeway.schema.common.v2.ValueType; import org.apache.causeway.testing.integtestsupport.applib.ApprovalsOptions; -@Disabled //FIXME +/** + * @implNote potential issues with ISO-8601 format equivalence of + * {@code 2026-07-01T08:15:30.000+00:00 == 2026-07-01T08:15:30.000Z}, + * tests might fail, because tests expect one or the other in a hardcoded way + */ class CommandDtoUtils_toYaml_Approval_Test { private static final DatatypeFactory DATATYPE_FACTORY = datatypeFactory(); diff --git a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt index d12adc45c08..4b844c701fb 100644 --- a/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt @@ -19,13 +19,13 @@ member: ! name: "Local Time" type: "localTime" - name: "Offset Date Time" - offsetDateTime: "2026-07-01T08:15:30.000+00:00" + offsetDateTime: "2026-07-01T08:15:30.000Z" type: "offsetDateTime" - name: "Offset Time" - offsetTime: "1970-01-01T08:15:30.000+00:00" + offsetTime: "1970-01-01T08:15:30.000Z" type: "offsetTime" - name: "Zoned Date Time" type: "zonedDateTime" - zonedDateTime: "2026-07-01T08:15:30.000+00:00" + zonedDateTime: "2026-07-01T08:15:30.000Z" interactionType: "action_invocation" logicalMemberIdentifier: "demo.Customer#allDateTimeTypes"