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/21 21:04:56 UTC
(causeway) branch JDOJPA-281 updated: JDOJPA-281: polishing
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch JDOJPA-281
in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/JDOJPA-281 by this push:
new a8fcb9ca740 JDOJPA-281: polishing
a8fcb9ca740 is described below
commit a8fcb9ca740d7b4a675726a0a1753a03b29c43ef
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Tue Apr 21 22:04:31 2026 +0100
JDOJPA-281: polishing
---
.../commandlog/applib/app/CommandLogMenu.java | 23 ++-
...ExportManager#exported.columnOrder.fallback.txt | 6 +
...Manager#notYetExported.columnOrder.fallback.txt | 6 +
.../applib/dom/replay/CommandExportManager.java | 178 ++++++++++++++++---
.../CommandExportManager.layout.fallback.xml | 8 +
...anager#pendingOrFailed.columnOrder.fallback.txt | 6 +
...er#succeededOrExcluded.columnOrder.fallback.txt | 6 +
.../applib/dom/replay/CommandReplayManager.java | 182 +++++++++++++------
.../ReplayableCommand.columnOrder.fallback.txt | 6 +
.../applib/dom/replay/ReplayableCommand.java | 197 +++++++++++++++------
.../replay/ReplayableCommand.layout.fallback.xml | 33 +++-
11 files changed, 503 insertions(+), 148 deletions(-)
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 a888729eb72..323b7590cc1 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,6 +18,8 @@
*/
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;
@@ -33,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;
@@ -44,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;
@@ -163,15 +170,23 @@ public class DomainEvent extends ActionDomainEvent<findAll> { }
public class exportManager {
public class DomainEvent extends ActionDomainEvent<exportManager> { }
- @MemberSupport public CommandExportManager act(final java.sql.Timestamp baseline) {
- return new CommandExportManager(baseline, 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 defaultBaseline() {
- return clockService.getClock().nowAsJavaSqlTimestamp();
+ @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 hours) {
+ return Timestamp.from(now.toInstant().truncatedTo(hours));
+ }
+
@Action(
commandPublishing = Publishing.DISABLED,
domainEvent = replayManager.DomainEvent.class,
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 098f4161ced..7a0a7e9dda2 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,8 @@
*/
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;
@@ -38,6 +40,10 @@
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,6 +54,8 @@
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState;
+import org.jspecify.annotations.NonNull;
+
@DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
@DomainObjectLayout(cssClassFa = "solid share-from-square")
@Named(CommandExportManager.LOGICAL_TYPE_NAME)
@@ -55,9 +63,10 @@ public final class CommandExportManager implements ViewModel {
public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager";
- private ReplayContext replayContext;
+ public static abstract class ActionDomainEvent<T>
+ extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
- private java.sql.Timestamp baseline;
+ private ReplayContext replayContext;
@Inject
public CommandExportManager(
@@ -67,9 +76,9 @@ public CommandExportManager(
}
public CommandExportManager(
- final java.sql.Timestamp baseline,
+ final java.sql.Timestamp since,
final ReplayContext replayContext) {
- this.baseline = baseline;
+ this.since = since;
this.replayContext = replayContext;
}
@@ -78,20 +87,102 @@ public CommandExportManager(
}
+ @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",
+ position = ActionLayout.Position.PANEL,
+ describedAs = "Move back one hour"
+ )
+ public class previousHour {
+ public class DomainEvent extends ActionDomainEvent<previousHour> { }
+
+ @MemberSupport public CommandExportManager act(final java.sql.Timestamp since) {
+ return new CommandExportManager(addSeconds(since, -3600), replayContext);
+ }
+ @MemberSupport public java.sql.Timestamp defaultSince() {
+ return CommandExportManager.this.since;
+ }
+ }
+
+ @Action(
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = nextHour.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "since", sequence = "3",
+ position = ActionLayout.Position.PANEL,
+ describedAs = "Move forward one hour"
+ )
+ public class nextHour {
+ public class DomainEvent extends ActionDomainEvent<nextHour> { }
+ @MemberSupport public CommandExportManager act(final java.sql.Timestamp since) {
+ return new CommandExportManager(addSeconds(since, +3600), replayContext);
+ }
+ @MemberSupport public java.sql.Timestamp defaultSince() {
+ return CommandExportManager.this.since;
+ }
+ }
+
+ @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 @NonNull Timestamp addSeconds(Timestamp since, int secondsToAdd) {
+ return Timestamp.from(since.toInstant().plusSeconds(secondsToAdd));
+ }
+
+
// -- NOT YET EXPORTED
@Collection
@CollectionLayout(
describedAs = "Commands that can be exported (since baseline)")
public List<ReplayableCommand> getNotYetExported() {
- return commandLogEntryRepository().findSinceAndCanBeExported(baseline).stream()
+ return commandLogEntryRepository().findSinceAndCanBeExported(since).stream()
.map(entry->new ReplayableCommand(
entry.getInteractionId(),
replayContext))
.collect(Collectors.toList());
}
- @Action(choicesFrom = "notYetExported", semantics = SemanticsOf.NON_IDEMPOTENT)
+ @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",
@@ -99,9 +190,13 @@ public List<ReplayableCommand> getNotYetExported() {
describedAs = "Exports selected Commands as zipped DTOs for import later. "
+ "(You need to refresh the page to see changed states.)")
public class exportSelected {
+ public class DomainEvent extends ActionDomainEvent<exportSelected> { }
- @MemberSupport
- public Blob act(final List<ReplayableCommand> selected) {
+ @MemberSupport public Blob act(
+ final List<ReplayableCommand> selected,
+ final String filenamePrefix,
+ final boolean filenameTimestamp
+ ) {
var selectedCommandLogEntries = selected.stream()
.map(ReplayableCommand::commandLogEntry)
@@ -117,12 +212,14 @@ public Blob act(final List<ReplayableCommand> selected) {
.collect(Collectors.toList()));
final var replayableCommand = selected.get(0); // validate ensures there is at least one command
- final var timestamp = replayableCommand.getTimestampIfAny()
- .map(ChronoZonedDateTime::toInstant)
- .map(Instant::toString)
- .map(x -> "." + x.replaceAll("[^A-Za-z0-9._-]", "_")) // make safe within filename
- .orElse("");
- final var filename = "commands" + timestamp + ".yaml";
+ 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 + ".yaml";
var blob = Clob.of(filename, CommonMimeType.YAML, yaml)
.toBlobUtf8()
@@ -134,8 +231,19 @@ public Blob act(final List<ReplayableCommand> selected) {
return blob;
}
- @MemberSupport
- public String validateSelected(final List<ReplayableCommand> selected) {
+ @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;
}
}
@@ -147,26 +255,43 @@ public String validateSelected(final List<ReplayableCommand> selected) {
@CollectionLayout(
describedAs = "Commands that have been exported")
public List<ReplayableCommand> getExported() {
- return commandLogEntryRepository().findSinceAndHasBeenExported(baseline).stream()
+ return commandLogEntryRepository().findSinceAndHasBeenExported(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)")
+
+ @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 to export" : null;
+ return selected != null && selected.isEmpty() ? "Select at least one command to make exportable" : null;
}
}
@@ -175,28 +300,27 @@ public String validateSelected(final List<ReplayableCommand> selected) {
@Override
public String viewModelMemento() {
- return toString(this.baseline);
+ return toString(this.since);
}
private static String toString(Timestamp ts) {
- return ts.toInstant().toString(); // e.g. 2026-04-20T18:30:15.123Z
+ // Uses ISO-8601 shape, but replace ':' with '~' to avoid URL encoding noise.
+ return ts.toInstant().toString().replace(':', '~'); // e.g. 2026-04-20T18~30~15.123Z
}
private static Timestamp fromString(String s, Timestamp fallback) {
- if(s == null) {
+ if (s == null) {
return fallback;
}
try {
- return Timestamp.from(Instant.parse(s));
+ return Timestamp.from(Instant.parse(s.replace('~', ':')));
} catch (Exception e) {
return fallback;
}
}
-
// -- 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..74c6b4db908 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
@@ -3,6 +3,14 @@
<bs:col span="8" unreferencedActions="true">
<cpt:domainObject/>
</bs:col>
+ <bs:col span="4">
+ <cpt:fieldSet name=" " id="since">
+ <cpt:action id="previousHour"/>
+ <cpt:action id="changeTo"/>
+ <cpt:action id="nextHour"/>
+ <cpt:property id="since"/>
+ </cpt:fieldSet>
+ </bs:col>
<bs:col span="4">
<bs:tabGroup>
<bs:tab name="Identity">
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 eba8d52a3ea..0177d4cbb2a 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
@@ -32,9 +32,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.Parameter;
import org.apache.causeway.applib.annotation.Programmatic;
+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;
@@ -44,8 +47,6 @@
import org.apache.causeway.schema.cmd.v2.CommandDto;
import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.experimental.Accessors;
@DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
@DomainObjectLayout(cssClassFa = "solid circle-play")
@@ -55,7 +56,10 @@ public final class CommandReplayManager implements ViewModel {
public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayManager";
- private final ReplayContext replayContext;
+ public static abstract class ActionDomainEvent<T>
+ extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+ private final ReplayContext replayContext;
@Programmatic
public ReplayContext replayContext() { return replayContext; }
@@ -71,22 +75,33 @@ public CommandReplayManager(
return "Command Replay Manager";
}
- @Action
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = importCommands.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
@ActionLayout(
sequence = "0.1",
associateWith = "pendingOrFailed",
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) {
+ 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();
+ var yamlDs = zippedCommandsYaml.unZip(CommonMimeType.YAML).asDataSource();
- final List<CommandDto> commandDtos = CommandDtoUtils.fromYaml(yamlDs);
- commandDtos.forEach(commandLogEntryRepository()::saveForReplay);
+ final List<CommandDto> commandDtos = CommandDtoUtils.fromYaml(yamlDs);
+ commandDtos.forEach(commandLogEntryRepository()::saveForReplay);
- return this;
+ return CommandReplayManager.this;
+ }
}
@@ -94,7 +109,8 @@ public CommandReplayManager importCommands(
@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()
.map(entry->new ReplayableCommand(
@@ -103,51 +119,96 @@ public List<ReplayableCommand> getPendingOrFailed() {
.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;
}
- 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;
+ }
}
- @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::delete); // filtered on its own responsibility
+ return CommandReplayManager.this;
+ }
}
+
+
// -- 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()
.map(entry->new ReplayableCommand(
@@ -156,16 +217,30 @@ public List<ReplayableCommand> getSucceededOrExcluded() {
.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::delete); // filtered on its own responsibility
+ return CommandReplayManager.this;
+ }
}
+
+
// -- VM STATE
@Override
@@ -179,5 +254,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/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 6ab05399b62..48c044edd67 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;
@@ -39,6 +41,9 @@
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;
@@ -72,7 +77,10 @@
@AllArgsConstructor
public final class ReplayableCommand implements ViewModel, Comparable<ReplayableCommand> {
- private final UUID interactionId;
+ public static abstract class ActionDomainEvent<T>
+ extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+ private final UUID interactionId;
@Programmatic
public UUID interactionId() { return interactionId; }
@@ -136,7 +144,8 @@ 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
@@ -256,96 +265,176 @@ public AsciiDoc getDto() {
// -- ACTIONS
- @Action
+ @Action(
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = openCommandLogEntry.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
@ActionLayout(
sequence = "0.2",
- describedAs = "Opens the associated Command Log Entry")
- public CommandLogEntry openCommandLogEntry() {
- return commandLogEntry()
- .orElse(null);
+ describedAs = "Opens the associated Command Log Entry"
+ )
+ public class openCommandLogEntry {
+ public class DomainEvent extends ActionDomainEvent<openCommandLogEntry> { }
+ @MemberSupport public CommandLogEntry act() {
+ return commandLogEntry()
+ .orElse(null);
+ }
+ @MemberSupport public String disableAct() {
+ return commandLogEntry().isEmpty() ? "No corresponding CommandLogEntry" : null;
+ }
}
- @Action
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.NON_IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = 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
- public ReplayableCommand replayOrRetry() {
- tryReplayOrRetry();
- return this;
+ cssClass = "btn-primary"
+ //hidden = Where.NOWHERE // show in tables //TODO NPE bug
+ )
+ public class replayOrRetry {
+ public class DomainEvent extends ActionDomainEvent<replayOrRetry> { }
+ @MemberSupport public ReplayableCommand act() {
+ tryReplayOrRetry();
+ return ReplayableCommand.this;
+ }
+ @MemberSupport public String disableAct() {
+ return disableReplayOrRetry();
+ }
}
- @MemberSupport public String disableReplayOrRetry() {
+ private String disableReplayOrRetry() {
return commandRecord()
- .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
- .orElse(false)
+ .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
+ .orElse(false)
? null
: "Cannot replay, if neither PENDING nor FAILED";
}
- @Action
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = makeExportable.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
@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
+ sequence = "2.1",
+ describedAs = "Makes Command exportable (again)"
+ )
+ public class makeExportable {
+ public class DomainEvent extends ActionDomainEvent<makeExportable> { }
+
+ @MemberSupport public ReplayableCommand act() {
+ return makeExportable();
+ }
+ @MemberSupport public String disableAct() {
+ return disableMakeExportable();
+ }
+ }
+
+ 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
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = 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.")
- public ReplayableCommand excludeFromReplay() {
- if(disableExcludeFromReplay()!=null)
- return this; // safe guard when called programmatically
+ describedAs = "Marks Command to be EXCLUDED from replay."
+ )
+ public class excludeFromReplay {
+ public class DomainEvent extends ActionDomainEvent<excludeFromReplay> { }
+
+ @MemberSupport public ReplayableCommand act() {
+ return excludeFromReplay();
+ }
+ @MemberSupport private String disableAct() {
+ return disableExcludeFromReplay();
+ }
+ }
+
+ 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() {
+ private 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
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.NON_IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = 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)")
- public void delete() {
+ describedAs = "Deletes the associated Command Log Entry (cannot be undone)"
+ )
+ public class delete {
+ public class DomainEvent extends ActionDomainEvent<delete> { }
+ @MemberSupport public void act() {
+ delete();
+ }
+ }
+ void delete() {
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 =
@@ -366,8 +455,9 @@ 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->{
@@ -420,5 +510,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..4191657298a 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,22 @@
<?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:action id="openCommandLogEntry"/>
+ </cpt:property>
+ <cpt:property id="timestamp"/>
+ </cpt:fieldSet>
</bs:col>
</bs:row>
</bs:tab>
@@ -22,16 +30,23 @@
<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"/>