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/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 new file mode 100644 index 00000000000..a0d4400520b --- /dev/null +++ b/api/applib/src/main/java/org/apache/causeway/applib/util/schema/CommandDtoJacksonSupport.java @@ -0,0 +1,244 @@ +/* + * 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 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; + +@UtilityClass +class CommandDtoJacksonSupport { + + JsonUtils.JacksonCustomizer yamlWriteCustomizer() { + return ((JacksonCustomizer) JsonUtils::jaxbAnnotationSupport) + .andThen((JacksonCustomizer) CommandDtoJacksonSupport::memberDtoSupport) + .andThen((JacksonCustomizer) CommandDtoJacksonSupport::valueDtoSupport) + .andThen((JacksonCustomizer) JsonUtils::onlyIncludeNonNull) + ::accept; + } + JsonUtils.JacksonCustomizer yamlReadCustomizer() { + return ((JacksonCustomizer) JsonUtils::jaxbAnnotationSupport) + .andThen((JacksonCustomizer) CommandDtoJacksonSupport::memberDtoSupport) + .andThen((JacksonCustomizer) CommandDtoJacksonSupport::valueDtoSupport) + ::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")); + } + + // 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 f97b639aff0..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 @@ -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,35 @@ 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); } - - public List fromYaml(final DataSource commandDtosYaml) { - return YamlUtils.tryReadAsList(CommandDto.class, commandDtosYaml, - ((JacksonCustomizer) JsonUtils::jaxbAnnotationSupport) - .andThen((JacksonCustomizer) CommandDtoUtils::memberDtoSupport) - ::accept) - .ifFailureFail() - .getValue() - .orElseGet(Collections::emptyList); + + /** + * 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))); } - // 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")); + /** + * 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) + .getValue() + .orElseGet(Collections::emptyList); } - + } 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/main/java/org/apache/causeway/commons/io/YamlUtils.java b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java index ce0c4ec5159..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 @@ -19,9 +19,12 @@ 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; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -31,11 +34,14 @@ 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; 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; @@ -49,6 +55,8 @@ @UtilityClass public class YamlUtils { + private static final String MULTI_DOC_DELIMITER = "---"; + @FunctionalInterface public interface YamlDumpCustomizer extends Consumer {} @FunctionalInterface @@ -82,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); } /** @@ -121,20 +127,58 @@ 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); })); } - + + /** + * 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(MULTI_DOC_DELIMITER)) { + 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 /** @@ -144,7 +188,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 +217,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 +230,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 +239,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(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(MULTI_DOC_DELIMITER + "\n")); + } + // -- CUSTOMIZERS /** 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..26ae6cb2493 --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java @@ -0,0 +1,103 @@ +/* + * 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.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; + +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..9faa5a3c43b --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java @@ -0,0 +1,162 @@ +/* + * 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.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; + +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..e77583704d5 --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java @@ -0,0 +1,172 @@ +/* + * 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.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; + +/** + * @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(); + + @Test + void marshals_all_date_time_datatypes() { + withDefaultTimeZone("UTC", () -> { + String yaml = CommandDtoUtils.toMultiDocYaml(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.toMultiDocYaml(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..4b844c701fb --- /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: + - id: "123" + type: "demo.Customer" +member: ! + parameters: + parameter: + - localDate: "2026-07-01" + name: "Local Date" + type: "localDate" + - localDateTime: "2026-07-01T10:15:30" + name: "Local Date Time" + type: "localDateTime" + - localTime: "10:15:30" + name: "Local Time" + type: "localTime" + - name: "Offset Date Time" + offsetDateTime: "2026-07-01T08:15:30.000Z" + type: "offsetDateTime" + - name: "Offset Time" + offsetTime: "1970-01-01T08:15:30.000Z" + type: "offsetTime" + - name: "Zoned Date Time" + type: "zonedDateTime" + zonedDateTime: "2026-07-01T08:15:30.000Z" + interactionType: "action_invocation" + logicalMemberIdentifier: "demo.Customer#allDateTimeTypes" 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..f4b68ec1197 --- /dev/null +++ b/core/mmtest/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java @@ -0,0 +1,133 @@ +/* + * 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.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; + +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.toMultiDocYaml(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/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 382bff2f850..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,11 +42,18 @@ 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)); + } private CommandDto commandDtoSample() { 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 64% 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 f15406b08bc..8596d72fb58 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 @@ -18,21 +18,30 @@ */ package org.apache.causeway.commons.io; +import java.util.List; + import org.approvaltests.Approvals; +import org.approvaltests.reporters.DiffReporter; +import org.approvaltests.reporters.UseReporter; 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 { private Person person; + private List persons; @BeforeEach void setup() { this.person = _TestDomain.samplePerson(); + this.persons = List.of( + person, + _TestDomain.samplePerson("fred")); } @Test @@ -72,5 +81,35 @@ void parseRecord() { .valueAsNonNullElseFail(); assertEquals(this.person, person); } + + @Test + @UseReporter(DiffReporter.class) + void toStringUtf8ForList_yamlList() { + var yaml = YamlUtils.toStringUtf8(persons); + Approvals.verify(yaml, ApprovalsOptions.defaultOptions()); + } + @Test + @UseReporter(DiffReporter.class) + void toStringUtf8ForList_multiDoc() { + var yaml = YamlUtils.writeMultiDoc( + persons.stream() + .map(YamlUtils::toStringUtf8)); + 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); + } + } 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/core/mmtest/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 new file mode 100644 index 00000000000..3cac21f215c --- /dev/null +++ b/core/mmtest/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/core/mmtest/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 new file mode 100644 index 00000000000..935533db17e --- /dev/null +++ b/core/mmtest/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/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 96% 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 index 1181e75ce8f..084396ef686 100644 --- 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 @@ -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( 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; }