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/09 09:55:14 UTC

(causeway) 01/01: CAUSEWAY-3989: Command Replay Manager (stub)

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch 3989-cmd-io
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit f84a0f689dc7dc152d4638b4e8658deff270c4a7
Author: andi-huber <ah...@apache.org>
AuthorDate: Thu Apr 9 11:55:03 2026 +0200

    CAUSEWAY-3989: Command Replay Manager (stub)
---
 .../applib/CausewayModuleExtCommandLogApplib.java  |  4 ++
 .../commandlog/applib/app/CommandLogMenu.java      | 18 +++++
 .../commandlog/applib/dom/CommandLogEntry.java     | 27 +++++---
 .../applib/dom/CommandLogEntryRepository.java      | 45 ++++++------
 .../dom/CommandLogEntryRepositoryAbstract.java     | 28 ++++----
 .../applib/dom/replay/CommandReplayManager.java    | 80 ++++++++++++++++++++++
 6 files changed, 160 insertions(+), 42 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 64303bf8c12..e7e71289f54 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
@@ -31,6 +31,7 @@
 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.CommandReplayManager;
 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;
@@ -42,6 +43,9 @@
         // @DomainService's
         CommandLogMenu.class,
 
+        // viewmodels
+        CommandReplayManager.class,
+
         // mixins
         HasInteractionId_commandLogEntry.class,
         HasUsername_recentCommandsByUser.class,
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 8ec1c82c712..b80f5c3a918 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
@@ -40,6 +40,7 @@
 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.replay.CommandReplayManager;
 
 import lombok.RequiredArgsConstructor;
 
@@ -141,6 +142,23 @@ public class DomainEvent extends ActionDomainEvent<findAll> { }
         }
     }
 
+    @Action(
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = replayManager.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED,
+            restrictTo = RestrictTo.PROTOTYPING,
+            semantics = SemanticsOf.SAFE
+    )
+    @ActionLayout(cssClassFa = "solid circle-play", sequence="50")
+    public class replayManager {
+        public class DomainEvent extends ActionDomainEvent<replayManager> { }
+
+        @MemberSupport public CommandReplayManager act() {
+            return new CommandReplayManager(null, commandLogEntryRepository);
+        }
+    }
+
+
     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 160af93de38..3911666f4ea 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
@@ -26,6 +26,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.UUID;
 import java.util.function.Consumer;
 
@@ -191,8 +192,11 @@ default void init(
         // the hierarchy of commands calling other commands is only available on the primary system.
         setParentInteractionId(null);
 
-        setStartedAt(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(commandDto.getTimings().getStartedAt()));
-        setCompletedAt(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(commandDto.getTimings().getCompletedAt()));
+        Optional.ofNullable(commandDto.getTimings())
+            .ifPresent(timings->{
+                setStartedAt(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(timings.getStartedAt()));
+                setCompletedAt(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(timings.getCompletedAt()));
+            });
 
         copyOver(commandDto, UserDataKeys.RESULT, value -> this.setResult(Bookmark.parse(value).orElse(null)));
         copyOver(commandDto, UserDataKeys.EXCEPTION, this::setException);
@@ -203,14 +207,17 @@ default void init(
     static void copyOver(
             final CommandDto commandDto,
             final String key, final Consumer<String> consume) {
-        commandDto.getUserData().getEntry()
-                .stream()
-                .filter(x -> Objects.equals(x.getKey(), key))
-                .map(MapDto.Entry::getValue)
-                .filter(Objects::nonNull)
-                .filter(x -> x.length() > 0)
-                .findFirst()
-                .ifPresent(consume);
+        Optional.ofNullable(commandDto.getUserData())
+            .ifPresent(userdata->{
+                userdata.getEntry()
+                    .stream()
+                    .filter(x -> Objects.equals(x.getKey(), key))
+                    .map(MapDto.Entry::getValue)
+                    .filter(Objects::nonNull)
+                    .filter(x -> x.length() > 0)
+                    .findFirst()
+                    .ifPresent(consume);
+            });
     }
 
     static final DateTimeFormatter DATETIME_FORMATTER =
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 97146dc08df..fef06fb7d07 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
@@ -51,36 +51,41 @@ public NotFoundException(final UUID interactionId) {
     }
 
     CommandLogEntry createEntryAndPersist(
-            final Command command, final UUID parentInteractionIdIfAny, final ExecuteIn executeIn);
+            Command command, UUID parentInteractionIdIfAny, final ExecuteIn executeIn);
 
-    Optional<CommandLogEntry> findByInteractionId(final UUID interactionId);
+    CommandLogEntry createAsPending(CommandDto commandToReplay, int targetIndex);
 
-    List<CommandLogEntry> findByParent(final CommandLogEntry parent);
+    Optional<CommandLogEntry> findByInteractionId(UUID interactionId);
 
-    List<CommandLogEntry> findByParentInteractionId(final UUID parentInteractionId);
+    List<CommandLogEntry> findByParent(CommandLogEntry parent);
+
+    List<CommandLogEntry> findByParentInteractionId(UUID parentInteractionId);
 
     List<CommandLogEntry> findByFromAndTo(
-            final @Nullable LocalDate from,
-            final @Nullable LocalDate to);
+            @Nullable LocalDate from,
+            @Nullable LocalDate to);
 
