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