You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/06/07 14:49:23 UTC

(causeway) 32/50: CAUSEWAY-4014: adds replayable command open target action

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

danhaywood pushed a commit to branch CAUSEWAY-4014
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 99cc4410bcb5dcc0564511f8fee83f50e8bd3bdf
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Jun 7 12:13:30 2026 +0100

    CAUSEWAY-4014: adds replayable command open target action
---
 .../applib/CausewayModuleExtCommandLogApplib.java  |  2 +
 .../dom/replay/ReplayableCommand_openTarget.java   | 70 ++++++++++++++++++++++
 .../dom/replay/ReplayableCommandMappingTest.java   | 38 ++++++++++++
 .../.openspec.yaml                                 |  0
 .../design.md                                      |  0
 .../proposal.md                                    |  0
 .../specs/replayable-command-remappings/spec.md    |  0
 .../tasks.md                                       | 14 +++++
 .../tasks.md                                       | 14 -----
 .../specs/replayable-command-remappings/spec.md    | 16 ++++-
 10 files changed, 138 insertions(+), 16 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 b17292d4e83..22eca15dcaf 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
@@ -57,6 +57,7 @@
 import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_excludeFromReplay;
 import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_makeExportable;
 import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_openCommandLogEntry;
+import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_openTarget;
 import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommand_replayOrRetry;
 import org.apache.causeway.extensions.commandlog.applib.fakescheduler.FakeScheduler;
 import org.apache.causeway.extensions.commandlog.applib.job.BackgroundCommandsJobControl;
@@ -88,6 +89,7 @@
         CommandReplayResultMapping_delete.class,
         ReplayableCommand_makeExportable.class,
         ReplayableCommand_openCommandLogEntry.class,
