You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/04/24 09:46:18 UTC
(causeway) branch CAUSEWAY-3989 updated (c989528ae36 -> 187f9ca14e6)
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a change to branch CAUSEWAY-3989
in repository https://gitbox.apache.org/repos/asf/causeway.git
discard c989528ae36 CAUSEWAY-3989: [v2] adds choices to hopefully avoid MM validation issue
discard 966c1846623 CAUSEWAY-3989: [v2] fixes (?) names queries
discard 9d12f794a76 CAUSEWAY-3989: [v2] declares mixins
discard 6ff5ce061d3 CAUSEWAY-3989: [v2] fixes (?) query for CommandLogEntry
discard 7d2b6cff4f6 CAUSEWAY-3989: [v2] improves unit test
discard ac6483e1089 CAUSEWAY-3989: [v2] fixes existing unit tests
discard e074fa062af CAUSEWAY-3989: [v2] fixes compile issue
discard f644e885805 CAUSEWAY-3989: [v2] minor refactoring
discard 09f9a9fcc8d CAUSEWAY-3989: [v2] adds unit test for fromYaml for all datatypes
discard 0e5e7e2407e CAUSEWAY-3989: [v2] fixes marshalling of all LocalDateTime and LocalTime datatypes
discard a4ff1ec7867 CAUSEWAY-3989: [v2] adds approval test to check marshalling of all date/time datatypes
discard 98c06bce31a CAUSEWAY-3989: [v2] fixes localdate marshalling
discard fbfd6048e8f CAUSEWAY-3989: [v2] adds test to demo bug
discard a915f993d5d CAUSEWAY-3989: [v2] uses 'since' in CommandReplayManager. Resurrects (?) openCommandLogEntry
discard fd4fe8cc0b2 CAUSEWAY-3989: [v2] fixes MM
discard 81d732198b8 CAUSEWAY-3989: [v2] fixes marshalling and other small stuff
discard 75b015b78ac CAUSEWAY-3989: [v2] adds a 'since' to CommandReplayManager; fixes for previous/next and layouts
discard c641cb7a141 CAUSEWAY-3989: [v2] saveForReplay sets executeIn to FOREGROUND always
discard 66fa318aa75 CAUSEWAY-3989: [v2] polishing
discard 2e8ae1e3e16 CAUSEWAY-3989: [v2] adds additional assertions
discard 7b44bf274c5 CAUSEWAY-3989: [v2] tests now pass
discard cd85816eeb9 CAUSEWAY-3989: [v2] reworks
discard c1ee3d6de3c CAUSEWAY-3989: [v2] adds tests
discard 842337d159d CAUSEWAY-3989: [v2] adds mixins
discard 04eca1de958 CAUSEWAY-3989: [v2] fixes compile error
discard 68745fded0d CAUSEWAY-3989: [v2] adds baseline to CommandExportManager
discard 2e4baf28bfe CAUSEWAY-3989: [v2] prevent ReplayContext from leaking into MM in a couple more places
discard 404707ec97d CAUSEWAY-3989: [v2] ensures ReplayContext is not picked up as a MM class from ReplayableCommand
new 4f624297612 CAUSEWAY-3989: [v2] ensures ReplayContext is not picked up as a MM class from ReplayableCommand
new feb7dfc7f5d CAUSEWAY-3989 : fixes compile issue
new 187f9ca14e6 CAUSEWAY-3989: [v2] adds test to demo bug
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (c989528ae36)
\
N -- N -- N refs/heads/CAUSEWAY-3989 (187f9ca14e6)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../commandlog/applib/integtest/CommandLog_IntegTestAbstract.java | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
(causeway) 03/03: CAUSEWAY-3989: [v2] adds test to demo bug
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-3989
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 187f9ca14e62be08b7355328377e0187dde32bc1
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Wed Apr 22 11:28:32 2026 +0100
CAUSEWAY-3989: [v2] adds test to demo bug
CAUSEWAY-3989: [v2] fixes localdate marshalling
CAUSEWAY-3989: [v2] adds approval test to check marshalling of all date/time datatypes
CAUSEWAY-3989: [v2] fixes marshalling of all LocalDateTime and LocalTime datatypes
CAUSEWAY-3989: [v2] adds unit test for fromYaml for all datatypes
CAUSEWAY-3989: [v2] minor refactoring
CAUSEWAY-3989: [v2] fixes compile issue
CAUSEWAY-3989: [v2] fixes existing unit tests
CAUSEWAY-3989: [v2] improves unit test
---
api/applib/pom.xml | 12 ++
.../applib/util/schema/CommandDtoUtils.java | 183 ++++++++++++++++++++-
.../CommandDtoUtils_fromYaml_Approval_Test.java | 105 ++++++++++++
...omYaml_Test.commands-with-collection-param.yaml | 6 +-
..._fromYaml_Test.commands-with-scalar-params.yaml | 2 +-
.../util/schema/CommandDtoUtils_fromYaml_Test.java | 16 ++
.../CommandDtoUtils_toYaml_Approval_Test.java | 165 +++++++++++++++++++
...t.marshals_all_date_time_datatypes.approved.txt | 31 ++++
.../CommandDtoUtils_toYaml_fromYaml_Test.java | 132 +++++++++++++++
.../integtest/CommandLog_IntegTestAbstract.java | 3 +
10 files changed, 650 insertions(+), 5 deletions(-)
diff --git a/api/applib/pom.xml b/api/applib/pom.xml
index fad7c62b386..483863a6218 100644
--- a/api/applib/pom.xml
+++ b/api/applib/pom.xml
@@ -107,6 +107,18 @@
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.approvaltests</groupId>
+ <artifactId>approvaltests</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <!-- required by com.approvaltests:test (version managed by spring boot) -->
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <scope>test</scope>
+ </dependency>
+
</dependencies>
</project>
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 8fec5928b9d..997a100625a 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
@@ -18,10 +18,15 @@
*/
package org.apache.causeway.applib.util.schema;
+import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
+import javax.xml.datatype.DatatypeConstants;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.commons.internal.base._Lazy;
import org.apache.causeway.commons.internal.base._NullSafe;
@@ -44,7 +49,15 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import lombok.experimental.UtilityClass;
@@ -178,12 +191,180 @@ private abstract class AbstractDtoMixIn {}
// Mix-in to ignore unknown properties for ValueDto
@JsonIgnoreProperties(ignoreUnknown = true)
- private abstract class AbstractValueDtoMixIn {}
+ 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 ObjectMapper 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 JsonSerializer<XMLGregorianCalendar> {
+
+ @Override
+ public void serialize(
+ final XMLGregorianCalendar value,
+ final JsonGenerator gen,
+ final SerializerProvider serializers) throws IOException {
+ 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 JsonDeserializer<XMLGregorianCalendar> {
+
+ @Override
+ public XMLGregorianCalendar deserialize(
+ final JsonParser p,
+ final DeserializationContext ctxt) throws IOException {
+ 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 JsonSerializer<XMLGregorianCalendar> {
+
+ @Override
+ public void serialize(
+ final XMLGregorianCalendar value,
+ final JsonGenerator gen,
+ final SerializerProvider serializers) throws IOException {
+ 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 JsonDeserializer<XMLGregorianCalendar> {
+
+ @Override
+ public XMLGregorianCalendar deserialize(
+ final JsonParser p,
+ final DeserializationContext ctxt) throws IOException {
+ 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 JsonSerializer<XMLGregorianCalendar> {
+
+ @Override
+ public void serialize(
+ final XMLGregorianCalendar value,
+ final JsonGenerator gen,
+ final SerializerProvider serializers) throws IOException {
+ 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 JsonDeserializer<XMLGregorianCalendar> {
+
+ @Override
+ public XMLGregorianCalendar deserialize(
+ final JsonParser p,
+ final DeserializationContext ctxt) throws IOException {
+ 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;
+ }
+
private void memberDtoSupport(final ObjectMapper mb) {
// add mix-in so MemberDto carries @JsonTypeInfo without modifying source
mb.addMixIn(MemberDto.class, AbstractDtoMixIn.class);
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Approval_Test.java
new file mode 100644
index 00000000000..f3f8bded4c8
--- /dev/null
+++ b/api/applib/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 javax.xml.datatype.DatatypeConstants;
+
+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<CommandDto> 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<ParamDto> 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/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml
index f2b7b6b3b5f..4175c718be9 100644
--- a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml
+++ b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml
@@ -73,13 +73,13 @@
type: "collection"
"null": false
name: "Lease Item Types"
- - localDate: "2026-06-30T22:00:00.000+00:00"
+ - localDate: "2026-06-30"
type: "localDate"
name: "Invoice Due Date"
- - localDate: "2026-06-30T22:00:00.000+00:00"
+ - localDate: "2026-06-30"
type: "localDate"
name: "Start Due Date"
- - localDate: "2026-07-01T22:00:00.000+00:00"
+ - localDate: "2026-07-01"
type: "localDate"
name: "Next Due Date"
- type: "string"
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml
index d88b314b5ec..81222d72ad7 100644
--- a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml
+++ b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml
@@ -22,7 +22,7 @@
member: !<ACT>
parameters:
parameter:
- - localDate: "2026-04-20T22:00:00.000+00:00"
+ - localDate: "2026-04-20"
type: "localDate"
name: "Invoice Date"
- boolean: false
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java
index 2608b975d59..bf46b304166 100644
--- a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java
+++ b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java
@@ -68,6 +68,7 @@ public void scalarValues() throws IOException {
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");
@@ -113,6 +114,21 @@ public void collectionValues() throws IOException {
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);
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java
new file mode 100644
index 00000000000..72e35d59914
--- /dev/null
+++ b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.java
@@ -0,0 +1,165 @@
+/*
+ * 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.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.springframework.util.StreamUtils;
+
+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);
+ });
+ }
+
+ @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/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_Approval_Test.marshals_all_date_time_datatypes.approved.txt b/api/applib/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..22481b6fa3c
--- /dev/null
+++ b/api/applib/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: !<ACT>
+ 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/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java
new file mode 100644
index 00000000000..6661093eb53
--- /dev/null
+++ b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_toYaml_fromYaml_Test.java
@@ -0,0 +1,132 @@
+/*
+ * 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.toYaml(List.of(command));
+ List<CommandDto> 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/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
index 727d5042d98..c33dbb87666 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
@@ -277,6 +277,9 @@ void roundtrip_CLE_bookmarks() {
void test_all_the_repository_methods() {
// given
+ final var baseline = clockService.getClock().nowAsJavaSqlTimestamp();
+
+
sudoService.run(InteractionContext.switchUser(UserMemento.builder().name("user-1").build()), () -> {
wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act();
});
(causeway) 02/03: CAUSEWAY-3989 : fixes compile issue
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-3989
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit feb7dfc7f5d606079903da611df2f2f62cb051ba
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Fri Apr 24 10:42:46 2026 +0100
CAUSEWAY-3989 : fixes compile issue
---
.../commandlog/applib/integtest/CommandLog_IntegTestAbstract.java | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
index 449cef754ec..727d5042d98 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
@@ -18,6 +18,7 @@
*/
package org.apache.causeway.extensions.commandlog.applib.integtest;
+import java.sql.Timestamp;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@@ -367,9 +368,10 @@ void test_all_the_repository_methods() {
val username1 = commandTarget1User1.getUsername();
val from = commandTarget1User1.getStartedAt().toLocalDateTime().toLocalDate();
val to = from.plusDays(1);
+ final var timestamp = commandTarget1User1.getTimestamp();
// when
- List<? extends CommandLogEntry> notYetReplayed = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(baseline);
+ List<? extends CommandLogEntry> notYetReplayed = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(timestamp);
// then
Assertions.assertThat(notYetReplayed).isEmpty();
@@ -382,7 +384,7 @@ void test_all_the_repository_methods() {
commandTarget1User1.setReplayState(ReplayState.PENDING);
// when
- List<? extends CommandLogEntry> notYetReplayed2 = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(baseline);
+ List<? extends CommandLogEntry> notYetReplayed2 = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(timestamp);
// then
Assertions.assertThat(notYetReplayed2).hasSize(1);
(causeway) 01/03: CAUSEWAY-3989: [v2] ensures ReplayContext is not picked up as a MM class from ReplayableCommand
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-3989
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 4f624297612ed51b30f84ee6d1a0395053624651
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Mon Apr 20 10:45:35 2026 +0100
CAUSEWAY-3989: [v2] ensures ReplayContext is not picked up as a MM class from ReplayableCommand
CAUSEWAY-3989: [v2] prevent ReplayContext from leaking into MM in a couple more places
CAUSEWAY-3989: [v2] adds baseline to CommandExportManager
and removes actions to delete commands
CAUSEWAY-3989: [v2] fixes compile error
CAUSEWAY-3989: [v2] adds mixins
CAUSEWAY-3989: [v2] adds tests
CAUSEWAY-3989: [v2] reworks
CAUSEWAY-3989: [v2] tests now pass
CAUSEWAY-3989: [v2] adds additional assertions
CAUSEWAY-3989: [v2] polishing
CAUSEWAY-3989: [v2] saveForReplay sets executeIn to FOREGROUND always
CAUSEWAY-3989: [v2] adds a 'since' to CommandReplayManager; fixes for previous/next and layouts
CAUSEWAY-3989: [v2] fixes marshalling and other small stuff
CAUSEWAY-3989: [v2] fixes MM
CAUSEWAY-3989: [v2] uses 'since' in CommandReplayManager. Resurrects (?) openCommandLogEntry
CAUSEWAY-3989: [v2] fixes (?) query for CommandLogEntry
CAUSEWAY-3989: [v2] declares mixins
CAUSEWAY-3989: [v2] fixes (?) names queries
CAUSEWAY-3989: [v2] adds choices to hopefully avoid MM validation issue
---
.../applib/util/schema/CommandDtoUtils.java | 16 +-
.../applib/util/schema/CommandDtoUtils_Test.java | 10 +
...omYaml_Test.commands-with-collection-param.yaml | 92 ++++++
..._fromYaml_Test.commands-with-scalar-params.yaml | 32 ++
.../util/schema/CommandDtoUtils_fromYaml_Test.java | 133 ++++++++
.../applib/CausewayModuleExtCommandLogApplib.java | 29 +-
.../commandlog/applib/app/CommandLogMenu.java | 38 ++-
.../commandlog/applib/dom/CommandLogEntry.java | 5 +-
.../applib/dom/CommandLogEntryRepository.java | 25 +-
.../dom/CommandLogEntryRepositoryAbstract.java | 64 ++--
.../commandlog/applib/dom/ReplayState.java | 24 +-
...ExportManager#exported.columnOrder.fallback.txt | 6 +
...Manager#notYetExported.columnOrder.fallback.txt | 6 +
.../applib/dom/replay/CommandExportManager.java | 290 ++++++++++++-----
.../CommandExportManager.layout.fallback.xml | 12 +-
...anager#pendingOrFailed.columnOrder.fallback.txt | 6 +
...er#succeededOrExcluded.columnOrder.fallback.txt | 6 +
.../applib/dom/replay/CommandReplayManager.java | 355 ++++++++++++++++-----
.../CommandReplayManager.layout.fallback.xml | 12 +-
.../applib/dom/replay/ReplayContext.java | 2 +
.../ReplayableCommand.columnOrder.fallback.txt | 6 +
.../applib/dom/replay/ReplayableCommand.java | 188 +++++------
.../replay/ReplayableCommand.layout.fallback.xml | 36 ++-
...yContext.java => ReplayableCommand_delete.java} | 41 ++-
.../ReplayableCommand_excludeFromReplay.java | 54 ++++
.../replay/ReplayableCommand_makeExportable.java | 55 ++++
.../ReplayableCommand_openCommandLogEntry.java | 54 ++++
.../replay/ReplayableCommand_replayOrRetry.java | 57 ++++
.../applib/dom/replay/TimestampMarshallUtil.java | 48 +++
.../integtest/CommandLog_IntegTestAbstract.java | 4 +-
.../commandlog/jdo/dom/CommandLogEntry.java | 14 +-
.../commandlog/jpa/dom/CommandLogEntry.java | 14 +-
32 files changed, 1397 insertions(+), 337 deletions(-)
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 83372393da9..8fec5928b9d 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
@@ -40,7 +40,9 @@
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 org.apache.causeway.schema.common.v2.ValueDto;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.NamedType;
@@ -140,10 +142,11 @@ public String toYaml(final Iterable<CommandDto> commandDtos) {
public ObjectMapper apply(ObjectMapper mapper) {
JsonUtils.jaxbAnnotationSupport(mapper);
CommandDtoUtils.memberDtoSupport(mapper);
+ CommandDtoUtils.valueDtoSupport(mapper);
JsonUtils.onlyIncludeNonNull(mapper);
return mapper;
}
- };
+ };
return YamlUtils.toStringUtf8(
_NullSafe.stream(commandDtos)
.collect(Collectors.toList()),
@@ -156,6 +159,7 @@ public List<CommandDto> fromYaml(final DataSource commandDtosYaml) {
public ObjectMapper apply(ObjectMapper mapper) {
JsonUtils.jaxbAnnotationSupport(mapper);
CommandDtoUtils.memberDtoSupport(mapper);
+ CommandDtoUtils.valueDtoSupport(mapper);
return mapper;
}
};
@@ -172,6 +176,14 @@ public ObjectMapper apply(ObjectMapper mapper) {
property = "type")
private abstract class AbstractDtoMixIn {}
+ // Mix-in to ignore unknown properties for ValueDto
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private abstract class AbstractValueDtoMixIn {}
+
+ private void valueDtoSupport(final ObjectMapper mb) {
+ mb.addMixIn(ValueDto.class, AbstractValueDtoMixIn.class);
+ }
+
private void memberDtoSupport(final ObjectMapper mb) {
// add mix-in so MemberDto carries @JsonTypeInfo without modifying source
mb.addMixIn(MemberDto.class, AbstractDtoMixIn.class);
@@ -179,5 +191,5 @@ private void memberDtoSupport(final ObjectMapper mb) {
mb.registerSubtypes(new NamedType(ActionDto.class, "ACT"));
mb.registerSubtypes(new NamedType(PropertyDto.class, "PROP"));
}
-
+
}
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_Test.java b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_Test.java
index e346f6780c9..5f81ec5116a 100644
--- a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_Test.java
+++ b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_Test.java
@@ -18,6 +18,13 @@
*/
package org.apache.causeway.applib.util.schema;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.causeway.applib.value.Blob;
+import org.apache.causeway.applib.value.NamedWithMimeType;
+
+import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -28,6 +35,8 @@
import org.apache.causeway.schema.cmd.v2.CommandDto;
import org.apache.causeway.schema.cmd.v2.MapDto;
+import org.springframework.util.StreamUtils;
+
public class CommandDtoUtils_Test {
CommandDto dto;
@@ -76,4 +85,5 @@ public void clearUserData() {
// then
assertThat(CommandDtoUtils.getUserData(dto, "someKey"), is(nullValue()));
}
+
}
\ No newline at end of file
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-collection-param.yaml
new file mode 100644
index 00000000000..f2b7b6b3b5f
--- /dev/null
+++ b/api/applib/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: !<ACT>
+ 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-30T22:00:00.000+00:00"
+ type: "localDate"
+ name: "Invoice Due Date"
+ - localDate: "2026-06-30T22:00:00.000+00:00"
+ type: "localDate"
+ name: "Start Due Date"
+ - localDate: "2026-07-01T22:00:00.000+00:00"
+ 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/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.commands-with-scalar-params.yaml
new file mode 100644
index 00000000000..d88b314b5ec
--- /dev/null
+++ b/api/applib/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: !<ACT>
+ 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: !<ACT>
+ parameters:
+ parameter:
+ - localDate: "2026-04-20T22:00:00.000+00:00"
+ 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/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_fromYaml_Test.java
new file mode 100644
index 00000000000..2608b975d59
--- /dev/null
+++ b/api/applib/src/test/java/org/apache/causeway/applib/util/schema/CommandDtoUtils_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.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;
+
+public class CommandDtoUtils_fromYaml_Test {
+
+ @Test
+ public void scalarValues() throws IOException {
+ var commandDtos = loadCommands("commands-with-scalar-params.yaml");
+
+ 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<ParamDto> 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();
+
+ 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
+ public 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<ParamDto> 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 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<CommandDto> 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.getMimeType(),
+ bytes);
+
+ return CommandDtoUtils.fromYaml(commandsYaml.asDataSource());
+ }
+}
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java
index f26ba35f88c..4f83a6a5f1f 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java
@@ -18,6 +18,7 @@
*/
package org.apache.causeway.extensions.commandlog.applib;
+import org.apache.causeway.applib.services.clock.ClockService;
import org.apache.causeway.applib.services.command.CommandExecutorService;
import org.apache.causeway.applib.services.iactnlayer.InteractionService;
import org.apache.causeway.applib.services.repository.RepositoryService;
@@ -35,6 +36,11 @@
import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager;
import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager;
import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayContext;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_delete;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_excludeFromReplay;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_makeExportable;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_openCommandLogEntry;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_replayOrRetry;
import org.apache.causeway.extensions.commandlog.applib.fakescheduler.FakeScheduler;
import org.apache.causeway.extensions.commandlog.applib.job.BackgroundCommandsJobControl;
import org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob;
@@ -60,6 +66,24 @@
CommandLogEntry_childCommands.class,
CommandLogEntry_openResultObject.class,
CommandLogEntry_siblingCommands.class,
+ ReplayableCommand_makeExportable.class,
+ ReplayableCommand_openCommandLogEntry.class,
+ ReplayableCommand_replayOrRetry.class,
+ ReplayableCommand_excludeFromReplay.class,
+ ReplayableCommand_delete.class,
+ CommandExportManager.changeSince.class,
+ CommandExportManager.previousHour.class,
+ CommandExportManager.nextHour.class,
+ CommandExportManager.exportSelected.class,
+ CommandExportManager.makeSelectedExportable.class,
+ CommandReplayManager.changeSince.class,
+ CommandExportManager.previousHour.class,
+ CommandReplayManager.nextHour.class,
+ CommandReplayManager.importCommands.class,
+ CommandReplayManager.replayOrRetrySelected.class,
+ CommandReplayManager.excludeSelectedFromReplay.class,
+ CommandReplayManager.deleteSelectedSucceededOrExcluded.class,
+ CommandReplayManager.deleteSelectedPendingOrFailed.class,
// @Component's
RunBackgroundCommandsJob.class,
@@ -122,9 +146,10 @@ public static void honorSystemEnvironment() {
final RepositoryService repositoryService,
final InteractionService interactionService,
final CommandLogEntryRepository commandLogEntryRepository,
- final CommandExecutorService commandExecutorService) {
+ final CommandExecutorService commandExecutorService,
+ final ClockService clockService) {
return new ReplayContext(repositoryService, interactionService,
- commandLogEntryRepository, commandExecutorService);
+ commandLogEntryRepository, commandExecutorService, clockService);
}
}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java
index 607bd8e4583..70285c2f1a7 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java
@@ -18,8 +18,13 @@
*/
package org.apache.causeway.extensions.commandlog.applib.app;
+import java.sql.Timestamp;
+import java.time.Instant;
import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
import java.util.List;
import javax.inject.Inject;
@@ -30,7 +35,9 @@
import org.apache.causeway.applib.annotation.DomainService;
import org.apache.causeway.applib.annotation.DomainServiceLayout;
import org.apache.causeway.applib.annotation.MemberSupport;
+import org.apache.causeway.applib.annotation.ParameterLayout;
import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.annotation.PropertyLayout;
import org.apache.causeway.applib.annotation.Publishing;
import org.apache.causeway.applib.annotation.RestrictTo;
import org.apache.causeway.applib.annotation.SemanticsOf;
@@ -41,6 +48,9 @@
import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager;
import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager;
import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayContext;
+
+import org.jspecify.annotations.NonNull;
+
import org.springframework.lang.Nullable;
import lombok.RequiredArgsConstructor;
@@ -160,8 +170,16 @@ public class DomainEvent extends ActionDomainEvent<findAll> { }
public class exportManager {
public class DomainEvent extends ActionDomainEvent<exportManager> { }
- @MemberSupport public CommandExportManager act() {
- return new CommandExportManager(null, replayContext);
+ @MemberSupport public CommandExportManager act(
+ @ParameterLayout(describedAs = "Limits the commands shown; only commands since this timestamp are available for export. Set to a time immediately before the commands to be replayed.")
+ final java.sql.Timestamp since
+ ) {
+ return new CommandExportManager(since, replayContext);
+ }
+
+ @MemberSupport public java.sql.Timestamp defaultSince() {
+ final var now = clockService.getClock().nowAsJavaSqlTimestamp();
+ return truncatedTo(now, ChronoUnit.HOURS);
}
}
@@ -176,9 +194,21 @@ public class DomainEvent extends ActionDomainEvent<exportManager> { }
public class replayManager {
public class DomainEvent extends ActionDomainEvent<replayManager> { }
- @MemberSupport public CommandReplayManager act() {
- return new CommandReplayManager(null, replayContext);
+ @MemberSupport public CommandReplayManager act(
+ @ParameterLayout(describedAs = "Limits the commands shown; only commands since this timestamp are available for replay. Set to a time immediately before the commands to be replayed.")
+ final java.sql.Timestamp since
+ ) {
+ return new CommandReplayManager(since, replayContext);
}
+
+ @MemberSupport public java.sql.Timestamp defaultSince() {
+ final var now = clockService.getClock().nowAsJavaSqlTimestamp();
+ return truncatedTo(now, ChronoUnit.HOURS);
+ }
+ }
+
+ private static @NonNull Timestamp truncatedTo(Timestamp now, ChronoUnit chronoUnit) {
+ return Timestamp.from(now.toInstant().truncatedTo(chronoUnit));
}
private LocalDate now() {
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java
index e3ca1d877b7..0636fa19e0a 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java
@@ -149,7 +149,10 @@ public static class Nq {
* primary before the production database was restored to the secondary.
*/
public static final String FIND_MOST_RECENT_COMPLETED = LOGICAL_TYPE_NAME + ".findMostRecentCompleted";
- public static final String FIND_BY_REPLAY_STATE = LOGICAL_TYPE_NAME + ".findNotYetReplayed";
+ public static final String FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE
+ = LOGICAL_TYPE_NAME + ".findForegroundByTimestampAfterAndReplayState";
+ public static final String FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES
+ = LOGICAL_TYPE_NAME + ".findForegroundByTimestampAfterAndReplayStates";
public static final String FIND_BACKGROUND_AND_NOT_YET_STARTED = LOGICAL_TYPE_NAME + ".findBackgroundAndNotYetStarted";
public static final String FIND_RECENT_BACKGROUND_BY_TARGET = LOGICAL_TYPE_NAME + ".findRecentBackgroundByTarget";
}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java
index bf8401c6e8c..cf964950e37 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java
@@ -18,6 +18,7 @@
*/
package org.apache.causeway.extensions.commandlog.applib.dom;
+import java.sql.Timestamp;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
@@ -121,6 +122,22 @@ List<CommandLogEntry> findByTargetAndFromAndTo(
*/
List<CommandLogEntry> findSince(final UUID interactionId, final Integer batchSize);
+ List<CommandLogEntry> findForegroundSinceTimestampAndCanBeExported(final Timestamp since);
+
+ List<CommandLogEntry> findForegroundSinceTimestampAndHasBeenExported(final Timestamp since);
+
+ /**
+ * Command Replay feature: Can replay or retry.
+ */
+ List<CommandLogEntry> findForegroundSinceTimestampAndWithReplayPendingOrFailed(Timestamp since);
+
+ /**
+ * Command Replay feature: Cannot replay or retry.
+ */
+ List<CommandLogEntry> findSinceAndWithReplayOkOrExcluded(Timestamp since);
+
+
+
/**
* Returns any persisted commands that have not yet started.
*
@@ -160,14 +177,6 @@ List<CommandLogEntry> findByTargetAndFromAndTo(
*/
Optional<CommandLogEntry> findMostRecentCompleted();
- /**
- * Command Replay feature: Can replay or retry.
- */
- List<CommandLogEntry> findReplayPendingOrFailed();
- /**
- * Command Replay feature: Cannot replay or retry.
- */
- List<CommandLogEntry> findReplaySucceededOrExcluded();
CommandLogEntry saveForReplay(final CommandDto dto);
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java
index fdbb8b23379..bff9967e360 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepositoryAbstract.java
@@ -26,6 +26,7 @@
import java.util.List;
import java.util.Optional;
import java.util.UUID;
+import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Provider;
@@ -333,27 +334,6 @@ public Optional<CommandLogEntry> findMostRecentCompleted() {
Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_MOST_RECENT_COMPLETED))
);
}
- @Override
- public List<CommandLogEntry> findReplayPendingOrFailed() {
- return _Casts.uncheckedCast(
- repositoryService().allMatches(
- Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_BY_REPLAY_STATE)
- .withParameter("replayState1", ReplayState.PENDING)
- .withParameter("replayState2", ReplayState.FAILED))
- );
- }
- /**
- * Command Replay feature: Cannot replay or retry.
- */
- @Override
- public List<CommandLogEntry> findReplaySucceededOrExcluded() {
- return _Casts.uncheckedCast(
- repositoryService().allMatches(
- Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_BY_REPLAY_STATE)
- .withParameter("replayState1", ReplayState.OK)
- .withParameter("replayState2", ReplayState.EXCLUDED))
- );
- }
@Override
public C saveForReplay(final CommandDto commandToReplay) {
@@ -370,7 +350,7 @@ public C saveForReplay(final CommandDto commandToReplay) {
final C entity = factoryService.detachedEntity(commandLogEntryClass);
entity.init(commandToReplay, ReplayState.PENDING, 0);
entity.setParentInteractionId(null); // n/a for replay
- entity.setExecuteIn(null); // to be specified later depending on user action
+ entity.setExecuteIn(ExecuteIn.FOREGROUND); // only ever replay foreground commands.
persist(entity);
@@ -427,6 +407,46 @@ private List<CommandLogEntry> findSince(
: commandJdos;
}
+ @Override
+ public List<CommandLogEntry> findForegroundSinceTimestampAndCanBeExported(final Timestamp since) {
+ return findForegroundSinceTimestampWithState(since, ReplayState.UNDEFINED);
+ }
+
+ @Override
+ public List<CommandLogEntry> findForegroundSinceTimestampAndHasBeenExported(final Timestamp since) {
+ return findForegroundSinceTimestampWithState(since, ReplayState.EXPORTED);
+ }
+
+ @Override
+ public List<CommandLogEntry> findForegroundSinceTimestampAndWithReplayPendingOrFailed(final Timestamp since) {
+ return findForegroundSinceTimestampWithStates(since, ReplayState.PENDING, ReplayState.FAILED);
+ }
+
+ /**
+ * Command Replay feature: Cannot replay or retry.
+ */
+ @Override
+ public List<CommandLogEntry> findSinceAndWithReplayOkOrExcluded(final Timestamp since) {
+ return findForegroundSinceTimestampWithStates(since, ReplayState.OK, ReplayState.EXCLUDED);
+ }
+
+ private List<CommandLogEntry> findForegroundSinceTimestampWithState(Timestamp from, ReplayState replayState) {
+ return _Casts.uncheckedCast(
+ repositoryService().allMatches(
+ Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE)
+ .withParameter("from", from)
+ .withParameter("replayState", replayState)));
+ }
+
+ private List<CommandLogEntry> findForegroundSinceTimestampWithStates(Timestamp from, ReplayState replayState1, ReplayState replayState2) {
+ return _Casts.uncheckedCast(
+ repositoryService().allMatches(
+ Query.named(commandLogEntryClass, CommandLogEntry.Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES)
+ .withParameter("from", from)
+ .withParameter("replayState1", replayState1)
+ .withParameter("replayState2", replayState2)));
+ }
+
private RepositoryService repositoryService() {
return repositoryServiceProvider.get();
}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ReplayState.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ReplayState.java
index e644cd240d6..93ed4f68660 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ReplayState.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ReplayState.java
@@ -54,33 +54,31 @@ public enum ReplayState {
public boolean isExported() { return this == EXPORTED; }
public boolean isFailed() { return this == FAILED; }
- public boolean canExport() {
+ public boolean isExportable() {
return this == ReplayState.UNDEFINED;
}
- public boolean canReplayOrRetryOrMarkForExclusion() {
+ public boolean isPendingOrFailed() {
return this == ReplayState.PENDING
|| this == ReplayState.FAILED;
}
// -- NULL SAFE
- public static boolean canExport(final @Nullable ReplayState replayState) {
- return replayState!=null
- ? replayState.canExport()
- : true;
+ public static boolean isExportable(final @Nullable ReplayState replayState) {
+ return replayState == null || replayState.isExportable();
}
- public static boolean canReplayOrRetryOrMarkForExclusion(final @Nullable ReplayState replayState) {
- return replayState!=null
- ? replayState.canReplayOrRetryOrMarkForExclusion()
- : false;
+ public static boolean isPendingOrFailed(final @Nullable ReplayState replayState) {
+ return replayState != null && replayState.isPendingOrFailed();
+ }
+
+ public static boolean isOkOrExcluded(ReplayState replayState) {
+ return replayState == ReplayState.OK || replayState == ReplayState.EXCLUDED;
}
public static boolean isExported(final ReplayState replayState) {
- return replayState!=null
- ? replayState.isExported()
- : false;
+ return replayState != null && replayState.isExported();
}
}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#exported.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#exported.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#exported.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#notYetExported.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#notYetExported.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#notYetExported.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
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 8f963454dda..f0243676053 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
@@ -18,6 +18,11 @@
*/
package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+import lombok.Getter;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.chrono.ChronoZonedDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -25,10 +30,6 @@
import javax.inject.Inject;
import javax.inject.Named;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.experimental.Accessors;
-
import org.apache.causeway.applib.ViewModel;
import org.apache.causeway.applib.annotation.Action;
import org.apache.causeway.applib.annotation.ActionLayout;
@@ -37,7 +38,12 @@
import org.apache.causeway.applib.annotation.DomainObject;
import org.apache.causeway.applib.annotation.DomainObjectLayout;
import org.apache.causeway.applib.annotation.Introspection;
+import org.apache.causeway.applib.annotation.MemberSupport;
import org.apache.causeway.applib.annotation.ObjectSupport;
+import org.apache.causeway.applib.annotation.Property;
+import org.apache.causeway.applib.annotation.PropertyLayout;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.RestrictTo;
import org.apache.causeway.applib.annotation.SemanticsOf;
import org.apache.causeway.applib.util.schema.CommandDtoUtils;
import org.apache.causeway.applib.value.Blob;
@@ -48,138 +54,266 @@
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState;
+import static org.apache.causeway.extensions.commandlog.applib.dom.replay.TimestampMarshallUtil.fromString;
+
@DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
@DomainObjectLayout(cssClassFa = "solid share-from-square")
@Named(CommandExportManager.LOGICAL_TYPE_NAME)
-@AllArgsConstructor
public final class CommandExportManager implements ViewModel {
-
+
public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager";
-
- @Getter @Accessors(fluent = true)
- private final ReplayContext replayContext;
+
+ public static abstract class ActionDomainEvent<T>
+ extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+ private ReplayContext replayContext;
@Inject
public CommandExportManager(
final String memento,
final ReplayContext replayContext) {
- this(replayContext);
+ this(fromString(memento, replayContext.clockService().getClock().nowAsJavaSqlTimestamp()), replayContext);
+ }
+
+ public CommandExportManager(
+ final java.sql.Timestamp since,
+ final ReplayContext replayContext) {
+ this.since = since;
+ this.replayContext = replayContext;
}
@ObjectSupport public String title() {
return "Command Export Manager";
}
- @Action(semantics = SemanticsOf.IDEMPOTENT_ARE_YOU_SURE)
+
+ @Property
+ @PropertyLayout(describedAs = "Only commands since this timestamp are available for export")
+ @Getter
+ private java.sql.Timestamp since;
+
+ @Action(
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = previousHour.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "since", sequence = "1",
+ named = "Previous",
+ position = ActionLayout.Position.PANEL,
+ describedAs = "Move back one hour"
+ )
+ public class previousHour {
+ public class DomainEvent extends ActionDomainEvent<previousHour> { }
+
+ @MemberSupport public CommandExportManager act() {
+ return new CommandExportManager(addSeconds(since, -3600), replayContext);
+ }
+ }
+
+ @Action(
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = nextHour.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
@ActionLayout(
- sequence = "0.1",
- describedAs = "Deletes all commands, regardless of state (cannot be undone)")
- public CommandExportManager deleteAll() {
- commandLogEntryRepository().removeAll();
- return this;
+ associateWith = "since", sequence = "3",
+ named = "Next",
+ position = ActionLayout.Position.PANEL,
+ describedAs = "Move forward one hour"
+ )
+ public class nextHour {
+ public class DomainEvent extends ActionDomainEvent<nextHour> { }
+ @MemberSupport public CommandExportManager act() {
+ return new CommandExportManager(addSeconds(since, +3600), replayContext);
+ }
+ }
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = changeSince.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "since", sequence = "2",
+ named = "Change",
+ position = ActionLayout.Position.PANEL
+ )
+ public class changeSince {
+ public class DomainEvent extends ActionDomainEvent<nextHour> { }
+ @MemberSupport public CommandExportManager act(final java.sql.Timestamp since) {
+ return new CommandExportManager(since, replayContext);
+ }
+ @MemberSupport public java.sql.Timestamp defaultSince() {
+ return CommandExportManager.this.since;
+ }
+ }
+
+ private static Timestamp addSeconds(Timestamp since, int secondsToAdd) {
+ return Timestamp.from(since.toInstant().plusSeconds(secondsToAdd));
}
+
// -- NOT YET EXPORTED
@Collection
@CollectionLayout(
- describedAs = "Commands that can be exported")
+ describedAs = "Commands that can be exported"
+ )
public List<ReplayableCommand> getNotYetExported() {
- return commandLogEntryRepository().findAll().stream()
- .filter(entry->ReplayState.canExport(entry.getReplayState()))
+ return commandLogEntryRepository().findForegroundSinceTimestampAndCanBeExported(since).stream()
.map(entry->new ReplayableCommand(
entry.getInteractionId(),
replayContext))
.collect(Collectors.toList());
}
- @Action(choicesFrom = "notYetExported", semantics = SemanticsOf.NON_IDEMPOTENT)
- @ActionLayout(associateWith = "notYetExported",
- sequence = "1.1",
- cssClassFa = "solid share-from-square",
- cssClass = "btn-primary",
- describedAs = "Exports selected Commands as zipped DTOs for import later. "
- + "(You need to refresh the page to see changed states.)")
- public Blob exportSelected(
- final List<ReplayableCommand> selected) {
-
- var selectedCommandLogEntries = selected.stream()
- .map(ReplayableCommand::commandLogEntry)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .sorted()
- .collect(Collectors.toList());
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ choicesFrom = "notYetExported",
+ semantics = SemanticsOf.NON_IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = exportSelected.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "notYetExported", sequence = "1.1",
+ cssClassFa = "solid share-from-square",
+ cssClass = "btn-primary",
+ describedAs = "Exports selected Commands as zipped DTOs for import later. "
+ + "Refresh the page to see changed states."
+ )
+ public class exportSelected {
+ public class DomainEvent extends ActionDomainEvent<exportSelected> { }
- var yaml = CommandDtoUtils.toYaml(
- selectedCommandLogEntries.stream()
- .filter(entry->!ReplayState.isExported(entry.getReplayState()))
- .map(CommandLogEntry::getCommandDto)
- .collect(Collectors.toList()));
+ @MemberSupport public Blob act(
+ final List<ReplayableCommand> selected,
+ final String filenamePrefix,
+ final boolean filenameTimestamp
+ ) {
- var blob = Clob.of("commands.yaml", CommonMimeType.YAML, yaml)
- .toBlobUtf8()
- .zip();
+ var selectedCommandLogEntries = selected.stream()
+ .map(ReplayableCommand::commandLogEntry)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .sorted()
+ .collect(Collectors.toList());
- // do this last once we have successfully created the Clob
- selectedCommandLogEntries.forEach(c->c.setReplayState(ReplayState.EXPORTED));
+ var yaml = CommandDtoUtils.toYaml(
+ selectedCommandLogEntries.stream()
+ .filter(entry->!ReplayState.isExported(entry.getReplayState())) // shouldn't be necessary unless a race condition
+ .map(CommandLogEntry::getCommandDto)
+ .collect(Collectors.toList()));
- return blob;
- }
+ final var replayableCommand = selected.get(0); // validate ensures there is at least one command
+ final var timestamp = filenameTimestamp
+ ? replayableCommand.getTimestampIfAny()
+ .map(ChronoZonedDateTime::toInstant)
+ .map(Instant::toString)
+ .map(x -> "." + x.replaceAll("[^A-Za-z0-9._-]", "_")) // make safe within filename
+ .orElse("")
+ : "";
+ final var filename = filenamePrefix + timestamp;
+
+ var blob = Clob.of(filename, CommonMimeType.YAML, yaml)
+ .toBlobUtf8()
+ .zip();
+
+ // do this last once we have successfully created the Clob
+ selectedCommandLogEntries.forEach(c->c.setReplayState(ReplayState.EXPORTED));
+
+ return blob;
+ }
+
+ @MemberSupport public String disableAct() {
+ return getNotYetExported().isEmpty() ? "No commands in collection" : null;
+ }
+
+ @MemberSupport public String defaultFilenamePrefix() {
+ return "commands";
+ }
+
+ @MemberSupport public boolean defaultFilenameTimestamp() {
+ return true;
+ }
+
+ @MemberSupport public String validateSelected(final List<ReplayableCommand> selected) {
+ return selected != null && selected.isEmpty() ? "Select at least one command to export" : null;
+ }
- @Action(choicesFrom = "notYetExported")
- @ActionLayout(associateWith = "notYetExported", sequence = "1.2",
- describedAs = "Deletes selected Commands (cannot be undone)")
- public CommandExportManager deleteSelected(final List<ReplayableCommand> selected) {
- selected.stream()
- .forEach(ReplayableCommand::delete); // filtered on its own responsibility
- return this;
+ // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+ @MemberSupport
+ public List<ReplayableCommand> choicesSelected() {
+ return getNotYetExported();
+ }
}
+
// -- EXPORTED
@Collection
- @CollectionLayout(
- describedAs = "Commands that were exported")
+ @CollectionLayout(describedAs = "Commands that have been exported")
public List<ReplayableCommand> getExported() {
- return commandLogEntryRepository().findAll().stream()
- .filter(entry->ReplayState.isExported(entry.getReplayState()))
+ return commandLogEntryRepository().findForegroundSinceTimestampAndHasBeenExported(since).stream()
.map(entry->new ReplayableCommand(
entry.getInteractionId(),
replayContext))
.collect(Collectors.toList());
}
- @Action(choicesFrom = "exported")
- @ActionLayout(associateWith = "exported", sequence = "2.1",
- describedAs = "Makes selected Commands exportable (again)")
- public CommandExportManager makeSelectedExportable(final List<ReplayableCommand> selected) {
- selected.stream()
- .forEach(ReplayableCommand::makeExportable); // filtered on its own responsibility
- return this;
- }
- @Action(choicesFrom = "exported")
- @ActionLayout(associateWith = "exported", sequence = "2.2",
- named = "Delete Selected",
- describedAs = "Deletes selected Commands (cannot be undone)")
- public CommandExportManager deleteSelected2(final List<ReplayableCommand> selected) {
- selected.stream()
- .forEach(ReplayableCommand::delete); // filtered on its own responsibility
- return this;
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ choicesFrom = "exported",
+ commandPublishing = Publishing.DISABLED,
+ semantics = SemanticsOf.IDEMPOTENT,
+ domainEvent = makeSelectedExportable.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "exported", sequence = "2.1",
+ describedAs = "Makes selected Commands exportable (again)"
+ )
+ public class makeSelectedExportable {
+ public class DomainEvent extends ActionDomainEvent<makeSelectedExportable> { }
+
+ @MemberSupport
+ public CommandExportManager act(final List<ReplayableCommand> selected) {
+ selected.forEach(ReplayableCommand::makeExportable); // filtered on its own responsibility
+ return CommandExportManager.this;
+ }
+
+ @MemberSupport
+ public String disableAct() {
+ return getExported().isEmpty() ? "No commands in collection" : null;
+ }
+
+ @MemberSupport
+ public String validateSelected(final List<ReplayableCommand> selected) {
+ return selected != null && selected.isEmpty() ? "Select at least one command" : null;
+ }
+
+ // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+ @MemberSupport
+ public List<ReplayableCommand> choicesSelected() {
+ return getExported();
+ }
}
+
// -- VM STATE
@Override
public String viewModelMemento() {
- // TODO could use to store filter state
- return null;
+ return TimestampMarshallUtil.toString(this.since);
}
// -- HELPER
-
private CommandLogEntryRepository commandLogEntryRepository() {
return replayContext.commandLogEntryRepository();
}
-
}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml
index df20c9b73aa..ba0d2da0ef7 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml
@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?><bs:grid xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3" xmlns:lnk="https://causeway.apache.org/applib/layout/links" xmlns:cpt="https://causeway.apache.org/applib/layout/component" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/links https://caus [...]
<bs:row>
- <bs:col span="8" unreferencedActions="true">
+ <bs:col span="6" unreferencedActions="true">
<cpt:domainObject/>
</bs:col>
- <bs:col span="4">
+ <bs:col span="3">
+ <cpt:fieldSet name=" " id="since">
+ <cpt:action id="previousHour"/>
+ <cpt:action id="changeSince"/>
+ <cpt:action id="nextHour"/>
+ <cpt:property id="since"/>
+ </cpt:fieldSet>
+ </bs:col>
+ <bs:col span="3">
<bs:tabGroup>
<bs:tab name="Identity">
<bs:row>
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#pendingOrFailed.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#pendingOrFailed.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#pendingOrFailed.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#succeededOrExcluded.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#succeededOrExcluded.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#succeededOrExcluded.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
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 e6405bb37b0..b326e4c4be2 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
@@ -18,6 +18,7 @@
*/
package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+import java.sql.Timestamp;
import java.util.List;
import java.util.stream.Collectors;
@@ -32,8 +33,13 @@
import org.apache.causeway.applib.annotation.DomainObject;
import org.apache.causeway.applib.annotation.DomainObjectLayout;
import org.apache.causeway.applib.annotation.Introspection;
+import org.apache.causeway.applib.annotation.MemberSupport;
import org.apache.causeway.applib.annotation.ObjectSupport;
import org.apache.causeway.applib.annotation.Parameter;
+import org.apache.causeway.applib.annotation.Property;
+import org.apache.causeway.applib.annotation.PropertyLayout;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.RestrictTo;
import org.apache.causeway.applib.annotation.SemanticsOf;
import org.apache.causeway.applib.util.schema.CommandDtoUtils;
import org.apache.causeway.applib.value.Blob;
@@ -42,140 +48,348 @@
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
import org.apache.causeway.schema.cmd.v2.CommandDto;
-import lombok.AllArgsConstructor;
import lombok.Getter;
-import lombok.experimental.Accessors;
+
+import static org.apache.causeway.extensions.commandlog.applib.dom.replay.TimestampMarshallUtil.fromString;
@DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
@DomainObjectLayout(cssClassFa = "solid circle-play")
@Named(CommandReplayManager.LOGICAL_TYPE_NAME)
-@AllArgsConstructor
public final class CommandReplayManager implements ViewModel {
-
+
public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayManager";
-
- @Getter @Accessors(fluent = true)
- private final ReplayContext replayContext;
+
+ public static abstract class ActionDomainEvent<T>
+ extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+ private ReplayContext replayContext;
@Inject
public CommandReplayManager(
final String memento,
final ReplayContext replayContext) {
- this(replayContext);
+ this(fromString(memento, replayContext.clockService().getClock().nowAsJavaSqlTimestamp()), replayContext);
+ }
+
+ public CommandReplayManager(
+ final java.sql.Timestamp since,
+ final ReplayContext replayContext) {
+ this.since = since;
+ this.replayContext = replayContext;
}
@ObjectSupport public String title() {
return "Command Replay Manager";
}
- @Action
+
+ @Property
+ @PropertyLayout(describedAs = "Only commands since this timestamp are available for export")
+ @Getter
+ private java.sql.Timestamp since;
+
+ @Action(
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = previousHour.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
@ActionLayout(
- sequence = "0.1",
- cssClass = "btn-primary",
- describedAs = "Imports commands from a zipped yaml, then persists them with replayState=PENDING.")
- public CommandReplayManager importCommands(
- @Parameter(fileAccept = ".zip")
- final Blob zippedCommandsYaml) {
+ associateWith = "since", sequence = "1",
+ named = "Previous",
+ position = ActionLayout.Position.PANEL,
+ describedAs = "Move back one hour"
+ )
+ public class previousHour {
+ public class DomainEvent extends ActionDomainEvent<previousHour> { }
- var yamlDs = zippedCommandsYaml.unZip(CommonMimeType.YAML).asDataSource();
+ @MemberSupport public CommandReplayManager act() {
+ return new CommandReplayManager(addSeconds(since, -3600), replayContext);
+ }
+ }
- final List<CommandDto> commandDtos = CommandDtoUtils.fromYaml(yamlDs);
- commandDtos.forEach(commandLogEntryRepository()::saveForReplay);
+ @Action(
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = nextHour.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "since", sequence = "3",
+ named = "Next",
+ position = ActionLayout.Position.PANEL,
+ describedAs = "Move forward one hour"
+ )
+ public class nextHour {
+ public class DomainEvent extends ActionDomainEvent<nextHour> { }
+ @MemberSupport public CommandReplayManager act() {
+ return new CommandReplayManager(addSeconds(since, +3600), replayContext);
+ }
+ }
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = changeSince.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "since", sequence = "2",
+ named = "Change",
+ position = ActionLayout.Position.PANEL
+ )
+ public class changeSince {
+ public class DomainEvent extends ActionDomainEvent<nextHour> { }
+ @MemberSupport public CommandReplayManager act(final java.sql.Timestamp since) {
+ return new CommandReplayManager(since, replayContext);
+ }
+ @MemberSupport public java.sql.Timestamp defaultSince() {
+ return CommandReplayManager.this.since;
+ }
+ }
- return this;
+ private static Timestamp addSeconds(Timestamp since, int secondsToAdd) {
+ return Timestamp.from(since.toInstant().plusSeconds(secondsToAdd));
}
- @Action(semantics = SemanticsOf.IDEMPOTENT_ARE_YOU_SURE)
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = importCommands.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
@ActionLayout(
- sequence = "0.2",
- describedAs = "Deletes all commands, regardless of state (cannot be undone)")
- public CommandReplayManager deleteAll() {
- commandLogEntryRepository().removeAll();
- return this;
+ sequence = "0.1",
+ associateWith = "pendingOrFailed",
+ cssClass = "btn-primary",
+ describedAs = "Imports commands from a zipped yaml, then persists them with replayState=PENDING."
+ )
+ public class importCommands {
+ public class DomainEvent extends ActionDomainEvent<importCommands> { }
+ public CommandReplayManager act(
+ @Parameter(fileAccept = ".zip")
+ final Blob zippedCommandsYaml) {
+
+ var yamlDs = zippedCommandsYaml.unZip(CommonMimeType.YAML).asDataSource();
+
+ final List<CommandDto> commandDtos = CommandDtoUtils.fromYaml(yamlDs);
+ commandDtos.forEach(commandLogEntryRepository()::saveForReplay);
+
+ return CommandReplayManager.this;
+ }
}
+
// -- PENDING OR FAILED
@Collection
@CollectionLayout(
- describedAs = "Imported Commands that can be either replayed (replayState=PENDING) or retried (when replayState=FAILED)")
+ describedAs = "Imported Commands that can be either replayed (replayState=PENDING) or retried (when replayState=FAILED)"
+ )
public List<ReplayableCommand> getPendingOrFailed() {
- return commandLogEntryRepository().findReplayPendingOrFailed().stream()
+ return commandLogEntryRepository().findForegroundSinceTimestampAndWithReplayPendingOrFailed(since).stream()
.map(entry->new ReplayableCommand(
entry.getInteractionId(),
replayContext))
.collect(Collectors.toList());
}
- @Action(choicesFrom = "pendingOrFailed")
- @ActionLayout(associateWith = "pendingOrFailed",
- sequence = "1.1",
- cssClassFa = "solid circle-play",
- cssClass = "btn-primary",
- describedAs = "Executes the list of commands in sequence, after having sorted them by their timestamp. "
- + "If any of the given commands fails, "
- + "the surrounding transaction is rolled back and any successful commands are undone). "
- + "The command, that caused the failure, gets marked as FAILED.")
- public CommandReplayManager replayOrRetrySelected(final List<ReplayableCommand> selected) {
- var replayables = selected.stream()
- .sorted()
- .collect(Collectors.toList());
- for(var replayableCommand : replayables) {
- var tryReplayOrRetry = replayableCommand.tryReplayOrRetry(); // filtered on its own responsibility
- if(tryReplayOrRetry.isFailure())
- return this; // stop further execution
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ choicesFrom = "pendingOrFailed",
+ semantics = SemanticsOf.NON_IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = replayOrRetrySelected.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "pendingOrFailed", sequence = "1.1",
+ cssClassFa = "solid circle-play",
+ cssClass = "btn-primary",
+ describedAs = "Executes the list of commands in sequence, after having sorted them by their timestamp. "
+ + "If any of the given commands fails, "
+ + "the surrounding transaction is rolled back and any successful commands are undone). "
+ + "The command, that caused the failure, gets marked as FAILED."
+ )
+ public class replayOrRetrySelected {
+ public class DomainEvent extends ActionDomainEvent<replayOrRetrySelected> { }
+ @MemberSupport public CommandReplayManager act(final List<ReplayableCommand> selected) {
+ var replayables = selected.stream()
+ .sorted()
+ .collect(Collectors.toList());
+ for(var replayableCommand : replayables) {
+ var tryReplayOrRetry = replayableCommand.tryReplayOrRetry(); // filtered on its own responsibility
+ if(tryReplayOrRetry.isFailure()) {
+ return CommandReplayManager.this; // stop further execution
+ }
+ }
+ return CommandReplayManager.this;
+ }
+
+
+ @MemberSupport
+ public String disableAct() {
+ return getPendingOrFailed().isEmpty() ? "No commands in collection" : null;
+ }
+
+ @MemberSupport
+ public String validateSelected(final List<ReplayableCommand> selected) {
+ return selected != null && selected.isEmpty() ? "Select at least one command" : null;
+ }
+
+ // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+ @MemberSupport
+ public List<ReplayableCommand> choicesSelected() {
+ return getPendingOrFailed();
}
- return this;
}
- @Action(choicesFrom = "pendingOrFailed")
- @ActionLayout(associateWith = "pendingOrFailed", sequence = "1.2",
- describedAs = "Marks selected Commands to be EXCLUDED from replay")
- public CommandReplayManager excludeSelectedFromReplay(final List<ReplayableCommand> selected) {
- selected.stream()
- .forEach(ReplayableCommand::excludeFromReplay); // filtered on its own responsibility
- return this;
+
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ choicesFrom = "pendingOrFailed",
+ semantics = SemanticsOf.NON_IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = excludeSelectedFromReplay.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "pendingOrFailed", sequence = "1.2",
+ describedAs = "Marks selected Commands to be EXCLUDED from replay"
+ )
+ public class excludeSelectedFromReplay {
+ public class DomainEvent extends ActionDomainEvent<excludeSelectedFromReplay> { }
+ @MemberSupport
+ public CommandReplayManager act(final List<ReplayableCommand> selected) {
+ selected.stream()
+ .forEach(ReplayableCommand::excludeFromReplay); // filtered on its own responsibility
+ return CommandReplayManager.this;
+ }
+
+ @MemberSupport
+ public String disableAct() {
+ return getPendingOrFailed().isEmpty() ? "No commands in collection" : null;
+ }
+
+ @MemberSupport
+ public String validateSelected(final List<ReplayableCommand> selected) {
+ return selected != null && selected.isEmpty() ? "Select at least one command" : null;
+ }
+
+ // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+ @MemberSupport
+ public List<ReplayableCommand> choicesSelected() {
+ return getPendingOrFailed();
+ }
+
}
- @Action(choicesFrom = "pendingOrFailed")
- @ActionLayout(associateWith = "pendingOrFailed", sequence = "1.3",
- describedAs = "Deletes selected Commands (cannot be undone)")
- public CommandReplayManager deleteSelected(final List<ReplayableCommand> selected) {
- selected.stream()
- .forEach(ReplayableCommand::delete); // filtered on its own responsibility
- return this;
+
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ choicesFrom = "pendingOrFailed",
+ semantics = SemanticsOf.NON_IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = deleteSelectedPendingOrFailed.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "pendingOrFailed", sequence = "1.3",
+ describedAs = "Deletes selected Commands (cannot be undone)"
+ )
+ public class deleteSelectedPendingOrFailed {
+ public class DomainEvent extends ActionDomainEvent<deleteSelectedPendingOrFailed> { }
+ public CommandReplayManager act(final List<ReplayableCommand> selected) {
+ selected.stream()
+ .forEach(ReplayableCommand::deleteObj); // filtered on its own responsibility
+ return CommandReplayManager.this;
+ }
+
+ @MemberSupport
+ public String disableAct() {
+ return getPendingOrFailed().isEmpty() ? "No commands in collection" : null;
+ }
+
+ @MemberSupport
+ public String validateSelected(final List<ReplayableCommand> selected) {
+ return selected != null && selected.isEmpty() ? "Select at least one command" : null;
+ }
+
+ // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+ @MemberSupport
+ public List<ReplayableCommand> choicesSelected() {
+ return getPendingOrFailed();
+ }
+
}
+
+
// -- OK OR EXCLUDE
@Collection
@CollectionLayout(
describedAs = "Imported Commands that were either replayed with success (replayState=OK) "
- + "or marked to be excluded from replay (replayState=EXCLUDE)")
+ + "or marked to be excluded from replay (replayState=EXCLUDE)"
+ )
public List<ReplayableCommand> getSucceededOrExcluded() {
- return commandLogEntryRepository().findReplaySucceededOrExcluded().stream()
+ return commandLogEntryRepository().findSinceAndWithReplayOkOrExcluded(since).stream()
.map(entry->new ReplayableCommand(
entry.getInteractionId(),
replayContext))
.collect(Collectors.toList());
}
- @Action(choicesFrom = "succeededOrExcluded")
- @ActionLayout(associateWith = "succeededOrExcluded",
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ choicesFrom = "succeededOrExcluded",
+ semantics = SemanticsOf.IDEMPOTENT,
+ domainEvent = deleteSelectedSucceededOrExcluded.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "succeededOrExcluded",
named = "Delete Selected",
- describedAs = "Deletes selected Commands (cannot be undone)")
- public CommandReplayManager deleteSelected2(final List<ReplayableCommand> selected) {
- selected.stream()
- .forEach(ReplayableCommand::delete); // filtered on its own responsibility
- return this;
+ describedAs = "Deletes selected Commands (cannot be undone)"
+ )
+ public class deleteSelectedSucceededOrExcluded {
+ public class DomainEvent extends ActionDomainEvent<deleteSelectedSucceededOrExcluded> { }
+ public CommandReplayManager act(final List<ReplayableCommand> selected) {
+ selected.stream()
+ .forEach(ReplayableCommand::deleteObj); // filtered on its own responsibility
+ return CommandReplayManager.this;
+ }
+
+ @MemberSupport
+ public String disableAct() {
+ return getSucceededOrExcluded().isEmpty() ? "No commands in collection" : null;
+ }
+
+ @MemberSupport
+ public String validateSelected(final List<ReplayableCommand> selected) {
+ return selected != null && selected.isEmpty() ? "Select at least one command" : null;
+ }
+
+ // TODO: shouldn't be required because of 'choicesFrom', but in v2 there seems to be a MM validation error due to a missing choicesFacet
+ @MemberSupport
+ public List<ReplayableCommand> choicesSelected() {
+ return getSucceededOrExcluded();
+ }
}
+
+
// -- VM STATE
@Override
public String viewModelMemento() {
- // TODO could use to store filter state
- return null;
+ return TimestampMarshallUtil.toString(this.since);
}
// -- HELPER
@@ -183,5 +397,4 @@ public String viewModelMemento() {
private CommandLogEntryRepository commandLogEntryRepository() {
return replayContext.commandLogEntryRepository();
}
-
}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.layout.fallback.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.layout.fallback.xml
index 30489ca2158..c7aed354e9f 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.layout.fallback.xml
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.layout.fallback.xml
@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?><bs:grid xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3" xmlns:lnk="https://causeway.apache.org/applib/layout/links" xmlns:cpt="https://causeway.apache.org/applib/layout/component" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/links https://caus [...]
<bs:row>
- <bs:col span="8" unreferencedActions="true">
+ <bs:col span="6" unreferencedActions="true">
<cpt:domainObject/>
</bs:col>
- <bs:col span="4">
+ <bs:col span="3">
+ <cpt:fieldSet name=" " id="since">
+ <cpt:action id="previousHour"/>
+ <cpt:action id="changeSince"/>
+ <cpt:action id="nextHour"/>
+ <cpt:property id="since"/>
+ </cpt:fieldSet>
+ </bs:col>
+ <bs:col span="3">
<bs:tabGroup>
<bs:tab name="Identity">
<bs:row>
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
index 60d47e0a15f..580e2f05ae6 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
@@ -18,6 +18,7 @@
*/
package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+import org.apache.causeway.applib.services.clock.ClockService;
import org.apache.causeway.applib.services.command.CommandExecutorService;
import org.apache.causeway.applib.services.iactnlayer.InteractionService;
import org.apache.causeway.applib.services.repository.RepositoryService;
@@ -35,4 +36,5 @@ public final class ReplayContext {
InteractionService interactionService;
CommandLogEntryRepository commandLogEntryRepository;
CommandExecutorService commandExecutorService;
+ ClockService clockService;
}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
index 573651b2fde..e2ea51718b0 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
@@ -18,7 +18,9 @@
*/
package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+import java.time.Instant;
import java.time.ZonedDateTime;
+import java.time.chrono.ChronoZonedDateTime;
import java.util.Comparator;
import java.util.Optional;
import java.util.UUID;
@@ -36,8 +38,12 @@
import org.apache.causeway.applib.annotation.LabelPosition;
import org.apache.causeway.applib.annotation.MemberSupport;
import org.apache.causeway.applib.annotation.ObjectSupport;
+import org.apache.causeway.applib.annotation.Programmatic;
import org.apache.causeway.applib.annotation.Property;
import org.apache.causeway.applib.annotation.PropertyLayout;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.RestrictTo;
+import org.apache.causeway.applib.annotation.SemanticsOf;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.jaxb.JavaTimeXMLGregorianCalendarMarshalling;
import org.apache.causeway.applib.services.command.CommandExecutorService.InteractionContextPolicy;
@@ -49,7 +55,6 @@
import org.apache.causeway.commons.io.YamlUtils;
import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
-import org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn;
import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState;
import org.apache.causeway.schema.cmd.v2.CommandDto;
import org.apache.causeway.schema.cmd.v2.MemberDto;
@@ -59,7 +64,6 @@
import org.apache.causeway.valuetypes.asciidoc.builder.AsciiDocFactory;
import lombok.AllArgsConstructor;
-import lombok.Getter;
import lombok.Value;
import lombok.experimental.Accessors;
@@ -71,27 +75,34 @@
@Named(ReplayableCommand.LOGICAL_TYPE_NAME)
@AllArgsConstructor
public final class ReplayableCommand implements ViewModel, Comparable<ReplayableCommand> {
-
- @Getter @Accessors(fluent = true)
- private final UUID interactionId;
-
- @Getter @Accessors(fluent = true)
+
+ public static abstract class ActionDomainEvent<T>
+ extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+ private final UUID interactionId;
+ @Programmatic
+ public UUID interactionId() { return interactionId; }
+
private final ReplayContext replayContext;
-
- @Getter @Accessors(fluent = true)
+ @Programmatic
+ public ReplayContext replayContext() { return replayContext; }
+
+
private final ObjectReference<CommandRecord> recordRef;
+ @Programmatic
+ public ObjectReference<CommandRecord> recordRef() { return recordRef; }
public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".ReplayableCommand";
// decoupled from the underlying entity
@Value @Accessors(fluent = true)
final static class CommandRecord {
-
+
final CommandDto commandDto;
final ReplayState replayState;
-
+
boolean canReplayOrRetryOrMarkForExclusion() {
- return replayState.canReplayOrRetryOrMarkForExclusion();
+ return replayState.isPendingOrFailed();
}
public String faQuickIcon() {
switch(replayState) {
@@ -132,10 +143,11 @@ public ReplayableCommand(
}
@ObjectSupport public String title() {
- return "Replayable Command";
+ final var timestamp = getTimestampIfAny().map(ChronoZonedDateTime::toInstant).map(Instant::toString).map(x -> " @ " + x).orElse("");
+ return getTargetType() + ":" + getTargetId() + " #" + getMember() + timestamp;
}
-//requires v4
+//requires v4
// @ObjectSupport public ObjectSupport.IconResource icon(final ObjectSupport.IconSize iconSize) {
// return commandRecord()
// .map(CommandRecord::faQuickIcon)
@@ -153,7 +165,7 @@ public ReplayableCommand(
@PropertyLayout(
sequence = "1.1",
fieldSetId = "details",
- describedAs = "UUID of the original (replayabel) Command")
+ describedAs = "UUID of the original (replayable) Command")
public UUID getInteractionId() {
return interactionId;
}
@@ -162,20 +174,25 @@ public UUID getInteractionId() {
@PropertyLayout(
sequence = "1.2",
fieldSetId = "details",
- describedAs = "Timestamp of the original (replayabel) Command")
+ describedAs = "Timestamp of the original (replayable) Command")
public ZonedDateTime getTimestamp() {
- return commandRecord()
- .map(CommandRecord::commandDto)
- .map(CommandDto::getTimestamp)
- .map(JavaTimeXMLGregorianCalendarMarshalling::toZonedDateTime)
+ return getTimestampIfAny()
.orElse(null);
}
+ @Programmatic
+ public Optional<ZonedDateTime> getTimestampIfAny() {
+ return commandRecord()
+ .map(CommandRecord::commandDto)
+ .map(CommandDto::getTimestamp)
+ .map(JavaTimeXMLGregorianCalendarMarshalling::toZonedDateTime);
+ }
+
@Property
@PropertyLayout(
sequence = "2.1",
fieldSetId = "details",
- describedAs = "Target Type of the original (replayabel) Command")
+ describedAs = "Target Type of the original (replayable) Command")
public String getTargetType() {
return commandRecord()
.map(CommandRecord::commandDto)
@@ -188,7 +205,7 @@ public String getTargetType() {
@PropertyLayout(
sequence = "2.2",
fieldSetId = "details",
- describedAs = "Target ID of the original (replayabel) Command")
+ describedAs = "Target ID of the original (replayable) Command")
public String getTargetId() {
return commandRecord()
.map(CommandRecord::commandDto)
@@ -202,7 +219,7 @@ public String getTargetId() {
@PropertyLayout(
sequence = "3.1",
fieldSetId = "details",
- describedAs = "Replayabel Action or Property, that was executed as captured by the original Command")
+ describedAs = "Replayable Action or Property, that was executed as captured by the original Command")
public String getMember() {
return commandRecord()
.map(CommandRecord::commandDto)
@@ -217,7 +234,7 @@ public String getMember() {
@PropertyLayout(
sequence = "4",
fieldSetId = "details",
- describedAs = "Replay State of the original (replayabel) Command. "
+ describedAs = "Replay State of the original (replayable) Command. "
+ "When imported initially is PENDING. "
+ "Then after replay its either OK or FAILED. "
+ "Can be manually set to EXCLUDED, which marks it to be ignored for replay.")
@@ -233,7 +250,7 @@ public ReplayState getReplayState() {
fieldSetId = "dto",
hidden = Where.ALL_TABLES,
labelPosition = LabelPosition.NONE,
- describedAs = "DTO of the original (replayabel) Command")
+ describedAs = "DTO of the original (replayable) Command")
public AsciiDoc getDto() {
return commandRecord()
.map(CommandRecord::commandDto)
@@ -247,96 +264,59 @@ public AsciiDoc getDto() {
// -- ACTIONS
- @Action
- @ActionLayout(
- sequence = "0.2",
- describedAs = "Opens the associated Command Log Entry")
- public CommandLogEntry openCommandLogEntry() {
- return commandLogEntry()
- .orElse(null);
- }
- @Action
- @ActionLayout(
- sequence = "0.1",
- cssClassFa = "solid circle-play",
- cssClass = "btn-primary")
- //hidden = Where.NOWHERE) // show in tables //TODO NPE bug
- public ReplayableCommand replayOrRetry() {
- tryReplayOrRetry();
- return this;
- }
- @MemberSupport public String disableReplayOrRetry() {
- return commandRecord()
- .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
- .orElse(false)
- ? null
- : "Cannot replay, if neither PENDING nor FAILED";
- }
-
- @Action
- @ActionLayout(
- //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
- sequence = "2.1",
- associateWith = "replayState",
- describedAs = "Makes Command exportable (again)")
- public ReplayableCommand makeExportable() {
- if(disableMakeExportable()!=null)
- return this; // safe guard when called programmatically
+ ReplayableCommand makeExportable() {
+ if(disableMakeExportable()!=null) {
+ return this; // safeguard when called programmatically
+ }
commandLogEntry()
- .filter(commandLogEntry->ReplayState.isExported(commandLogEntry.getReplayState()))
- .ifPresent(commandLogEntry->{
- commandLogEntry.setReplayState(ReplayState.UNDEFINED);
- invalidateCachedRecord();
- });
+ .filter(commandLogEntry->ReplayState.isExported(commandLogEntry.getReplayState()))
+ .ifPresent(commandLogEntry->{
+ commandLogEntry.setReplayState(ReplayState.UNDEFINED);
+ invalidateCachedRecord();
+ });
return this;
}
- @MemberSupport public String disableMakeExportable() {
+ String disableMakeExportable() {
return commandRecord()
- .map(rec->ReplayState.isExported(rec.replayState()))
- .orElse(false)
+ .map(rec->ReplayState.isExported(rec.replayState()))
+ .orElse(false)
? null
: "Cannot make exportable, if not EXPORTED";
}
- @Action
- @ActionLayout(
- //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
- sequence = "2.2",
- associateWith = "replayState",
- describedAs = "Marks Command to be EXCLUDED from replay.")
- public ReplayableCommand excludeFromReplay() {
- if(disableExcludeFromReplay()!=null)
- return this; // safe guard when called programmatically
+
+ ReplayableCommand excludeFromReplay() {
+ if(disableExcludeFromReplay()!=null) {
+ return ReplayableCommand.this; // safeguard when called programmatically
+ }
commandLogEntry()
- .filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
- .ifPresent(commandLogEntry->{
- commandLogEntry.setReplayState(ReplayState.EXCLUDED);
- invalidateCachedRecord();
- });
- return this;
+ .filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
+ .ifPresent(commandLogEntry->{
+ commandLogEntry.setReplayState(ReplayState.EXCLUDED);
+ invalidateCachedRecord();
+ });
+ return ReplayableCommand.this;
}
- @MemberSupport private String disableExcludeFromReplay() {
+ String disableExcludeFromReplay() {
return commandRecord()
- .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
- .orElse(false)
+ .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
+ .orElse(false)
? null
: "Cannot mark for exclusion, if neither PENDING nor FAILED";
}
- @Action
- @ActionLayout(
- sequence = "0.3",
- //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
- describedAs = "Deletes the associated Command Log Entry (cannot be undone)")
- public void delete() {
+
+ @Programmatic
+ void deleteObj() {
commandLogEntry()
- .ifPresent(commandLogEntry->{
- replayContext.repositoryService().remove(commandLogEntry);
- invalidateCachedRecord();
- });
+ .ifPresent(commandLogEntry->{
+ replayContext.repositoryService().remove(commandLogEntry);
+ invalidateCachedRecord();
+ });
}
+
// -- EXECUTION ORDER GOVERNED BY TIMESTAMP
private static final Comparator<ReplayableCommand> TIMESTAMP_COMPARATOR =
@@ -357,12 +337,12 @@ public String viewModelMemento() {
// -- UTIL
Try<ReplayableCommand> tryReplayOrRetry() {
- if(disableReplayOrRetry()!=null)
+ if(disableReplayOrRetry()!=null) {
return Try.success(null); // guard against disallowed invocation
+ }
return commandLogEntry()
.filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
.map(commandLogEntry->{
- commandLogEntry.setExecuteIn(ExecuteIn.FOREGROUND);
var tryResultBookmark = replayContext.commandExecutorService().executeCommand(
InteractionContextPolicy.SWITCH_USER_AND_TIME,
commandLogEntry.getCommandDto());
@@ -379,6 +359,13 @@ Try<ReplayableCommand> tryReplayOrRetry() {
})
.orElseGet(()->Try.success(null));
}
+ String disableReplayOrRetry() {
+ return commandRecord()
+ .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
+ .orElse(false)
+ ? null
+ : "Cannot replay, if neither PENDING nor FAILED";
+ }
// -- HELPER
@@ -402,7 +389,7 @@ Optional<CommandLogEntry> commandLogEntry() {
}
private static boolean canReplayOrRetryOrMarkForExclusion(final CommandLogEntry commandLogEntry) {
- return ReplayState.canReplayOrRetryOrMarkForExclusion(commandLogEntry.getReplayState());
+ return ReplayState.isPendingOrFailed(commandLogEntry.getReplayState());
}
private void onReplayError(final UUID interactionId, final Throwable ex) {
@@ -411,5 +398,4 @@ private void onReplayError(final UUID interactionId, final Throwable ex) {
replayContext.commandLogEntryRepository().findByInteractionId(interactionId)
.ifPresent(entry->entry.saveAnalysis(ex.toString()))));
}
-
-}
+}
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml
index 0b6c5fee4d5..331644bba1a 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml
@@ -1,14 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?><bs:grid xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3" xmlns:lnk="https://causeway.apache.org/applib/layout/links" xmlns:cpt="https://causeway.apache.org/applib/layout/component" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/links https://caus [...]
<bs:row>
- <bs:col span="8" unreferencedActions="true">
+ <bs:col span="12" unreferencedActions="true">
<cpt:domainObject/>
</bs:col>
- <bs:col span="4">
+ </bs:row>
+ <bs:row>
+ <bs:col span="6">
<bs:tabGroup>
<bs:tab name="Identity">
<bs:row>
<bs:col span="12">
- <cpt:fieldSet name="Identity" id="identity"/>
+ <cpt:fieldSet name="Id" id="id">
+ <cpt:action id="delete" position="PANEL"/>
+ <cpt:property id="interactionId"/>
+ <cpt:property id="timestamp"/>
+ </cpt:fieldSet>
</bs:col>
</bs:row>
</bs:tab>
@@ -22,19 +28,29 @@
<bs:tab name="Metadata">
<bs:row>
<bs:col span="12">
- <cpt:fieldSet name="Metadata" id="metadata"/>
+ <cpt:fieldSet name="Metadata" id="metadata">
+ <cpt:property id="logicalTypeName"/>
+ <cpt:property id="interactionId"/>
+ </cpt:fieldSet>
</bs:col>
</bs:row>
</bs:tab>
</bs:tabGroup>
- </bs:col>
- </bs:row>
- <bs:row>
- <bs:col span="6">
- <cpt:fieldSet name="Details" id="details"/>
+ <cpt:fieldSet name="Target" id="target">
+ <cpt:property id="targetType"/>
+ <cpt:property id="targetId"/>
+ <cpt:property id="member"/>
+ </cpt:fieldSet>
+ <cpt:fieldSet name="Control" id="control">
+ <cpt:action id="replayOrRetry" position="PANEL"/>
+ <cpt:property id="replayState"/>
+ </cpt:fieldSet>
</bs:col>
<bs:col span="6">
- <cpt:fieldSet name="Command Dto" id="dto"/>
+ <cpt:fieldSet name="Command Dto" id="dto">
+ <cpt:action id="openCommandLogEntry" position="PANEL"/>
+ <cpt:property id="dto"/>
+ </cpt:fieldSet>
</bs:col>
</bs:row>
<bs:row>
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_delete.java
similarity index 50%
copy from extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
copy to extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_delete.java
index 60d47e0a15f..6289ab9647e 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_delete.java
@@ -18,21 +18,32 @@
*/
package org.apache.causeway.extensions.commandlog.applib.dom.replay;
-import org.apache.causeway.applib.services.command.CommandExecutorService;
-import org.apache.causeway.applib.services.iactnlayer.InteractionService;
-import org.apache.causeway.applib.services.repository.RepositoryService;
-import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
+import lombok.RequiredArgsConstructor;
-import lombok.Value;
-import lombok.experimental.Accessors;
+import org.apache.causeway.applib.annotation.*;
-/**
- * Bundles dependencies for the replay logic.
- */
-@Value @Accessors(fluent = true)
-public final class ReplayContext {
- RepositoryService repositoryService;
- InteractionService interactionService;
- CommandLogEntryRepository commandLogEntryRepository;
- CommandExecutorService commandExecutorService;
+@Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.NON_IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = ReplayableCommand_delete.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+ sequence = "0.3",
+ //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
+ describedAs = "Deletes the associated Command Log Entry (cannot be undone)"
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_delete {
+
+ public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_delete> {
+ }
+
+ private final ReplayableCommand replayableCommand;
+
+ @MemberSupport
+ public void act() {
+ replayableCommand.deleteObj();
+ }
}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_excludeFromReplay.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_excludeFromReplay.java
new file mode 100644
index 00000000000..955c7ef4c2d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_excludeFromReplay.java
@@ -0,0 +1,54 @@
+/*
+ * 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.extensions.commandlog.applib.dom.replay;
+
+import lombok.RequiredArgsConstructor;
+
+import org.apache.causeway.applib.annotation.*;
+
+@Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = ReplayableCommand_excludeFromReplay.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+ //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
+ sequence = "2.2",
+ associateWith = "replayState",
+ describedAs = "Marks Command to be EXCLUDED from replay."
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_excludeFromReplay {
+
+ public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_excludeFromReplay> {
+ }
+
+ private final ReplayableCommand replayableCommand;
+
+ @MemberSupport
+ public ReplayableCommand act() {
+ return replayableCommand.excludeFromReplay();
+ }
+
+ @MemberSupport
+ private String disableAct() {
+ return replayableCommand.disableExcludeFromReplay();
+ }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_makeExportable.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_makeExportable.java
new file mode 100644
index 00000000000..591746fa0c9
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_makeExportable.java
@@ -0,0 +1,55 @@
+/*
+ * 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.extensions.commandlog.applib.dom.replay;
+
+import lombok.RequiredArgsConstructor;
+
+import org.apache.causeway.applib.annotation.*;
+
+@Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = ReplayableCommand_makeExportable.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+ //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
+ associateWith = "replayState",
+ sequence = "2.1",
+ describedAs = "Makes Command exportable (again)"
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_makeExportable {
+
+ public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_makeExportable> {
+ }
+
+ private final ReplayableCommand replayableCommand;
+
+ @MemberSupport
+ public ReplayableCommand act() {
+ return replayableCommand.makeExportable();
+ }
+
+ @MemberSupport
+ public String disableAct() {
+ return replayableCommand.disableMakeExportable();
+ }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_openCommandLogEntry.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_openCommandLogEntry.java
new file mode 100644
index 00000000000..600744eab9c
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_openCommandLogEntry.java
@@ -0,0 +1,54 @@
+/*
+ * 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.extensions.commandlog.applib.dom.replay;
+
+import lombok.RequiredArgsConstructor;
+
+import org.apache.causeway.applib.annotation.*;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
+
+@Action(
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = ReplayableCommand_openCommandLogEntry.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+ sequence = "0.2",
+ describedAs = "Opens the underlying Command Log Entry"
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_openCommandLogEntry {
+
+ public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_openCommandLogEntry> {
+ }
+
+ private final ReplayableCommand replayableCommand;
+
+ @MemberSupport
+ public CommandLogEntry act() {
+ return replayableCommand.commandLogEntry()
+ .orElse(null);
+ }
+
+ @MemberSupport
+ public String disableAct() {
+ return replayableCommand.commandLogEntry().isEmpty() ? "No corresponding CommandLogEntry" : null;
+ }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_replayOrRetry.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_replayOrRetry.java
new file mode 100644
index 00000000000..e2847773951
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_replayOrRetry.java
@@ -0,0 +1,57 @@
+/*
+ * 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.extensions.commandlog.applib.dom.replay;
+
+import lombok.RequiredArgsConstructor;
+
+import org.apache.causeway.applib.annotation.*;
+
+@Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.NON_IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = ReplayableCommand_replayOrRetry.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+ sequence = "0.1",
+ cssClassFa = "solid circle-play",
+ cssClass = "btn-primary"
+ //hidden = Where.NOWHERE // show in tables //TODO NPE bug
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_replayOrRetry {
+
+ public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_replayOrRetry> {
+ }
+
+ private final ReplayableCommand replayableCommand;
+
+
+ @MemberSupport
+ public ReplayableCommand act() {
+ replayableCommand.tryReplayOrRetry();
+ return replayableCommand;
+ }
+
+ @MemberSupport
+ public String disableAct() {
+ return replayableCommand.disableReplayOrRetry();
+ }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/TimestampMarshallUtil.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/TimestampMarshallUtil.java
new file mode 100644
index 00000000000..ba8eaab85a4
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/TimestampMarshallUtil.java
@@ -0,0 +1,48 @@
+/*
+ * 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.extensions.commandlog.applib.dom.replay;
+
+import lombok.experimental.UtilityClass;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+
+@UtilityClass
+class TimestampMarshallUtil {
+ private static final java.time.format.DateTimeFormatter VM_MEMENTO_FORMATTER =
+ java.time.format.DateTimeFormatter
+ .ofPattern("uuuu-MM-dd'T'HH-mm-ss.SSSX")
+ .withZone(java.time.ZoneOffset.UTC);
+
+ static String toString(Timestamp ts) {
+ // Human-readable and URL-friendly (no ':' or '~').
+ return VM_MEMENTO_FORMATTER.format(ts.toInstant()); // e.g. 2026-04-22T02-00-00.000Z
+ }
+
+ static Timestamp fromString(String s, Timestamp fallback) {
+ if (s == null || s.isBlank()) {
+ return fallback;
+ }
+ try {
+ return Timestamp.from(Instant.from(VM_MEMENTO_FORMATTER.parse(s)));
+ } catch (Exception e) {
+ return fallback;
+ }
+ }
+}
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
index f1644491016..449cef754ec 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
@@ -369,7 +369,7 @@ void test_all_the_repository_methods() {
val to = from.plusDays(1);
// when
- List<? extends CommandLogEntry> notYetReplayed = commandLogEntryRepository.findReplayPendingOrFailed();
+ List<? extends CommandLogEntry> notYetReplayed = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(baseline);
// then
Assertions.assertThat(notYetReplayed).isEmpty();
@@ -382,7 +382,7 @@ void test_all_the_repository_methods() {
commandTarget1User1.setReplayState(ReplayState.PENDING);
// when
- List<? extends CommandLogEntry> notYetReplayed2 = commandLogEntryRepository.findReplayPendingOrFailed();
+ List<? extends CommandLogEntry> notYetReplayed2 = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(baseline);
// then
Assertions.assertThat(notYetReplayed2).hasSize(1);
diff --git a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java
index 520b5cb37d9..49513f3859d 100644
--- a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java
+++ b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java
@@ -206,10 +206,20 @@
+ " RANGE 0,2"), // this should be RANGE 0,1 but results in DataNucleus submitting "FETCH NEXT ROW ONLY"
// which SQL Server doesn't understand. However, as workaround, SQL Server *does* understand FETCH NEXT 2 ROWS ONLY
@Query(
- name = Nq.FIND_BY_REPLAY_STATE,
+ name = Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE,
value = "SELECT "
+ " FROM " + CommandLogEntry.FQCN + " "
- + " WHERE replayState == :replayState1 || replayState == :replayState2 "
+ + " WHERE executeIn == 'FOREGROUND' "
+ + " && timestamp >= :from "
+ + " && replayState == :replayState "
+ + " ORDER BY timestamp ASC"),
+ @Query(
+ name = Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES,
+ value = "SELECT "
+ + " FROM " + CommandLogEntry.FQCN + " "
+ + " WHERE executeIn == 'FOREGROUND' "
+ + " && timestamp >= :from "
+ + " && (replayState == :replayState1 || replayState == :replayState2) "
+ " ORDER BY timestamp ASC"),
})
@Named(CommandLogEntry.LOGICAL_TYPE_NAME)
diff --git a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
index ffc6b5ade4a..8cc67652432 100644
--- a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
+++ b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
@@ -209,10 +209,20 @@
+ " AND cl.completedAt is not null "
+ " ORDER BY cl.timestamp DESC"), // programmatic LIMIT 1
@NamedQuery(
- name = Nq.FIND_BY_REPLAY_STATE,
+ name = Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE,
query = "SELECT cl "
+ " FROM CommandLogEntry cl "
- + " WHERE (cl.replayState = :replayState1 OR cl.replayState = :replayState2) "
+ + " WHERE cl.executeIn = org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn.FOREGROUND "
+ + " AND cl.timestamp >= :from "
+ + " AND cl.replayState = :replayState "
+ + " ORDER BY cl.timestamp ASC"),
+ @NamedQuery(
+ name = Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES,
+ query = "SELECT cl "
+ + " FROM CommandLogEntry cl "
+ + " WHERE cl.executeIn = org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn.FOREGROUND "
+ + " AND cl.timestamp >= :from "
+ + " AND (cl.replayState = :replayState1 OR cl.replayState = :replayState2) "
+ " ORDER BY cl.timestamp ASC"),
})
@Named(CommandLogEntry.LOGICAL_TYPE_NAME)