+    /**
+     * Still running, not completed yet.
+     */
     List<CommandLogEntry> findCurrent();
 
     List<CommandLogEntry> findCompleted();
 
     List<CommandLogEntry> findByTargetAndFromAndTo(
-            final Bookmark target,
-            final @Nullable LocalDate from,
-            final @Nullable LocalDate to);
+            Bookmark target,
+            @Nullable LocalDate from,
+            @Nullable LocalDate to);
 
     List<CommandLogEntry> findMostRecent();
 
-    List<CommandLogEntry> findMostRecent(final int limit);
+    List<CommandLogEntry> findMostRecent(int limit);
 
-    List<CommandLogEntry> findRecentByUsername(final String username);
+    List<CommandLogEntry> findRecentByUsername(String username);
 
-    List<CommandLogEntry> findRecentByTarget(final Bookmark target);
+    List<CommandLogEntry> findRecentByTarget(Bookmark target);
 
-    List<CommandLogEntry> findRecentByTargetOrResult(final Bookmark targetOrResult);
+    List<CommandLogEntry> findRecentByTargetOrResult(Bookmark targetOrResult);
 
     /**
      * Intended to support the replay of commands on a secondary instance of
@@ -112,7 +117,7 @@ List<CommandLogEntry> findByTargetAndFromAndTo(
      * @param batchSize - to restrict the number returned (so that replay
      *                   commands can be batched).
      */
-    List<CommandLogEntry> findSince(final UUID interactionId, final Integer batchSize);
+    List<CommandLogEntry> findSince(UUID interactionId, Integer batchSize);
 
     /**
      * Returns any persisted commands that have not yet started.
@@ -126,7 +131,7 @@ List<CommandLogEntry> findByTargetAndFromAndTo(
      */
     List<CommandLogEntry> findBackgroundAndNotYetStarted();
 
-    List<CommandLogEntry> findRecentBackgroundByTarget(final Bookmark target);
+    List<CommandLogEntry> findRecentBackgroundByTarget(Bookmark target);
 
     /**
      * The most recent replayed command previously replicated from primary to
@@ -154,19 +159,19 @@ List<CommandLogEntry> findByTargetAndFromAndTo(
 
     List<CommandLogEntry> findNotYetReplayed();
 
-    CommandLogEntry saveForReplay(final CommandDto dto);
+    CommandLogEntry saveForReplay(CommandDto dto);
 
-    List<CommandLogEntry> saveForReplay(final CommandsDto commandsDto);
+    List<CommandLogEntry> saveForReplay(CommandsDto commandsDto);
 
-    void persist(final CommandLogEntry commandLogEntry);
+    void persist(CommandLogEntry commandLogEntry);
 
     void truncateLog();
 
     // --
 
     List<CommandLogEntry> findCommandsOnPrimaryElseFail(
-            final @Nullable UUID interactionId,
-            final @Nullable Integer batchSize) throws NotFoundException;
+            @Nullable UUID interactionId,
+            @Nullable Integer batchSize) throws NotFoundException;
 
     /**
      * intended for testing purposes only
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 804fbc92179..67371bd75a2 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
@@ -80,6 +80,16 @@ public C createEntryAndPersist(
         return c;
     }
 
+    @Override
+    public C createAsPending(final CommandDto commandToReplay, final int targetIndex) {
+        C c = factoryService.detachedEntity(commandLogEntryClass);
+        c.init(commandToReplay, ReplayState.PENDING, targetIndex);
+        c.setParentInteractionId(null); // n/a for replay
+        c.setExecuteIn(null); // to be specified later depending on user action
+        persist(c);
+        return c;
+    }
+
     @Override
     public Optional<CommandLogEntry> findByInteractionId(final UUID interactionId) {
         return _Casts.uncheckedCast(
@@ -257,13 +267,11 @@ public List<CommandLogEntry> findRecentByTargetOrResult(final Bookmark targetOrR
      */
     @Override
     public List<CommandLogEntry> findSince(final UUID interactionId, final Integer batchSize) {
-        if(interactionId == null) {
+        if(interactionId == null)
             return findFirst();
-        }
         final C from = findByInteractionIdElseNull(interactionId);
-        if(from == null) {
+        if(from == null)
             return Collections.emptyList();
-        }
         return findSince(from.getTimestamp(), batchSize);
     }
 
