You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2026/04/27 07:35:11 UTC
(causeway) 01/01: CAUSEWAY-3989: forwardport of replay managers v2->v4
This is an automated email from the ASF dual-hosted git repository.
andi-huber pushed a commit to branch 3989-replay.fwport
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 246d56bcb054ab978a7ad00753a6350c4b544638
Author: andi-huber <ah...@apache.org>
AuthorDate: Mon Apr 27 09:34:59 2026 +0200
CAUSEWAY-3989: forwardport of replay managers v2->v4
---
.../applib/CausewayModuleExtCommandLogApplib.java | 34 +-
.../commandlog/applib/app/CommandLogMenu.java | 36 +-
.../commandlog/applib/dom/CommandLogEntry.java | 4 +-
.../applib/dom/CommandLogEntryRepository.java | 24 +-
.../dom/CommandLogEntryRepositoryAbstract.java | 73 ++--
.../commandlog/applib/dom/ReplayState.java | 35 +-
...ExportManager#exported.columnOrder.fallback.txt | 6 +
...Manager#notYetExported.columnOrder.fallback.txt | 6 +
.../applib/dom/replay/CommandExportManager.java | 297 ++++++++++++-----
.../CommandExportManager.layout.fallback.xml | 12 +-
...anager#pendingOrFailed.columnOrder.fallback.txt | 6 +
...er#succeededOrExcluded.columnOrder.fallback.txt | 6 +
.../applib/dom/replay/CommandReplayManager.java | 371 ++++++++++++++++-----
.../CommandReplayManager.layout.fallback.xml | 12 +-
.../applib/dom/replay/ReplayContext.java | 6 +-
.../ReplayableCommand.columnOrder.fallback.txt | 6 +
.../applib/dom/replay/ReplayableCommand.java | 224 ++++++-------
.../replay/ReplayableCommand.layout.fallback.xml | 36 +-
...yContext.java => ReplayableCommand_delete.java} | 39 ++-
.../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 | 5 +-
.../commandlog/jpa/dom/CommandLogEntry.java | 14 +-
26 files changed, 1156 insertions(+), 364 deletions(-)
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 e6a467b836a..8cdf6f7877e 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
@@ -22,9 +22,11 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
+import org.apache.causeway.applib.services.clock.ClockService;
import org.apache.causeway.applib.services.command.CommandExecutorService;
import org.apache.causeway.applib.services.iactn.InteractionService;
import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.applib.services.xactn.TransactionService;
import org.apache.causeway.core.config.util.SpringProfileUtil;
import org.apache.causeway.extensions.commandlog.applib.app.CommandLogMenu;
import org.apache.causeway.extensions.commandlog.applib.contributions.HasInteractionId_commandLogEntry;
@@ -36,8 +38,14 @@
import org.apache.causeway.extensions.commandlog.applib.dom.mixins.CommandLogEntry_childCommands;
import org.apache.causeway.extensions.commandlog.applib.dom.mixins.CommandLogEntry_openResultObject;
import org.apache.causeway.extensions.commandlog.applib.dom.mixins.CommandLogEntry_siblingCommands;
+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;
@@ -59,6 +67,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,
@@ -119,10 +145,12 @@ public static void honorSystemEnvironment() {
@Bean ReplayContext replayContext(
final RepositoryService repositoryService,
final InteractionService interactionService,
+ final TransactionService transactionService,
final CommandLogEntryRepository commandLogEntryRepository,
- final CommandExecutorService commandExecutorService) {
- return new ReplayContext(repositoryService, interactionService,
- commandLogEntryRepository, commandExecutorService);
+ final CommandExecutorService commandExecutorService,
+ final ClockService clockService) {
+ return new ReplayContext(repositoryService, interactionService, transactionService,
+ 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 0e33e4e7c57..8dafea89645 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,10 @@
*/
package org.apache.causeway.extensions.commandlog.applib.app;
+import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
import java.util.List;
import jakarta.inject.Inject;
@@ -32,6 +34,7 @@
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.Publishing;
import org.apache.causeway.applib.annotation.RestrictTo;
@@ -156,8 +159,19 @@ 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);
}
}
@@ -172,11 +186,25 @@ 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 Timestamp truncatedTo(final Timestamp now, final ChronoUnit chronoUnit) {
+ return Timestamp.from(now.toInstant().truncatedTo(chronoUnit));
+ }
private LocalDate now() {
return clockService.getClock().nowAsLocalDate(ZoneId.systemDefault());
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 efae57536c3..99a167025f0 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
@@ -144,7 +144,9 @@ 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 d067f446e76..3cc377148c1 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;
@@ -120,6 +121,20 @@ List<CommandLogEntry> findByTargetAndFromAndTo(
*/
List<CommandLogEntry> findSince(UUID interactionId, Integer batchSize);
+ List<CommandLogEntry> findForegroundSinceTimestampAndCanBeExported(Timestamp since);
+
+ List<CommandLogEntry> findForegroundSinceTimestampAndHasBeenExported(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.
*
@@ -158,15 +173,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(CommandDto dto);
default List<CommandLogEntry> saveForReplay(@Nullable final List<CommandDto> commandDtoList) {
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 d7e8cb391a9..d4656396785 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
@@ -332,44 +332,13 @@ 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) {
-
-//TODO why?
-// if(commandToReplay.getMember().getInteractionType() == InteractionType.ACTION_INVOCATION) {
-// final MapDto userData = commandToReplay.getUserData();
-// if (userData == null )
-// throw new IllegalStateException(String.format(
-// "Can only persist action DTOs with additional userData; got: \n%s",
-// CommandDtoUtils.dtoMapper().toString(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);
@@ -425,6 +394,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(final Timestamp from, final 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(final Timestamp from, final ReplayState replayState1, final 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 932dfa7394f..5be703106dd 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,40 @@ 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;
}
+ public boolean isOkOrExcluded() {
+ return this == ReplayState.OK
+ || this == ReplayState.EXCLUDED;
+ }
+
// -- 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 isExported(final @Nullable ReplayState replayState) {
+ return replayState != null
+ && replayState.isExported();
}
- 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 isExported(final ReplayState replayState) {
- return replayState!=null
- ? replayState.isExported()
- : false;
+ public static boolean isOkOrExcluded(final @Nullable ReplayState replayState) {
+ return replayState != null
+ && replayState.isOkOrExcluded();
}
}
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 878f545f81d..bf9e2dbe94f 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,8 +18,12 @@
*/
package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+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;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -32,7 +36,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;
@@ -43,135 +52,267 @@
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;
+
+import lombok.Getter;
+
@DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
@DomainObjectLayout(cssClassFa = "solid share-from-square")
@Named(CommandExportManager.LOGICAL_TYPE_NAME)
-public record CommandExportManager(
- ReplayContext replayContext) implements ViewModel {
+public final class CommandExportManager implements ViewModel {
+
+ public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager";
- public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager";
+ 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(
+ 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(
- sequence = "0.1",
- describedAs = "Deletes all commands, regardless of state (cannot be undone)")
- public CommandExportManager deleteAll() {
- commandLogEntryRepository().removeAll();
- return this;
+ 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(final Timestamp since, final 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))
- .toList();
+ .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()
- .toList();
-
- var yaml = CommandDtoUtils.toMultiDocYaml(
- selectedCommandLogEntries.stream()
- .filter(entry->!ReplayState.isExported(entry.getReplayState()))
- .map(CommandLogEntry::getCommandDto)
- .toList());
-
- var blob = Clob.of("commands.yaml", CommonMimeType.YAML, yaml)
- .toBlobUtf8()
- .zip();
-
- // do this last once we have successfully created the Clob
- selectedCommandLogEntries.forEach(c->c.setReplayState(ReplayState.EXPORTED));
-
- return blob;
- }
+ @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> { }
- @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;
+ @MemberSupport public Blob act(
+ final List<ReplayableCommand> selected,
+ final String filenamePrefix,
+ final boolean filenameTimestamp
+ ) {
+
+ var selectedCommandLogEntries = selected.stream()
+ .map(ReplayableCommand::commandLogEntry)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .sorted()
+ .collect(Collectors.toList());
+
+ 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()));
+
+ 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();
+
+ // 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;
+ }
+
+ // 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))
- .toList();
+ .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 f734e487f96..d7fdb81a42d 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,7 +18,9 @@
*/
package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+import java.sql.Timestamp;
import java.util.List;
+import java.util.stream.Collectors;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -31,144 +33,360 @@
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;
-import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
import org.apache.causeway.schema.cmd.v2.CommandDto;
+import static org.apache.causeway.extensions.commandlog.applib.dom.replay.TimestampMarshallUtil.fromString;
+
+import lombok.Getter;
+
@DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
@DomainObjectLayout(cssClassFa = "solid circle-play")
@Named(CommandReplayManager.LOGICAL_TYPE_NAME)
-public record CommandReplayManager(
- ReplayContext replayContext) implements ViewModel {
+public final class CommandReplayManager implements ViewModel {
public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayManager";
+ 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);
+ }
+ }
- return this;
+ @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;
+ }
}
- @Action(semantics = SemanticsOf.IDEMPOTENT_ARE_YOU_SURE)
+ private static Timestamp addSeconds(final Timestamp since, final int secondsToAdd) {
+ return Timestamp.from(since.toInstant().plusSeconds(secondsToAdd));
+ }
+
+ @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 = ".yml")
+ final Blob commandsYaml) {
+ var yamlDs = commandsYaml.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))
- .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()
- .toList();
- for(var replayableCommand : replayables) {
- var tryReplayOrRetry = replayableCommand.tryReplayOrRetry(); // filtered on its own responsibility
- if(tryReplayOrRetry.isFailure()) {
- return this; // stop further execution
- }
- }
- 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(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;
+ .collect(Collectors.toList());
+ }
+
+
+ @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();
+ }
}
+
+
+ @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(
+ 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))
- .toList();
+ .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
@@ -176,5 +394,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 fb65cb4a511..937599bb12a 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,9 +18,11 @@
*/
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.iactn.InteractionService;
import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.applib.services.xactn.TransactionService;
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
/**
@@ -29,6 +31,8 @@
public record ReplayContext(
RepositoryService repositoryService,
InteractionService interactionService,
+ TransactionService transactionService,
CommandLogEntryRepository commandLogEntryRepository,
- CommandExecutorService commandExecutorService) {
+ 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 3428266fc05..88ed8da2de2 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,9 +18,10 @@
*/
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.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.Executors;
@@ -28,15 +29,15 @@
import jakarta.inject.Inject;
import jakarta.inject.Named;
+import org.springframework.transaction.annotation.Propagation;
+
import org.apache.causeway.applib.ViewModel;
-import org.apache.causeway.applib.annotation.Action;
-import org.apache.causeway.applib.annotation.ActionLayout;
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.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.Where;
@@ -51,7 +52,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;
@@ -60,38 +60,55 @@
import org.apache.causeway.valuetypes.asciidoc.builder.AsciiDocBuilder;
import org.apache.causeway.valuetypes.asciidoc.builder.AsciiDocFactory;
+import lombok.AllArgsConstructor;
+import lombok.Value;
+import lombok.experimental.Accessors;
+
/**
* Viewmodel that wraps a {@link CommandLogEntry}.
*/
-@DomainObject(introspection = Introspection.ENCAPSULATION_ENABLED)
-@DomainObjectLayout(cssClassFa = "terminal")
+@DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
+@DomainObjectLayout//(cssClassFa = "terminal")
@Named(ReplayableCommand.LOGICAL_TYPE_NAME)
-public record ReplayableCommand(
- UUID interactionId,
- ReplayContext replayContext,
- ObjectReference<CommandRecord> recordRef) implements ViewModel, Comparable<ReplayableCommand> {
+@AllArgsConstructor
+public final class ReplayableCommand implements ViewModel, Comparable<ReplayableCommand> {
+
+ public static abstract class ActionDomainEvent<T>
+ extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+ private final UUID interactionId;
+ @Programmatic
+ public UUID interactionId() { return interactionId; }
+
+ private final ReplayContext replayContext;
+ @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
- record CommandRecord(
- CommandDto commandDto,
- ReplayState replayState) {
- CommandRecord {
- Objects.requireNonNull(commandDto);
- Objects.requireNonNull(replayState);
- }
+ @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() {
- return switch(replayState) {
+ return switch (replayState) {
case UNDEFINED -> "solid terminal .col-indigo";
- case EXPORTED -> "solid terminal .col-indigo, solid circle-arrow-right .ov-size-80 .ov-right-45 .ov-bottom-45 .col-dodgerblue";
- case PENDING -> "solid terminal .col-indigo, solid circle-pause .ov-size-80 .ov-right-45 .ov-bottom-45 .col-gold";
- case OK -> "solid terminal .col-indigo, solid circle-check .ov-size-80 .ov-right-45 .ov-bottom-45 .col-green";
- case FAILED -> "solid terminal .col-indigo, solid circle-exclamation .ov-size-80 .ov-right-45 .ov-bottom-45 .col-red";
- case EXCLUDED -> "solid terminal .col-indigo, solid circle-xmark .ov-size-80 .ov-right-45 .ov-bottom-45 .col-grey";
+ case EXPORTED -> "solid terminal .col-indigo, solid circle-arrow-right .ov-size-80 .ov-right-45 .ov-bottom-45 .col-dodgerblue";
+ case PENDING -> "solid terminal .col-indigo, solid circle-pause .ov-size-80 .ov-right-45 .ov-bottom-45 .col-gold";
+ case OK -> "solid terminal .col-indigo, solid circle-check .ov-size-80 .ov-right-45 .ov-bottom-45 .col-green";
+ case FAILED -> "solid terminal .col-indigo, solid circle-exclamation .ov-size-80 .ov-right-45 .ov-bottom-45 .col-red";
+ case EXCLUDED -> "solid terminal .col-indigo, solid circle-xmark .ov-size-80 .ov-right-45 .ov-bottom-45 .col-grey";
};
}
}
@@ -110,7 +127,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;
}
@ObjectSupport public ObjectSupport.IconResource icon(final ObjectSupport.IconSize iconSize) {
@@ -125,29 +143,34 @@ 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();
+ return interactionId;
}
@Property
@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)
@@ -160,7 +183,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)
@@ -174,7 +197,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)
@@ -189,7 +212,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.")
@@ -205,7 +228,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)
@@ -219,96 +242,57 @@ 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 private 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() {
+ ReplayableCommand makeExportable() {
if(disableMakeExportable()!=null)
- return this; // safe guard when called programmatically
+ 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 private 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() {
+
+ ReplayableCommand excludeFromReplay() {
if(disableExcludeFromReplay()!=null)
- return this; // safe guard when called programmatically
+ 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 =
@@ -334,10 +318,14 @@ Try<ReplayableCommand> tryReplayOrRetry() {
return commandLogEntry()
.filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
.map(commandLogEntry->{
- commandLogEntry.setExecuteIn(ExecuteIn.FOREGROUND);
- var tryResultBookmark = replayContext.commandExecutorService().executeCommand(
- InteractionContextPolicy.SWITCH_USER_AND_TIME,
- commandLogEntry.getCommandDto());
+ final var commandDto = commandLogEntry.getCommandDto();
+ final var tryResultBookmark = replayContext.transactionService().callTransactional(Propagation.REQUIRES_NEW,
+ () -> {
+ final var bookmarkTry = replayContext.commandExecutorService().executeCommand(
+ InteractionContextPolicy.SWITCH_USER_AND_TIME,
+ commandDto);
+ return bookmarkTry.valueAsNullableElseFail();
+ });
// handle the replay outcome
tryResultBookmark.accept(
@@ -347,10 +335,17 @@ Try<ReplayableCommand> tryReplayOrRetry() {
invalidateCachedRecord();
return tryResultBookmark
- .mapSuccessAsNullable(__->this);
+ .mapSuccessAsNullable(__ -> this);
})
.orElseGet(()->Try.success(null));
}
+ String disableReplayOrRetry() {
+ return commandRecord()
+ .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
+ .orElse(false)
+ ? null
+ : "Cannot replay, if neither PENDING nor FAILED";
+ }
// -- HELPER
@@ -374,7 +369,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) {
@@ -383,5 +378,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 fb65cb4a511..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,17 +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.iactn.InteractionService;
-import org.apache.causeway.applib.services.repository.RepositoryService;
-import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
+import lombok.RequiredArgsConstructor;
-/**
- * Bundles dependencies for the replay logic.
- */
-public record ReplayContext(
- RepositoryService repositoryService,
- InteractionService interactionService,
- CommandLogEntryRepository commandLogEntryRepository,
- CommandExecutorService commandExecutorService) {
+import org.apache.causeway.applib.annotation.*;
+
+@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 1c94cd85468..8c280960613 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
@@ -359,9 +359,10 @@ void test_all_the_repository_methods() {
var username1 = commandTarget1User1.getUsername();
var from = commandTarget1User1.getStartedAt().toLocalDateTime().toLocalDate();
var to = from.plusDays(1);
+ final var timestamp = commandTarget1User1.getTimestamp();
// when
- List<? extends CommandLogEntry> notYetReplayed = commandLogEntryRepository.findReplayPendingOrFailed();
+ List<? extends CommandLogEntry> notYetReplayed = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(timestamp);
// then
Assertions.assertThat(notYetReplayed).isEmpty();
@@ -374,7 +375,7 @@ void test_all_the_repository_methods() {
commandTarget1User1.setReplayState(ReplayState.PENDING);
// when
- List<? extends CommandLogEntry> notYetReplayed2 = commandLogEntryRepository.findReplayPendingOrFailed();
+ List<? extends CommandLogEntry> notYetReplayed2 = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(timestamp);
// then
Assertions.assertThat(notYetReplayed2).hasSize(1);
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 0ac083a3198..65c156e70e0 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)