findAll() {
return _Casts.uncheckedCast(repositoryService().allInstances(commandLogEntryClass));
}
-
/**
* intended for testing purposes only
*/
@@ -491,5 +484,4 @@ public void removeAll() {
repositoryService().removeAll(commandLogEntryClass);
}
-
}
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 30a137e1aa3..93ed4f68660 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ReplayState.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ReplayState.java
@@ -18,20 +18,22 @@
*/
package org.apache.causeway.extensions.commandlog.applib.dom;
+import org.springframework.lang.Nullable;
+
/**
- * Curently unused.
- *
- *
- * This enum to support the (incubating) Command Replay extension.
- *
+ * Introduced in support of the Command Replay Feature.
*
* @since 2.x {@index}
*/
public enum ReplayState {
/**
- * As used on primary system.
+ * As used on primary system, indicating an initial state.
*/
UNDEFINED,
+ /**
+ * Marks a {@link CommandLogEntry} as exported, such that consecutive export actions will skip those.
+ */
+ EXPORTED,
/**
* For use on secondary system, indicates that the command has not yet been replayed.
*/
@@ -47,8 +49,37 @@ public enum ReplayState {
/**
* For use on secondary system, indicates that the command should not be replayed.
*/
- EXCLUDED,
- ;
+ EXCLUDED;
+
+ public boolean isExported() { return this == EXPORTED; }
+ public boolean isFailed() { return this == FAILED; }
+
+ public boolean isExportable() {
+ return this == ReplayState.UNDEFINED;
+ }
+
+ public boolean isPendingOrFailed() {
+ return this == ReplayState.PENDING
+ || this == ReplayState.FAILED;
+ }
+
+ // -- NULL SAFE
+
+ public static boolean isExportable(final @Nullable ReplayState replayState) {
+ return replayState == null || replayState.isExportable();
+ }
+
+ public static boolean isPendingOrFailed(final @Nullable ReplayState replayState) {
+ return replayState != null && replayState.isPendingOrFailed();
+ }
+
+ public static boolean isOkOrExcluded(ReplayState replayState) {
+ return replayState == ReplayState.OK || replayState == ReplayState.EXCLUDED;
+ }
+
+ public static boolean isExported(final ReplayState replayState) {
+ return replayState != null && replayState.isExported();
+ }
- public boolean isFailed() { return this == FAILED;}
}
+
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
new file mode 100644
index 00000000000..f0243676053
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java
@@ -0,0 +1,319 @@
+/*
+ * 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.Getter;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.chrono.ChronoZonedDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+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.Collection;
+import org.apache.causeway.applib.annotation.CollectionLayout;
+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;
+import org.apache.causeway.applib.value.Clob;
+import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
+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.CommandLogEntryRepository;
+import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState;
+
+import static org.apache.causeway.extensions.commandlog.applib.dom.replay.TimestampMarshallUtil.fromString;
+
+@DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
+@DomainObjectLayout(cssClassFa = "solid share-from-square")
+@Named(CommandExportManager.LOGICAL_TYPE_NAME)
+public final class CommandExportManager implements ViewModel {
+
+ public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager";
+
+ public static abstract class ActionDomainEvent
+ extends CausewayModuleExtCommandLogApplib.ActionDomainEvent { }
+
+ private ReplayContext replayContext;
+
+ @Inject
+ public CommandExportManager(
+ final String memento,
+ final ReplayContext 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";
+ }
+
+
+ @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 { }
+
+ @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 { }
+ @MemberSupport public CommandExportManager act() {
+ return new CommandExportManager(addSeconds(since, +3600), replayContext);
+ }
+ }
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = changeSince.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "since", sequence = "2",
+ named = "Change",
+ position = ActionLayout.Position.PANEL
+ )
+ public class changeSince {
+ public class DomainEvent extends ActionDomainEvent { }
+ @MemberSupport public CommandExportManager act(final java.sql.Timestamp since) {
+ return new CommandExportManager(since, replayContext);
+ }
+ @MemberSupport public java.sql.Timestamp defaultSince() {
+ return CommandExportManager.this.since;
+ }
+ }
+
+ private static Timestamp addSeconds(Timestamp since, int secondsToAdd) {
+ return Timestamp.from(since.toInstant().plusSeconds(secondsToAdd));
+ }
+
+
+ // -- NOT YET EXPORTED
+
+ @Collection
+ @CollectionLayout(
+ describedAs = "Commands that can be exported"
+ )
+ public List getNotYetExported() {
+ return commandLogEntryRepository().findForegroundSinceTimestampAndCanBeExported(since).stream()
+ .map(entry->new ReplayableCommand(
+ entry.getInteractionId(),
+ replayContext))
+ .collect(Collectors.toList());
+ }
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ choicesFrom = "notYetExported",
+ semantics = SemanticsOf.NON_IDEMPOTENT,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = exportSelected.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "notYetExported", sequence = "1.1",
+ cssClassFa = "solid share-from-square",
+ cssClass = "btn-primary",
+ describedAs = "Exports selected Commands as zipped DTOs for import later. "
+ + "Refresh the page to see changed states."
+ )
+ public class exportSelected {
+ public class DomainEvent extends ActionDomainEvent { }
+
+ @MemberSupport public Blob act(
+ final List 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()
+ .zip();
+
+ // do this last once we have successfully created the Clob
+ selectedCommandLogEntries.forEach(c->c.setReplayState(ReplayState.EXPORTED));
+
+ return blob;
+ }
+
+ @MemberSupport public String disableAct() {
+ return getNotYetExported().isEmpty() ? "No commands in collection" : null;
+ }
+
+ @MemberSupport public String defaultFilenamePrefix() {
+ return "commands";
+ }
+
+ @MemberSupport public boolean defaultFilenameTimestamp() {
+ return true;
+ }
+
+ @MemberSupport public String validateSelected(final List 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 choicesSelected() {
+ return getNotYetExported();
+ }
+ }
+
+
+ // -- EXPORTED
+
+ @Collection
+ @CollectionLayout(describedAs = "Commands that have been exported")
+ public List getExported() {
+ return commandLogEntryRepository().findForegroundSinceTimestampAndHasBeenExported(since).stream()
+ .map(entry->new ReplayableCommand(
+ entry.getInteractionId(),
+ replayContext))
+ .collect(Collectors.toList());
+ }
+
+
+ @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 { }
+
+ @MemberSupport
+ public CommandExportManager act(final List 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 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 choicesSelected() {
+ return getExported();
+ }
+ }
+
+
+ // -- VM STATE
+
+ @Override
+ public String viewModelMemento() {
+ 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
new file mode 100644
index 00000000000..ba0d2da0ef7
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 00000000000..b326e4c4be2
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
@@ -0,0 +1,400 @@
+/*
+ * 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 java.sql.Timestamp;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+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.Collection;
+import org.apache.causeway.applib.annotation.CollectionLayout;
+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 lombok.Getter;
+
+import static org.apache.causeway.extensions.commandlog.applib.dom.replay.TimestampMarshallUtil.fromString;
+
+@DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
+@DomainObjectLayout(cssClassFa = "solid circle-play")
+@Named(CommandReplayManager.LOGICAL_TYPE_NAME)
+public final class CommandReplayManager implements ViewModel {
+
+ public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayManager";
+
+ public static abstract class ActionDomainEvent
+ extends CausewayModuleExtCommandLogApplib.ActionDomainEvent { }
+
+ private ReplayContext replayContext;
+
+ @Inject
+ public CommandReplayManager(
+ final String memento,
+ final ReplayContext 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";
+ }
+
+
+ @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 { }
+
+ @MemberSupport public CommandReplayManager act() {
+ return new CommandReplayManager(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 { }
+ @MemberSupport public CommandReplayManager act() {
+ return new CommandReplayManager(addSeconds(since, +3600), replayContext);
+ }
+ }
+
+ @Action(
+ restrictTo = RestrictTo.PROTOTYPING,
+ semantics = SemanticsOf.SAFE,
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = changeSince.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED
+ )
+ @ActionLayout(
+ associateWith = "since", sequence = "2",
+ named = "Change",
+ position = ActionLayout.Position.PANEL
+ )
+ public class changeSince {
+ public class DomainEvent extends ActionDomainEvent { }
+ @MemberSupport public CommandReplayManager act(final java.sql.Timestamp since) {
+ return new CommandReplayManager(since, replayContext);
+ }
+ @MemberSupport public java.sql.Timestamp defaultSince() {
+ return CommandReplayManager.this.since;
+ }
+ }
+
+ private static Timestamp addSeconds(Timestamp since, 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.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 { }
+ public CommandReplayManager act(
+ @Parameter(fileAccept = ".zip")
+ final Blob zippedCommandsYaml) {
+
+ var yamlDs = zippedCommandsYaml.unZip(CommonMimeType.YAML).asDataSource();
+
+ final List 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)"
+ )
+ public List getPendingOrFailed() {
+ return commandLogEntryRepository().findForegroundSinceTimestampAndWithReplayPendingOrFailed(since).stream()
+ .map(entry->new ReplayableCommand(
+ entry.getInteractionId(),
+ replayContext))
+ .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 { }
+ @MemberSupport public CommandReplayManager act(final List 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 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 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 { }
+ @MemberSupport
+ public CommandReplayManager act(final List 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 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 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 { }
+ public CommandReplayManager act(final List 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 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 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)"
+ )
+ public List getSucceededOrExcluded() {
+ return commandLogEntryRepository().findSinceAndWithReplayOkOrExcluded(since).stream()
+ .map(entry->new ReplayableCommand(
+ entry.getInteractionId(),
+ replayContext))
+ .collect(Collectors.toList());
+ }
+
+
+ @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 class deleteSelectedSucceededOrExcluded {
+ public class DomainEvent extends ActionDomainEvent { }
+ public CommandReplayManager act(final List 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 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 choicesSelected() {
+ return getSucceededOrExcluded();
+ }
+ }
+
+
+
+ // -- VM STATE
+
+ @Override
+ public String viewModelMemento() {
+ 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/CommandReplayManager.layout.fallback.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.layout.fallback.xml
new file mode 100644
index 00000000000..c7aed354e9f
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.layout.fallback.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 00000000000..580e2f05ae6
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
@@ -0,0 +1,40 @@
+/*
+ * 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 org.apache.causeway.applib.services.clock.ClockService;
+import org.apache.causeway.applib.services.command.CommandExecutorService;
+import org.apache.causeway.applib.services.iactnlayer.InteractionService;
+import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
+
+import lombok.Value;
+import lombok.experimental.Accessors;
+
+/**
+ * Bundles dependencies for the replay logic.
+ */
+@Value @Accessors(fluent = true)
+public final class ReplayContext {
+ RepositoryService repositoryService;
+ InteractionService interactionService;
+ CommandLogEntryRepository commandLogEntryRepository;
+ CommandExecutorService commandExecutorService;
+ ClockService clockService;
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-excluded.png b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-excluded.png
new file mode 100644
index 00000000000..9a1d6cd8e30
Binary files /dev/null and b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-excluded.png differ
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-exported.png b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-exported.png
new file mode 100644
index 00000000000..60452341123
Binary files /dev/null and b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-exported.png differ
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-failed.png b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-failed.png
new file mode 100644
index 00000000000..24a95966c1b
Binary files /dev/null and b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-failed.png differ
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-ok.png b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-ok.png
new file mode 100644
index 00000000000..1578312869f
Binary files /dev/null and b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-ok.png differ
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-pending.png b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-pending.png
new file mode 100644
index 00000000000..e83e889a682
Binary files /dev/null and b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand-pending.png differ
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
new file mode 100644
index 00000000000..e2ea51718b0
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
@@ -0,0 +1,401 @@
+/*
+ * 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 java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.chrono.ChronoZonedDateTime;
+import java.util.Comparator;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+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.Publishing;
+import org.apache.causeway.applib.annotation.RestrictTo;
+import org.apache.causeway.applib.annotation.SemanticsOf;
+import org.apache.causeway.applib.annotation.Where;
+import org.apache.causeway.applib.jaxb.JavaTimeXMLGregorianCalendarMarshalling;
+import org.apache.causeway.applib.services.command.CommandExecutorService.InteractionContextPolicy;
+import org.apache.causeway.commons.functional.Try;
+import org.apache.causeway.commons.internal.base._Refs.ObjectReference;
+import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.io.JsonUtils;
+import org.apache.causeway.commons.io.TextUtils;
+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.ReplayState;
+import org.apache.causeway.schema.cmd.v2.CommandDto;
+import org.apache.causeway.schema.cmd.v2.MemberDto;
+import org.apache.causeway.schema.common.v2.OidDto;
+import org.apache.causeway.valuetypes.asciidoc.applib.value.AsciiDoc;
+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.ANNOTATION_REQUIRED)
+@DomainObjectLayout//(cssClassFa = "terminal")
+@Named(ReplayableCommand.LOGICAL_TYPE_NAME)
+@AllArgsConstructor
+public final class ReplayableCommand implements ViewModel, Comparable {
+
+ public static abstract class ActionDomainEvent
+ extends CausewayModuleExtCommandLogApplib.ActionDomainEvent { }
+
+ private final UUID interactionId;
+ @Programmatic
+ public UUID interactionId() { return interactionId; }
+
+ private final ReplayContext replayContext;
+ @Programmatic
+ public ReplayContext replayContext() { return replayContext; }
+
+
+ private final ObjectReference recordRef;
+ @Programmatic
+ public ObjectReference recordRef() { return recordRef; }
+
+ public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".ReplayableCommand";
+
+ // decoupled from the underlying entity
+ @Value @Accessors(fluent = true)
+ final static class CommandRecord {
+
+ final CommandDto commandDto;
+ final ReplayState replayState;
+
+ boolean canReplayOrRetryOrMarkForExclusion() {
+ return replayState.isPendingOrFailed();
+ }
+ public String faQuickIcon() {
+ switch(replayState) {
+ case UNDEFINED: return "solid terminal .col-indigo";
+ case EXPORTED: return "solid terminal .col-indigo, solid circle-arrow-right .ov-size-80 .ov-right-45 .ov-bottom-45 .col-dodgerblue";
+ case PENDING: return "solid terminal .col-indigo, solid circle-pause .ov-size-80 .ov-right-45 .ov-bottom-45 .col-gold";
+ case OK: return "solid terminal .col-indigo, solid circle-check .ov-size-80 .ov-right-45 .ov-bottom-45 .col-green";
+ case FAILED: return "solid terminal .col-indigo, solid circle-exclamation .ov-size-80 .ov-right-45 .ov-bottom-45 .col-red";
+ case EXCLUDED: return "solid terminal .col-indigo, solid circle-xmark .ov-size-80 .ov-right-45 .ov-bottom-45 .col-grey";
+ };
+ return null;
+ }
+ //v2 backport, using png screenshots from v4
+ public String iconSuffix() {
+ switch(replayState) {
+ case UNDEFINED: return "";
+ case EXPORTED: return "exported";
+ case PENDING: return "pending";
+ case OK: return "ok";
+ case FAILED: return "failed";
+ case EXCLUDED: return "excluded";
+ };
+ return null;
+ }
+ }
+
+ @Inject
+ public ReplayableCommand(
+ final String memento,
+ final ReplayContext replayContext) {
+ this(UUID.fromString(memento), replayContext);
+ }
+
+ ReplayableCommand(
+ final UUID interactionId,
+ final ReplayContext replayContext) {
+ this(interactionId, replayContext, new ObjectReference<>(null));
+ }
+
+ @ObjectSupport public String title() {
+ final var timestamp = getTimestampIfAny().map(ChronoZonedDateTime::toInstant).map(Instant::toString).map(x -> " @ " + x).orElse("");
+ return getTargetType() + ":" + getTargetId() + " #" + getMember() + timestamp;
+ }
+
+//requires v4
+// @ObjectSupport public ObjectSupport.IconResource icon(final ObjectSupport.IconSize iconSize) {
+// return commandRecord()
+// .map(CommandRecord::faQuickIcon)
+// .map(FontAwesomeLayers::fromQuickNotation)
+// .map(ObjectSupport.FontAwesomeIconResource::new)
+// .orElse(null);
+// }
+ @ObjectSupport public String iconName() {
+ return commandRecord()
+ .map(CommandRecord::iconSuffix)
+ .orElse(null);
+ }
+
+ @Property
+ @PropertyLayout(
+ sequence = "1.1",
+ fieldSetId = "details",
+ describedAs = "UUID of the original (replayable) Command")
+ public UUID getInteractionId() {
+ return interactionId;
+ }
+
+ @Property
+ @PropertyLayout(
+ sequence = "1.2",
+ fieldSetId = "details",
+ describedAs = "Timestamp of the original (replayable) Command")
+ public ZonedDateTime getTimestamp() {
+ return getTimestampIfAny()
+ .orElse(null);
+ }
+
+ @Programmatic
+ public Optional 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 (replayable) Command")
+ public String getTargetType() {
+ return commandRecord()
+ .map(CommandRecord::commandDto)
+ .map(commandDto->commandDto.getTargets().getOid().get(0))
+ .map(OidDto::getType)
+ .orElse(null);
+ }
+
+ @Property
+ @PropertyLayout(
+ sequence = "2.2",
+ fieldSetId = "details",
+ describedAs = "Target ID of the original (replayable) Command")
+ public String getTargetId() {
+ return commandRecord()
+ .map(CommandRecord::commandDto)
+ .map(commandDto->commandDto.getTargets().getOid().get(0))
+ .map(OidDto::getId)
+ .map(id->_Strings.ellipsifyAtEnd(id, 10, "..."))
+ .orElse(null);
+ }
+
+ @Property
+ @PropertyLayout(
+ sequence = "3.1",
+ fieldSetId = "details",
+ describedAs = "Replayable Action or Property, that was executed as captured by the original Command")
+ public String getMember() {
+ return commandRecord()
+ .map(CommandRecord::commandDto)
+ .map(CommandDto::getMember)
+ .map(MemberDto::getLogicalMemberIdentifier)
+ .map(TextUtils::cutter)
+ .map(cutter->cutter.keepAfter("#").getValue())
+ .orElse(null);
+ }
+
+ @Property
+ @PropertyLayout(
+ sequence = "4",
+ fieldSetId = "details",
+ 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.")
+ public ReplayState getReplayState() {
+ return commandRecord()
+ .map(CommandRecord::replayState)
+ .orElse(null);
+ }
+
+ @Property
+ @PropertyLayout(
+ sequence = "9",
+ fieldSetId = "dto",
+ hidden = Where.ALL_TABLES,
+ labelPosition = LabelPosition.NONE,
+ describedAs = "DTO of the original (replayable) Command")
+ public AsciiDoc getDto() {
+ return commandRecord()
+ .map(CommandRecord::commandDto)
+ .map(commandDto->YamlUtils.toStringUtf8(commandDto,
+ JsonUtils::onlyIncludeNonNull))
+ .map(yaml->new AsciiDocBuilder()
+ .append(doc->AsciiDocFactory.sourceBlock(doc, "yaml", yaml))
+ .buildAsValue())
+ .orElseGet(()->new AsciiDoc("empty"));
+ }
+
+ // -- ACTIONS
+
+
+ ReplayableCommand makeExportable() {
+ if(disableMakeExportable()!=null) {
+ return this; // safeguard when called programmatically
+ }
+ commandLogEntry()
+ .filter(commandLogEntry->ReplayState.isExported(commandLogEntry.getReplayState()))
+ .ifPresent(commandLogEntry->{
+ commandLogEntry.setReplayState(ReplayState.UNDEFINED);
+ invalidateCachedRecord();
+ });
+ return this;
+ }
+ String disableMakeExportable() {
+ return commandRecord()
+ .map(rec->ReplayState.isExported(rec.replayState()))
+ .orElse(false)
+ ? null
+ : "Cannot make exportable, if not EXPORTED";
+ }
+
+
+ ReplayableCommand excludeFromReplay() {
+ if(disableExcludeFromReplay()!=null) {
+ return ReplayableCommand.this; // safeguard when called programmatically
+ }
+ commandLogEntry()
+ .filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
+ .ifPresent(commandLogEntry->{
+ commandLogEntry.setReplayState(ReplayState.EXCLUDED);
+ invalidateCachedRecord();
+ });
+ return ReplayableCommand.this;
+ }
+ String disableExcludeFromReplay() {
+ return commandRecord()
+ .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
+ .orElse(false)
+ ? null
+ : "Cannot mark for exclusion, if neither PENDING nor FAILED";
+ }
+
+
+ @Programmatic
+ void deleteObj() {
+ commandLogEntry()
+ .ifPresent(commandLogEntry->{
+ replayContext.repositoryService().remove(commandLogEntry);
+ invalidateCachedRecord();
+ });
+ }
+
+
+ // -- EXECUTION ORDER GOVERNED BY TIMESTAMP
+
+ private static final Comparator TIMESTAMP_COMPARATOR =
+ Comparator.nullsLast(Comparator.comparing(ReplayableCommand::getTimestamp));
+
+ @Override
+ public int compareTo(final ReplayableCommand other) {
+ return TIMESTAMP_COMPARATOR.compare(this, other);
+ }
+
+ // -- VM STATE
+
+ @Override
+ public String viewModelMemento() {
+ return interactionId.toString();
+ }
+
+ // -- UTIL
+
+ Try tryReplayOrRetry() {
+ if(disableReplayOrRetry()!=null) {
+ return Try.success(null); // guard against disallowed invocation
+ }
+ return commandLogEntry()
+ .filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
+ .map(commandLogEntry->{
+ var tryResultBookmark = replayContext.commandExecutorService().executeCommand(
+ InteractionContextPolicy.SWITCH_USER_AND_TIME,
+ commandLogEntry.getCommandDto());
+
+ // handle the replay outcome
+ tryResultBookmark.accept(
+ ex->onReplayError(commandLogEntry.getInteractionId(), ex),
+ bookmarkOpt->commandLogEntry.setReplayState(ReplayState.OK));
+
+ invalidateCachedRecord();
+
+ return tryResultBookmark
+ .mapSuccessAsNullable(__->this);
+ })
+ .orElseGet(()->Try.success(null));
+ }
+ String disableReplayOrRetry() {
+ return commandRecord()
+ .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
+ .orElse(false)
+ ? null
+ : "Cannot replay, if neither PENDING nor FAILED";
+ }
+
+ // -- HELPER
+
+ private void invalidateCachedRecord() {
+ recordRef.update(__->null); // invalidate cache
+ }
+
+ private Optional commandRecord() {
+ return Optional.ofNullable(recordRef.computeIfAbsent(()->
+ commandLogEntry()
+ .filter(commandLogEntry->commandLogEntry.getCommandDto()!=null)
+ .filter(commandLogEntry->commandLogEntry.getReplayState()!=null)
+ .map(commandLogEntry->new CommandRecord(
+ commandLogEntry.getCommandDto(),
+ commandLogEntry.getReplayState()))
+ .orElse(null)));
+ }
+
+ Optional commandLogEntry() {
+ return replayContext.commandLogEntryRepository().findByInteractionId(interactionId());
+ }
+
+ private static boolean canReplayOrRetryOrMarkForExclusion(final CommandLogEntry commandLogEntry) {
+ return ReplayState.isPendingOrFailed(commandLogEntry.getReplayState());
+ }
+
+ private void onReplayError(final UUID interactionId, final Throwable ex) {
+ Executors.newSingleThreadExecutor()
+ .submit(()->replayContext.interactionService().runAnonymous(()->
+ 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
new file mode 100644
index 00000000000..331644bba1a
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.png b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.png
new file mode 100644
index 00000000000..ac153a4102e
Binary files /dev/null and b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.png differ
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_delete.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_delete.java
new file mode 100644
index 00000000000..6289ab9647e
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_delete.java
@@ -0,0 +1,49 @@
+/*
+ * 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_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 {
+ }
+
+ 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 {
+ }
+
+ 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 {
+ }
+
+ 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 {
+ }
+
+ 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 {
+ }
+
+ 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 0b69e6cc622..c33dbb87666 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
@@ -18,6 +18,7 @@
*/
package org.apache.causeway.extensions.commandlog.applib.integtest;
+import java.sql.Timestamp;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@@ -276,6 +277,9 @@ void roundtrip_CLE_bookmarks() {
void test_all_the_repository_methods() {
// given
+ final var baseline = clockService.getClock().nowAsJavaSqlTimestamp();
+
+
sudoService.run(InteractionContext.switchUser(UserMemento.builder().name("user-1").build()), () -> {
wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act();
});
@@ -367,9 +371,10 @@ void test_all_the_repository_methods() {
val username1 = commandTarget1User1.getUsername();
val from = commandTarget1User1.getStartedAt().toLocalDateTime().toLocalDate();
val to = from.plusDays(1);
+ final var timestamp = commandTarget1User1.getTimestamp();
// when
- List extends CommandLogEntry> notYetReplayed = commandLogEntryRepository.findNotYetReplayed();
+ List extends CommandLogEntry> notYetReplayed = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(timestamp);
// then
Assertions.assertThat(notYetReplayed).isEmpty();
@@ -382,7 +387,7 @@ void test_all_the_repository_methods() {
commandTarget1User1.setReplayState(ReplayState.PENDING);
// when
- List extends CommandLogEntry> notYetReplayed2 = commandLogEntryRepository.findNotYetReplayed();
+ List extends CommandLogEntry> notYetReplayed2 = commandLogEntryRepository.findForegroundSinceTimestampAndWithReplayPendingOrFailed(timestamp);
// then
Assertions.assertThat(notYetReplayed2).hasSize(1);
diff --git a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java
index 04a0b51f2e9..49513f3859d 100644
--- a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java
+++ b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java
@@ -206,12 +206,21 @@
+ " RANGE 0,2"), // this should be RANGE 0,1 but results in DataNucleus submitting "FETCH NEXT ROW ONLY"
// which SQL Server doesn't understand. However, as workaround, SQL Server *does* understand FETCH NEXT 2 ROWS ONLY
@Query(
- name = Nq.FIND_BY_REPLAY_STATE,
+ name = Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATE,
value = "SELECT "
+ " FROM " + CommandLogEntry.FQCN + " "
- + " WHERE replayState == :replayState "
- + " ORDER BY timestamp ASC "
- + " RANGE 0,10"), // same as batch size
+ + " WHERE executeIn == 'FOREGROUND' "
+ + " && timestamp >= :from "
+ + " && replayState == :replayState "
+ + " ORDER BY timestamp ASC"),
+ @Query(
+ name = Nq.FIND_FOREGROUND_BY_TIMESTAMP_AFTER_AND_REPLAY_STATES,
+ value = "SELECT "
+ + " FROM " + CommandLogEntry.FQCN + " "
+ + " WHERE executeIn == 'FOREGROUND' "
+ + " && timestamp >= :from "
+ + " && (replayState == :replayState1 || replayState == :replayState2) "
+ + " ORDER BY timestamp ASC"),
})
@Named(CommandLogEntry.LOGICAL_TYPE_NAME)
@DomainObject(
@@ -237,7 +246,7 @@ public CommandLogEntry(
final CommandDto commandDto,
final org.apache.causeway.extensions.commandlog.applib.dom.ReplayState replayState,
final int targetIndex) {
- super(commandDto, replayState, targetIndex);
+ init(commandDto, replayState, targetIndex);
}
@PrimaryKey
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 de36b0f1ca6..8cc67652432 100644
--- a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
+++ b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
@@ -198,7 +198,8 @@
name = Nq.FIND_MOST_RECENT_REPLAYED,
query = "SELECT cl "
+ " FROM CommandLogEntry cl "
- + " WHERE (cl.replayState = org.apache.causeway.extensions.commandlog.applib.dom.ReplayState.OK OR cl.replayState = org.apache.causeway.extensions.commandlog.applib.dom.ReplayState.FAILED) "
+ + " WHERE (cl.replayState = org.apache.causeway.extensions.commandlog.applib.dom.ReplayState.OK"
+ + " OR cl.replayState = org.apache.causeway.extensions.commandlog.applib.dom.ReplayState.FAILED) "
+ " ORDER BY cl.timestamp DESC"), // programmatic LIMIT 1
@NamedQuery(
name = Nq.FIND_MOST_RECENT_COMPLETED,
@@ -208,11 +209,21 @@
+ " 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 = :replayState "
- + " ORDER BY cl.timestamp ASC"), // programmatic LIMIT 10
+ + " FROM CommandLogEntry cl "
+ + " 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)
@DomainObject(
@@ -235,14 +246,12 @@ public CommandLogEntry(
final CommandDto commandDto,
final org.apache.causeway.extensions.commandlog.applib.dom.ReplayState replayState,
final int targetIndex) {
- super(commandDto, replayState, targetIndex);
+ init(commandDto, replayState, targetIndex);
}
-
@EmbeddedId
private CommandLogEntryPK pk;
-
@Transient
@InteractionId
@Override
@@ -255,46 +264,39 @@ public void setInteractionId(final UUID interactionId) {
this.pk = new CommandLogEntryPK(interactionId);
}
-
@Column(nullable = Username.NULLABLE, length = Username.MAX_LENGTH)
@Username
@Getter @Setter
private String username;
-
@Column(nullable = Timestamp.NULLABLE)
@Timestamp
@Getter @Setter
private java.sql.Timestamp timestamp;
-
@Convert(converter = CausewayBookmarkConverter.class)
@Column(nullable = Target.NULLABLE, length = Target.MAX_LENGTH)
@Target
@Getter @Setter
private Bookmark target;
-
@Column(nullable = ExecuteIn.NULLABLE, length = ExecuteIn.MAX_LENGTH)
@Enumerated(EnumType.STRING)
@ExecuteIn
@Getter @Setter
private org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn executeIn;
-
@Convert(converter = JavaUtilUuidConverter.class)
@Domain.Exclude
@Column(nullable = Parent.NULLABLE, length = InteractionId.MAX_LENGTH)
@Getter @Setter
private UUID parentInteractionId;
-
@Column(nullable = LogicalMemberIdentifier.NULLABLE, length = LogicalMemberIdentifier.MAX_LENGTH)
@LogicalMemberIdentifier
@Getter @Setter
private String logicalMemberIdentifier;
-
@Convert(converter = CausewayCommandDtoConverter.class)
@Lob @Basic(fetch = FetchType.LAZY)
@Column(nullable = CommandDtoAnnot.NULLABLE, columnDefinition = "CLOB")
@@ -302,40 +304,34 @@ public void setInteractionId(final UUID interactionId) {
@Getter @Setter
private CommandDto commandDto;
-
@Column(nullable = StartedAt.NULLABLE)
@StartedAt
@Getter @Setter
private java.sql.Timestamp startedAt;
-
@Column(nullable = CompletedAt.NULLABLE)
@CompletedAt
@Getter @Setter
private java.sql.Timestamp completedAt;
-
@Convert(converter = CausewayBookmarkConverter.class)
@Column(nullable = Result.NULLABLE, length = Result.MAX_LENGTH)
@Result
@Getter @Setter
private Bookmark result;
-
@Lob @Basic(fetch = FetchType.LAZY)
@Column(nullable = Exception.NULLABLE, columnDefinition = "CLOB")
@Exception
@Getter @Setter
private String exception;
-
@Column(nullable = ReplayState.NULLABLE, length = ReplayState.MAX_LENGTH)
@Enumerated(EnumType.STRING)
@ReplayState
@Getter @Setter
private org.apache.causeway.extensions.commandlog.applib.dom.ReplayState replayState;
-
@Column(nullable = ReplayStateFailureReason.NULLABLE, length = ReplayStateFailureReason.MAX_LENGTH)
@ReplayStateFailureReason
@Getter @Setter
diff --git a/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/jobcallables/ReplicateAndRunCommands.java b/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/jobcallables/ReplicateAndRunCommands.java
index a01bf730d18..63df7dfcba1 100644
--- a/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/jobcallables/ReplicateAndRunCommands.java
+++ b/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/jobcallables/ReplicateAndRunCommands.java
@@ -85,7 +85,7 @@ private void doCall() throws StatusException {
// is there a pending command already?
// (we fetch several at a time, so we may not have processed them all yet)
- commandsToReplay = commandLogEntryRepository.findNotYetReplayed();
+ commandsToReplay = commandLogEntryRepository.findReplayPendingOrFailed();
if(commandsToReplay.isEmpty()) {
diff --git a/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/mixins/CommandLogEntry_replayQueue.java b/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/mixins/CommandLogEntry_replayQueue.java
index a9078f9f7cb..683b4edb441 100644
--- a/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/mixins/CommandLogEntry_replayQueue.java
+++ b/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/mixins/CommandLogEntry_replayQueue.java
@@ -45,7 +45,7 @@ public static class CollectionDomainEvent
final CommandLogEntry commandLogEntry;
public List extends CommandLogEntry> coll() {
- return commandLogEntryRepository.findNotYetReplayed();
+ return commandLogEntryRepository.findReplayPendingOrFailed();
}
public boolean hideColl() {
return !secondaryConfig.isConfigured();