@@ -355,11 +363,10 @@ public C saveForReplay(final CommandDto dto) {
 
         if(dto.getMember().getInteractionType() == InteractionType.ACTION_INVOCATION) {
             final MapDto userData = dto.getUserData();
-            if (userData == null ) {
+            if (userData == null )
                 throw new IllegalStateException(String.format(
                         "Can only persist action DTOs with additional userData; got: \n%s",
                         CommandDtoUtils.dtoMapper().toString(dto)));
-            }
         }
 
         final C commandJdo = factoryService.detachedEntity(commandLogEntryClass);
@@ -408,9 +415,8 @@ public List<CommandLogEntry> findCommandsOnPrimaryElseFail(
             final @Nullable Integer batchSize) throws NotFoundException {
 
         final List<CommandLogEntry> commands = findSince(interactionId, batchSize);
-        if(commands == null) {
+        if(commands == null)
             throw new NotFoundException(interactionId);
-        }
         return commands;
     }
 
@@ -460,9 +466,8 @@ private static Timestamp toTimestampStartOfDayWithOffset(
      */
     @Override
     public List<CommandLogEntry> findAll() {
-        if (causewaySystemEnvironment.deploymentType().isProduction()) {
+        if (causewaySystemEnvironment.deploymentType().isProduction())
             throw new IllegalStateException("Cannot call 'findAll' in production systems");
-        }
         return _Casts.uncheckedCast(repositoryService().allInstances(commandLogEntryClass));
     }
 
@@ -471,9 +476,8 @@ public List<CommandLogEntry> findAll() {
      */
     @Override
     public void removeAll() {
-        if (causewaySystemEnvironment.deploymentType().isProduction()) {
+        if (causewaySystemEnvironment.deploymentType().isProduction())
             throw new IllegalStateException("Cannot call 'removeAll' in production systems");
-        }
         repositoryService().removeAll(commandLogEntryClass);
     }
 
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..7d13391054d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
@@ -0,0 +1,80 @@
+/*
+ *  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.util.List;
+
+import jakarta.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.DomainObjectLayout;
+import org.apache.causeway.applib.annotation.ObjectSupport;
+import org.apache.causeway.applib.annotation.Parameter;
+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.commons.io.JsonUtils;
+import org.apache.causeway.commons.io.YamlUtils;
+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;
+
+@DomainObjectLayout(cssClassFa = "solid circle-play")
+@Named(CommandReplayManager.LOGICAL_TYPE_NAME)
+public class CommandReplayManager implements ViewModel {
+
+    public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayManager";
+
+    private final CommandLogEntryRepository commandLogEntryRepository;
+
+    public CommandReplayManager(final String memento, final CommandLogEntryRepository commandLogEntryRepository) {
+        this.commandLogEntryRepository = commandLogEntryRepository;
+    }
+
+    @ObjectSupport public String title() {
+        return "Command Replay Manager";
+    }
+
+    @Action
+    @ActionLayout(describedAs = "Imports commands from a zipped yaml, then persists them with replayState=PENDING.")
+    public String importCommands(
+            @Parameter(fileAccept = ".zip")
+            final Blob zippedCommandsYaml) {
+
+        var yamlDs = zippedCommandsYaml.unZip(CommonMimeType.YAML).asDataSource();
+
+        final List<CommandDto> commandDtos = CommandDtoUtils.fromYaml(yamlDs);
+        commandDtos.forEach(commandDto->commandLogEntryRepository.createAsPending(commandDto, 0));
+
+        var yaml = YamlUtils.toStringUtf8(commandDtos,
+                JsonUtils::onlyIncludeNonNull);
+
+        return yaml;
+    }
+
+
+    @Override
+    public String viewModelMemento() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+}