+        ReplayableCommand_openTarget.class,
         ReplayableCommand_replayOrRetry.class,
         ReplayableCommand_excludeFromReplay.class,
         ReplayableCommand_delete.class,
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_openTarget.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_openTarget.java
new file mode 100644
index 00000000000..b578a0cc257
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand_openTarget.java
@@ -0,0 +1,70 @@
+/*
+ *  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.Objects;
+import java.util.Optional;
+
+import org.apache.causeway.applib.annotation.Action;
+import org.apache.causeway.applib.annotation.ActionLayout;
+import org.apache.causeway.applib.annotation.MemberSupport;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.SemanticsOf;
+
+import lombok.RequiredArgsConstructor;
+
+@Action(
+        semantics = SemanticsOf.SAFE,
+        commandPublishing = Publishing.DISABLED,
+        domainEvent = ReplayableCommand_openTarget.DomainEvent.class,
+        executionPublishing = Publishing.DISABLED
+)
+@ActionLayout(
+        sequence = "1",
+        associateWith = "participants",
+        describedAs = "Opens the actual target from the participants table"
+)
+@RequiredArgsConstructor
+public class ReplayableCommand_openTarget {
+
+    public static class DomainEvent extends ReplayableCommand.ActionDomainEvent<ReplayableCommand_openTarget> {
+    }
+
+    private final ReplayableCommand replayableCommand;
+
+    @MemberSupport
+    public Object act() {
+        return actualTarget().orElse(null);
+    }
+
+    @MemberSupport
+    public String disableAct() {
+        return actualTarget().isPresent()
+                ? null
+                : "No actual target available";
+    }
+
+    private Optional<Object> actualTarget() {
+        return replayableCommand.getParticipants().stream()
+                .filter(participant -> participant.getRole() == ReplayableCommandParticipant.Role.TARGET)
+                .map(ReplayableCommandParticipant::getTarget)
+                .filter(Objects::nonNull)
+                .findFirst();
+    }
+}
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommandMappingTest.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommandMappingTest.java
index ea5161955d4..f73cecbc392 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommandMappingTest.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommandMappingTest.java
@@ -169,6 +169,44 @@ void target_remapping_participant_uses_lookup_and_keeps_unresolved_bookmarks_vis
         assertThat(participant.getTarget()).isNull();
     }
 
+    @Test
+    void open_target_mixin_opens_actual_target_from_participants() {
+        UUID interactionId = UUID.randomUUID();
+        Object actualTarget = new Object();
+        Bookmark recordedTargetBookmark = Bookmark.forLogicalTypeNameAndIdentifier("simple.SimpleObject", "1");
+        Bookmark actualTargetBookmark = Bookmark.forLogicalTypeNameAndIdentifier("simple.SimpleObject", "2");
+        CommandDto recordedCommandDto = commandWithTargetAndReferenceParameter(
+                "simple.SimpleObject", "1", "simple.SimpleObject", "3");
+        CommandLogEntry commandLogEntry = commandLogEntryWithCommandDto(recordedCommandDto);
+        when(commandLogEntry.getInteractionId()).thenReturn(interactionId);
+        when(commandLogEntry.getReplayState()).thenReturn(ReplayState.PENDING);
+        CommandReplayMappingListener listener = mock(CommandReplayMappingListener.class);
+        when(listener.lookup(commandLogEntry, recordedTargetBookmark)).thenReturn(Optional.of(actualTargetBookmark));
+        BookmarkService bookmarkService = mock(BookmarkService.class);
+        when(bookmarkService.lookup(actualTargetBookmark)).thenReturn(Optional.of(actualTarget));
+        ReplayableCommand replayableCommand = replayableCommand(interactionId, commandLogEntry, listener);
+        replayableCommand.bookmarkService = bookmarkService;
+        ReplayableCommand_openTarget openTarget = new ReplayableCommand_openTarget(replayableCommand);
+
+        assertThat(openTarget.disableAct()).isNull();
+        assertThat(openTarget.act()).isSameAs(actualTarget);
+    }
+
+    @Test
+    void open_target_mixin_is_disabled_without_actual_target() {
+        UUID interactionId = UUID.randomUUID();
+        CommandDto recordedCommandDto = commandWithTargetAndReferenceParameter(
+                "simple.SimpleObject", "1", "simple.SimpleObject", "3");
+        CommandLogEntry commandLogEntry = commandLogEntryWithCommandDto(recordedCommandDto);
+        when(commandLogEntry.getInteractionId()).thenReturn(interactionId);
+        when(commandLogEntry.getReplayState()).thenReturn(ReplayState.PENDING);
+        ReplayableCommand_openTarget openTarget = new ReplayableCommand_openTarget(
+                replayableCommand(interactionId, commandLogEntry));
+
+        assertThat(openTarget.disableAct()).isEqualTo("No actual target available");
+        assertThat(openTarget.act()).isNull();
+    }
+
     @Test
     void successful_replay_populates_unchanged_actual_bookmarks_and_objects() {
         UUID interactionId = UUID.randomUUID();
diff --git a/openspec/changes/replayable-command-open-target-refinement/.openspec.yaml b/openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/.openspec.yaml
similarity index 100%
rename from openspec/changes/replayable-command-open-target-refinement/.openspec.yaml
rename to openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/.openspec.yaml
diff --git a/openspec/changes/replayable-command-open-target-refinement/design.md b/openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/design.md
similarity index 100%
rename from openspec/changes/replayable-command-open-target-refinement/design.md
rename to openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/design.md
diff --git a/openspec/changes/replayable-command-open-target-refinement/proposal.md b/openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/proposal.md
similarity index 100%
rename from openspec/changes/replayable-command-open-target-refinement/proposal.md
rename to openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/proposal.md
diff --git a/openspec/changes/replayable-command-open-target-refinement/specs/replayable-command-remappings/spec.md b/openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/specs/replayable-command-remappings/spec.md
similarity index 100%
rename from openspec/changes/replayable-command-open-target-refinement/specs/replayable-command-remappings/spec.md
rename to openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/specs/replayable-command-remappings/spec.md
diff --git a/openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/tasks.md b/openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/tasks.md
new file mode 100644
index 00000000000..5739b6960f9
--- /dev/null
+++ b/openspec/changes/archive/2026-06-07-replayable-command-open-target-refinement/tasks.md
@@ -0,0 +1,14 @@
+## 1. Open Target Mixin
+
+- [x] 1.1 Add `ReplayableCommand_openTarget` mixin back to the replay package.
+- [x] 1.2 Configure the mixin action layout with `associateWith = "participants"` and `sequence = "1"`.
+- [x] 1.3 Implement the action to open the actual target object from the target participant.
+- [x] 1.4 Disable the action when no actual target object is available.
+- [x] 1.5 Register the mixin in `CausewayModuleExtCommandLogApplib`.
+
+## 2. Tests and Validation
+
+- [x] 2.1 Add or update tests for opening the actual target object.
+- [x] 2.2 Add or update tests for disabling the action when no actual target is available.
+- [x] 2.3 Run focused command log applib tests for replayable command mapping and participant behaviour.
+- [x] 2.4 Run `openspec validate replayable-command-open-target-refinement --strict`.
diff --git a/openspec/changes/replayable-command-open-target-refinement/tasks.md b/openspec/changes/replayable-command-open-target-refinement/tasks.md
deleted file mode 100644
index 3a17a45d1da..00000000000
--- a/openspec/changes/replayable-command-open-target-refinement/tasks.md
+++ /dev/null
@@ -1,14 +0,0 @@
-## 1. Open Target Mixin
-
-- [ ] 1.1 Add `ReplayableCommand_openTarget` mixin back to the replay package.
-- [ ] 1.2 Configure the mixin action layout with `associateWith = "participants"` and `sequence = "1"`.
-- [ ] 1.3 Implement the action to open the actual target object from the target participant.
-- [ ] 1.4 Disable the action when no actual target object is available.
-- [ ] 1.5 Register the mixin in `CausewayModuleExtCommandLogApplib`.
-
-## 2. Tests and Validation
-
-- [ ] 2.1 Add or update tests for opening the actual target object.
-- [ ] 2.2 Add or update tests for disabling the action when no actual target is available.
-- [ ] 2.3 Run focused command log applib tests for replayable command mapping and participant behaviour.
-- [ ] 2.4 Run `openspec validate replayable-command-open-target-refinement --strict`.
diff --git a/openspec/specs/replayable-command-remappings/spec.md b/openspec/specs/replayable-command-remappings/spec.md
index e6fb7032391..c8e015c97d8 100644
--- a/openspec/specs/replayable-command-remappings/spec.md
+++ b/openspec/specs/replayable-command-remappings/spec.md
@@ -132,7 +132,10 @@ The system SHALL provide fallback layout metadata for `ReplayableCommandParticip
 The participant layout SHALL expose role, parameter name, recorded bookmark, target, parameter, result, and actual bookmark in that order.
 The `ReplayableCommand` fallback layout SHALL include the participants collection as a table named `Participants` before the control field set.
 The `ReplayableCommand` object view MUST NOT expose separate target summary properties for target type or target identifier.
-The `ReplayableCommand` object view MUST NOT expose a separate open target action.
+The `ReplayableCommand` object view SHALL expose an `openTarget` action associated with the participants collection.
+The `openTarget` action SHALL have layout sequence `1`.
+The `openTarget` action SHALL open the actual target object when a target participant has an actual target available.
+The `openTarget` action SHALL be disabled when no actual target object is available.
 Target inspection and navigation SHALL be available through target rows in the participants collection.
 
 #### Scenario: User views participants table on a replayable command
@@ -147,5 +150,14 @@ Target inspection and navigation SHALL be available through target rows in the p
 #### Scenario: User inspects target through participants
 - **WHEN** a user views a replayable command using fallback layout metadata
 - **THEN** the layout does not display separate `targetType` or `targetId` properties
-- **AND** the layout does not display a separate `openTarget` action
 - **AND** target information is available through the participants table
+
+#### Scenario: User opens actual target from participants action
+- **WHEN** a replayable command target participant has an actual target object available
+- **THEN** the replayable command exposes an `openTarget` action associated with the participants collection
+- **AND** the `openTarget` action has layout sequence `1`
+- **AND** invoking the action opens the actual target object
+
+#### Scenario: User cannot open target without actual target
+- **WHEN** a replayable command has no target participant actual target object available
+- **THEN** the `openTarget` action is disabled