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/08/04 18:47:35 UTC
(causeway) 04/22: CAUSEWAY-3910: reconcile command recording core policy
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 2ddd9bc37fdaecf35b85c08bdbc12de27e4871e9
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Tue Aug 4 10:57:57 2026 +0100
CAUSEWAY-3910: reconcile command recording core policy
---
.../command/CommandRecordingSuppressed.java | 30 +++++
.../services/command/PauseCommandLoggingEvent.java | 37 ++++++
.../command/ResumeCommandLoggingEvent.java | 37 ++++++
.../services/command/CommandLoggingEventTest.java | 34 ++++++
.../config/pages/sections/causeway.extensions.adoc | 9 ++
.../core/config/CausewayConfiguration.java | 26 +++++
...figuration_CommandLogRecordingSupport_Test.java | 45 +++++++
.../executor/MemberExecutorServiceDefault.java | 11 ++
.../executor/MemberExecutorServiceDefaultTest.java | 130 +++++++++++++++++++++
.../applib/CausewayModuleExtCommandLogApplib.java | 4 +
.../commandlog/applib/dom/CommandLogEntry.java | 3 +-
.../applib/dom/replay/CommandExportManager.java | 3 +-
.../applib/dom/replay/CommandReplayManager.java | 3 +-
.../applib/dom/replay/ReplayableCommand.java | 4 +-
.../applib/subscriber/CommandLogPauseState.java | 53 +++++++++
.../subscriber/CommandLogPauseStateListener.java | 60 ++++++++++
.../subscriber/CommandSubscriberForCommandLog.java | 7 +-
.../replay/CommandRecordingSuppressionTest.java | 41 +++++++
.../subscriber/CommandLogPauseStateTest.java | 56 +++++++++
.../CommandSubscriberForCommandLogTest.java | 100 ++++++++++++++++
.../tasks.md | 24 ++--
.../services/InitialFixtureScriptsInstaller.java | 15 ++-
.../InitialFixtureScriptsInstallerTest.java | 87 ++++++++++++++
23 files changed, 798 insertions(+), 21 deletions(-)
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/command/CommandRecordingSuppressed.java b/api/applib/src/main/java/org/apache/causeway/applib/services/command/CommandRecordingSuppressed.java
new file mode 100644
index 00000000000..be11e9b2745
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/command/CommandRecordingSuppressed.java
@@ -0,0 +1,30 @@
+/*
+ * 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.applib.services.command;
+
+/**
+ * Marker interface for objects whose member interactions are ignored by command recording support.
+ *
+ * <p>Implementing this interface opts the target object out of command recording.
+ * The member invocation itself still executes normally; only command recording is suppressed.
+ *
+ * @since 4.0 {@index}
+ */
+public interface CommandRecordingSuppressed {
+}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/command/PauseCommandLoggingEvent.java b/api/applib/src/main/java/org/apache/causeway/applib/services/command/PauseCommandLoggingEvent.java
new file mode 100644
index 00000000000..5b3321563cb
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/command/PauseCommandLoggingEvent.java
@@ -0,0 +1,37 @@
+/*
+ * 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.applib.services.command;
+
+import org.apache.causeway.applib.events.EventObjectBase;
+
+/**
+ * Pauses command-log persistence for a scoped operation.
+ *
+ * <p>Every pause event should be paired with a {@link ResumeCommandLoggingEvent}, preferably from a
+ * {@code finally} block.
+ * Command logging implementations may support nested pauses by tracking pause depth.
+ *
+ * @since 4.0 {@index}
+ */
+public class PauseCommandLoggingEvent extends EventObjectBase<Object> {
+
+ public PauseCommandLoggingEvent(final Object source) {
+ super(source);
+ }
+}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/command/ResumeCommandLoggingEvent.java b/api/applib/src/main/java/org/apache/causeway/applib/services/command/ResumeCommandLoggingEvent.java
new file mode 100644
index 00000000000..bf6e2666cfe
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/command/ResumeCommandLoggingEvent.java
@@ -0,0 +1,37 @@
+/*
+ * 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.applib.services.command;
+
+import org.apache.causeway.applib.events.EventObjectBase;
+
+/**
+ * Resumes command-log persistence after a scoped pause.
+ *
+ * <p>One resume event should be posted for each matching {@link PauseCommandLoggingEvent}.
+ * Command logging implementations should tolerate unmatched resume events without allowing pause depth to become
+ * negative.
+ *
+ * @since 4.0 {@index}
+ */
+public class ResumeCommandLoggingEvent extends EventObjectBase<Object> {
+
+ public ResumeCommandLoggingEvent(final Object source) {
+ super(source);
+ }
+}
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/services/command/CommandLoggingEventTest.java b/api/applib/src/test/java/org/apache/causeway/applib/services/command/CommandLoggingEventTest.java
new file mode 100644
index 00000000000..dba054cb396
--- /dev/null
+++ b/api/applib/src/test/java/org/apache/causeway/applib/services/command/CommandLoggingEventTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.applib.services.command;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CommandLoggingEventTest {
+
+ @Test
+ void eventsRetainTheirSource() {
+ var source = new Object();
+
+ assertThat(new PauseCommandLoggingEvent(source).getSource()).isSameAs(source);
+ assertThat(new ResumeCommandLoggingEvent(source).getSource()).isSameAs(source);
+ }
+}
diff --git a/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc b/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc
index 18a0d81c8e5..b982956da51 100644
--- a/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc
+++ b/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc
@@ -29,6 +29,15 @@ persist
| null
+|
+[[causeway.extensions.command-log.recording-support]]
+causeway.extensions.command-log. +
+recording-support
+
+| DISABLED
+| Whether command recording support is enabled.
+
+
|
[[causeway.extensions.command-log.run-background-commands.batch-size]]
causeway.extensions.command-log. +
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
index 9478fb4155c..545f893e606 100644
--- a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
+++ b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
@@ -3651,6 +3651,15 @@ public record CommandLog(
*/
@DefaultValue("ENABLED")
PersistPolicy persist,
+ /**
+ * Whether command recording support is enabled.
+ *
+ * <p>When enabled, recording-aware policies may broaden command publishing eligibility while
+ * honoring {@code CommandRecordingSuppressed}. Disabled by default to preserve existing
+ * command-publishing behavior.
+ */
+ @DefaultValue("DISABLED")
+ RecordingSupport recordingSupport,
@DefaultValue
RunBackgroundCommands runBackgroundCommands) {
@@ -3674,6 +3683,23 @@ public enum PersistPolicy {
public boolean isDisabled() { return this == DISABLED; }
}
+ /**
+ * As per {@link CommandLog#recordingSupport()}.
+ */
+ public enum RecordingSupport {
+ /**
+ * Enable command recording support.
+ */
+ ENABLED,
+ /**
+ * Disable command recording support. This is the default.
+ */
+ DISABLED;
+
+ public boolean isEnabled() { return this == ENABLED; }
+ public boolean isDisabled() { return this == DISABLED; }
+ }
+
public record RunBackgroundCommands(
/**
* Limits the number of pending commands that the <code>RunBackgroundCommandsJob</code>
diff --git a/core/config/src/test/java/org/apache/causeway/core/config/CausewayConfiguration_CommandLogRecordingSupport_Test.java b/core/config/src/test/java/org/apache/causeway/core/config/CausewayConfiguration_CommandLogRecordingSupport_Test.java
new file mode 100644
index 00000000000..661723ba6ad
--- /dev/null
+++ b/core/config/src/test/java/org/apache/causeway/core/config/CausewayConfiguration_CommandLogRecordingSupport_Test.java
@@ -0,0 +1,45 @@
+/*
+ * 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.core.config;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.util.TestPropertyValues;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CausewayConfiguration_CommandLogRecordingSupport_Test {
+
+ private final ConfigurationFactory configurationFactory = new ConfigurationFactory();
+
+ @Test
+ void disabledByDefault() {
+ configurationFactory.test(
+ TestPropertyValues.empty(),
+ causeway -> assertThat(causeway.extensions().commandLog().recordingSupport())
+ .isEqualTo(CausewayConfiguration.Extensions.CommandLog.RecordingSupport.DISABLED));
+ }
+
+ @Test
+ void canBeEnabled() {
+ configurationFactory.test(
+ TestPropertyValues.of("causeway.extensions.command-log.recording-support=ENABLED"),
+ causeway -> assertThat(causeway.extensions().commandLog().recordingSupport().isEnabled()).isTrue());
+ }
+}
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
index 8be99d381c8..e9a91cb2492 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
@@ -32,6 +32,7 @@
import org.apache.causeway.applib.annotation.PriorityPrecedence;
import org.apache.causeway.applib.services.command.Command;
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
import org.apache.causeway.applib.services.iactn.ActionInvocation;
import org.apache.causeway.applib.services.iactn.Execution;
import org.apache.causeway.applib.services.iactn.Interaction;
@@ -409,6 +410,9 @@ void prepareCommandForPublishing(
final @NonNull ObjectMember objectMember,
final @NonNull FacetHolder facetHolder) {
+ if(targetSuppressesCommandRecording(interactionHead))
+ return;
+
if(interactionHead.isCommandForMember(command, objectMember)
&& isPublishingEnabled(facetHolder)) {
command.updater().setPublishingPhase(Command.CommandPublishingPhase.READY);
@@ -417,6 +421,13 @@ && isPublishingEnabled(facetHolder)) {
commandPublisher().ready(command);
}
+ private boolean targetSuppressesCommandRecording(final InteractionHead interactionHead) {
+ var ownerPojo = MmUnwrapUtils.single(interactionHead.owner());
+ var targetPojo = MmUnwrapUtils.single(interactionHead.target());
+ return ownerPojo instanceof CommandRecordingSuppressed
+ || targetPojo instanceof CommandRecordingSuppressed;
+ }
+
private CommandPublisher commandPublisher() {
return commandPublisherProvider.get();
}
diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefaultTest.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefaultTest.java
new file mode 100644
index 00000000000..ac429dff3f4
--- /dev/null
+++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefaultTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.core.runtimeservices.executor;
+
+import java.util.Optional;
+
+import jakarta.inject.Provider;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.services.command.Command;
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
+import org.apache.causeway.applib.services.metamodel.BeanSort;
+import org.apache.causeway.core.config.observation.CausewayObservationIntegration;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.interactions.InteractionHead;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
+import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class MemberExecutorServiceDefaultTest {
+
+ @Test
+ void suppressedTargetBypassesCommandPreparation() {
+ var commandPublisherProvider = commandPublisherProvider();
+ var service = newService(commandPublisherProvider);
+ var interactionHead = interactionHead(new Object(), new SuppressedTarget());
+ var command = mock(Command.class);
+ var objectMember = mock(ObjectMember.class);
+ var facetHolder = mock(FacetHolder.class);
+
+ service.prepareCommandForPublishing(command, interactionHead, objectMember, facetHolder);
+
+ verifyNoInteractions(commandPublisherProvider, command, objectMember, facetHolder);
+ }
+
+ @Test
+ void suppressedOwnerBypassesCommandPreparation() {
+ var commandPublisherProvider = commandPublisherProvider();
+ var service = newService(commandPublisherProvider);
+ var interactionHead = interactionHead(new SuppressedTarget(), new Object());
+ var command = mock(Command.class);
+ var objectMember = mock(ObjectMember.class);
+ var facetHolder = mock(FacetHolder.class);
+
+ service.prepareCommandForPublishing(command, interactionHead, objectMember, facetHolder);
+
+ verifyNoInteractions(commandPublisherProvider, command, objectMember, facetHolder);
+ }
+
+ @Test
+ void unmarkedTargetRetainsNormalPublisherNotification() {
+ var commandPublisher = mock(CommandPublisher.class);
+ var commandPublisherProvider = commandPublisherProvider();
+ when(commandPublisherProvider.get()).thenReturn(commandPublisher);
+ var service = newService(commandPublisherProvider);
+ var interactionHead = interactionHead(new Object(), new Object());
+ var command = mock(Command.class);
+
+ service.prepareCommandForPublishing(
+ command,
+ interactionHead,
+ mock(ObjectMember.class),
+ mock(FacetHolder.class));
+
+ verify(commandPublisher).ready(command);
+ }
+
+ private InteractionHead interactionHead(final Object ownerPojo, final Object targetPojo) {
+ var owner = managedObject(ownerPojo);
+ var target = managedObject(targetPojo);
+ var interactionHead = mock(InteractionHead.class);
+ when(interactionHead.owner()).thenReturn(owner);
+ when(interactionHead.target()).thenReturn(target);
+ return interactionHead;
+ }
+
+ private ManagedObject managedObject(final Object pojo) {
+ var objectSpecification = mock(ObjectSpecification.class);
+ var specificationLoader = mock(SpecificationLoader.class);
+ when(objectSpecification.isValue()).thenReturn(true);
+ when(objectSpecification.getBeanSort()).thenReturn(BeanSort.VALUE);
+ when(objectSpecification.getSpecificationLoader()).thenReturn(specificationLoader);
+ when(specificationLoader.specForType(pojo.getClass())).thenReturn(Optional.of(objectSpecification));
+ return ManagedObject.value(objectSpecification, pojo);
+ }
+
+ @SuppressWarnings("unchecked")
+ private Provider<CommandPublisher> commandPublisherProvider() {
+ return mock(Provider.class);
+ }
+
+ private MemberExecutorServiceDefault newService(final Provider<CommandPublisher> commandPublisherProvider) {
+ return new MemberExecutorServiceDefault(
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ commandPublisherProvider,
+ mock(CausewayObservationIntegration.class));
+ }
+
+ private static final class SuppressedTarget implements CommandRecordingSuppressed {
+ }
+}
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 169370b4001..e2f451f1cdb 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
@@ -52,6 +52,8 @@
import org.apache.causeway.extensions.commandlog.applib.job.BackgroundCommandsJobControl;
import org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob;
import org.apache.causeway.extensions.commandlog.applib.spi.RunBackgroundCommandsJobListener;
+import org.apache.causeway.extensions.commandlog.applib.subscriber.CommandLogPauseState;
+import org.apache.causeway.extensions.commandlog.applib.subscriber.CommandLogPauseStateListener;
import org.apache.causeway.extensions.commandlog.applib.subscriber.CommandSubscriberForCommandLog;
@Configuration(proxyBeanMethods = false)
@@ -95,6 +97,8 @@
RunBackgroundCommandsJobListener.Noop.class,
// @Service's
+ CommandLogPauseState.class,
+ CommandLogPauseStateListener.class,
CommandSubscriberForCommandLog.class,
CommandLogEntry.TableColumnOrderDefault.class,
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 99a167025f0..0fff50c3b11 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
@@ -51,6 +51,7 @@
import org.apache.causeway.applib.annotation.Publishing;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
import org.apache.causeway.applib.mixins.system.DomainChangeRecord;
import org.apache.causeway.applib.mixins.system.HasInteractionId;
import org.apache.causeway.applib.services.bookmark.Bookmark;
@@ -96,7 +97,7 @@
cssClassUiEvent = CommandLogEntry.CssClassUiEvent.class,
layoutUiEvent = CommandLogEntry.LayoutUiEvent.class)
public interface CommandLogEntry
-extends Comparable<CommandLogEntry>, DomainChangeRecord, HasCommandDto {
+extends Comparable<CommandLogEntry>, DomainChangeRecord, HasCommandDto, CommandRecordingSuppressed {
public final static String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandLogEntry";
public static final String SCHEMA = CausewayModuleExtCommandLogApplib.SCHEMA;
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java
index 275aee2c82a..bfe33fc6c0b 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java
@@ -45,6 +45,7 @@
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.services.command.CommandRecordingSuppressed;
import org.apache.causeway.applib.value.Clob;
import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
@@ -61,7 +62,7 @@ public record CommandExportManager(
@Property
@PropertyLayout(describedAs = "Only commands after this timestamp are available for export")
java.sql.Timestamp baseline,
- ReplayContext replayContext) implements ViewModel {
+ ReplayContext replayContext) implements ViewModel, CommandRecordingSuppressed {
public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager";
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
index 4dae27d07cb..d48d19a0522 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
@@ -45,6 +45,7 @@
import org.apache.causeway.applib.annotation.RestrictTo;
import org.apache.causeway.applib.annotation.SemanticsOf;
import org.apache.causeway.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
import org.apache.causeway.applib.util.schema.CommandDtoUtils;
import org.apache.causeway.applib.value.Blob;
import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
@@ -60,7 +61,7 @@ public record CommandReplayManager(
@Property
@PropertyLayout(describedAs = "Only commands after this baseline are listed for replay")
java.sql.Timestamp baseline,
- ReplayContext replayContext) implements ViewModel {
+ ReplayContext replayContext) implements ViewModel, CommandRecordingSuppressed {
public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayManager";
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
index 82e249c5948..8a7492fdacf 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
@@ -48,6 +48,7 @@
import org.apache.causeway.applib.jaxb.JavaTimeXMLGregorianCalendarMarshalling;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.bookmark.BookmarkService;
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
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;
@@ -76,7 +77,8 @@
@DomainObjectLayout//(cssClassFa = "terminal")
@Named(ReplayableCommand.LOGICAL_TYPE_NAME)
@AllArgsConstructor
-public final class ReplayableCommand implements ViewModel, Comparable<ReplayableCommand> {
+public final class ReplayableCommand
+implements ViewModel, Comparable<ReplayableCommand>, CommandRecordingSuppressed {
public static abstract class ActionDomainEvent<T>
extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseState.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseState.java
new file mode 100644
index 00000000000..ce18d0e1fec
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseState.java
@@ -0,0 +1,53 @@
+/*
+ * 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.subscriber;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.inject.Named;
+
+import org.springframework.stereotype.Service;
+
+import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
+
+/**
+ * Tracks application-wide, nested command-log persistence pauses.
+ *
+ * @since 4.0 {@index}
+ */
+@Service
+@Named(CommandLogPauseState.LOGICAL_TYPE_NAME)
+public class CommandLogPauseState {
+
+ static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandLogPauseState";
+
+ private final AtomicInteger pauseDepth = new AtomicInteger();
+
+ public boolean isPaused() {
+ return pauseDepth.get() > 0;
+ }
+
+ public void pause() {
+ pauseDepth.incrementAndGet();
+ }
+
+ public void resume() {
+ pauseDepth.updateAndGet(current -> current > 0 ? current - 1 : 0);
+ }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateListener.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateListener.java
new file mode 100644
index 00000000000..689be353859
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateListener.java
@@ -0,0 +1,60 @@
+/*
+ * 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.subscriber;
+
+import jakarta.annotation.Priority;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+
+import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.services.command.PauseCommandLoggingEvent;
+import org.apache.causeway.applib.services.command.ResumeCommandLoggingEvent;
+import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Maintains command-log pause state in response to application events.
+ *
+ * @since 4.0 {@index}
+ */
+@Service
+@Named(CommandLogPauseStateListener.LOGICAL_TYPE_NAME)
+@Priority(PriorityPrecedence.MIDPOINT)
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
+public class CommandLogPauseStateListener {
+
+ static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE
+ + ".CommandLogPauseStateListener";
+
+ private final CommandLogPauseState commandLogPauseState;
+
+ @EventListener(PauseCommandLoggingEvent.class)
+ public void onPauseCommandLoggingEvent(final PauseCommandLoggingEvent event) {
+ commandLogPauseState.pause();
+ }
+
+ @EventListener(ResumeCommandLoggingEvent.class)
+ public void onResumeCommandLoggingEvent(final ResumeCommandLoggingEvent event) {
+ commandLogPauseState.resume();
+ }
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
index 17f83fd2ac2..4cec276dd52 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
@@ -61,6 +61,7 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber {
final RepositoryService repositoryService;
final CausewayConfiguration causewayConfiguration;
final ClockService clockService;
+ final CommandLogPauseState commandLogPauseState;
@Override
public boolean isEnabled() {
@@ -70,7 +71,7 @@ public boolean isEnabled() {
@Override
public void onReady(final Command command) {
- if (!isEnabled())
+ if (!isEnabled() || commandLogPauseState.isPaused())
return;
var existingCommandLogEntryIfAny =
@@ -112,7 +113,7 @@ public void onReady(final Command command) {
@Override
public void onStarted(final Command command) {
- if (!isEnabled())
+ if (!isEnabled() || commandLogPauseState.isPaused())
return;
commandLogEntryRepository.findByInteractionId(command.getInteractionId())
@@ -124,7 +125,7 @@ public void onStarted(final Command command) {
@Override
public void onCompleted(final Command command) {
- if (!isEnabled())
+ if (!isEnabled() || commandLogPauseState.isPaused())
return;
commandLogEntryRepository.findByInteractionId(command.getInteractionId())
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandRecordingSuppressionTest.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandRecordingSuppressionTest.java
new file mode 100644
index 00000000000..6d526e2c54c
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandRecordingSuppressionTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.services.command.CommandRecordingSuppressed;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CommandRecordingSuppressionTest {
+
+ @Test
+ void commandlogHelperTypesOptOutOfRecording() {
+ assertThat(isSuppressed(CommandExportManager.class)).isTrue();
+ assertThat(isSuppressed(CommandReplayManager.class)).isTrue();
+ assertThat(isSuppressed(ReplayableCommand.class)).isTrue();
+ assertThat(isSuppressed(CommandLogEntry.class)).isTrue();
+ }
+
+ private boolean isSuppressed(final Class<?> type) {
+ return CommandRecordingSuppressed.class.isAssignableFrom(type);
+ }
+}
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateTest.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateTest.java
new file mode 100644
index 00000000000..4207881d427
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandLogPauseStateTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.subscriber;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.services.command.PauseCommandLoggingEvent;
+import org.apache.causeway.applib.services.command.ResumeCommandLoggingEvent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CommandLogPauseStateTest {
+
+ private final CommandLogPauseState pauseState = new CommandLogPauseState();
+ private final CommandLogPauseStateListener listener = new CommandLogPauseStateListener(pauseState);
+
+ @Test
+ void nestedPausesRequireMatchingResumes() {
+ listener.onPauseCommandLoggingEvent(new PauseCommandLoggingEvent(this));
+ listener.onPauseCommandLoggingEvent(new PauseCommandLoggingEvent(this));
+
+ listener.onResumeCommandLoggingEvent(new ResumeCommandLoggingEvent(this));
+ assertThat(pauseState.isPaused()).isTrue();
+
+ listener.onResumeCommandLoggingEvent(new ResumeCommandLoggingEvent(this));
+ assertThat(pauseState.isPaused()).isFalse();
+ }
+
+ @Test
+ void unmatchedResumeDoesNotMakeStatePausedOrNegative() {
+ listener.onResumeCommandLoggingEvent(new ResumeCommandLoggingEvent(this));
+ listener.onResumeCommandLoggingEvent(new ResumeCommandLoggingEvent(this));
+
+ assertThat(pauseState.isPaused()).isFalse();
+
+ listener.onPauseCommandLoggingEvent(new PauseCommandLoggingEvent(this));
+ listener.onResumeCommandLoggingEvent(new ResumeCommandLoggingEvent(this));
+ assertThat(pauseState.isPaused()).isFalse();
+ }
+}
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLogTest.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLogTest.java
new file mode 100644
index 00000000000..08f0f314223
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLogTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.subscriber;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.services.command.Command;
+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.ExecuteIn;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class CommandSubscriberForCommandLogTest {
+
+ @Test
+ void pausedSubscriberSkipsReadyStartedAndCompletedNotifications() {
+ var command = new Command(UUID.randomUUID());
+ var commandLogEntryRepository = mock(CommandLogEntryRepository.class);
+ var commandLogPauseState = new CommandLogPauseState();
+ commandLogPauseState.pause();
+ var subscriber = subscriber(commandLogEntryRepository, commandLogPauseState);
+
+ subscriber.onReady(command);
+ subscriber.onStarted(command);
+ subscriber.onCompleted(command);
+
+ verify(commandLogEntryRepository, never()).findByInteractionId(command.getInteractionId());
+ }
+
+ @Test
+ void resumedSubscriberCreatesEntryForSubsequentReadyCommand() {
+ var command = new Command(UUID.randomUUID());
+ var commandLogEntryRepository = mock(CommandLogEntryRepository.class);
+ when(commandLogEntryRepository.findByInteractionId(command.getInteractionId())).thenReturn(Optional.empty());
+ var commandLogPauseState = new CommandLogPauseState();
+ commandLogPauseState.pause();
+ commandLogPauseState.resume();
+ var subscriber = subscriber(commandLogEntryRepository, commandLogPauseState);
+
+ subscriber.onReady(command);
+
+ verify(commandLogEntryRepository)
+ .createEntryAndPersist(command, command.getParentInteractionId(), ExecuteIn.FOREGROUND);
+ }
+
+ @Test
+ void unpausedSubscriberSynchronizesStartedAndCompletedNotifications() {
+ var command = new Command(UUID.randomUUID());
+ var commandLogEntry = mock(CommandLogEntry.class);
+ var commandLogEntryRepository = mock(CommandLogEntryRepository.class);
+ when(commandLogEntryRepository.findByInteractionId(command.getInteractionId()))
+ .thenReturn(Optional.of(commandLogEntry));
+ var subscriber = subscriber(commandLogEntryRepository, new CommandLogPauseState());
+
+ subscriber.onStarted(command);
+ subscriber.onCompleted(command);
+
+ verify(commandLogEntry, times(2)).sync(command);
+ }
+
+ private CommandSubscriberForCommandLog subscriber(
+ final CommandLogEntryRepository commandLogEntryRepository,
+ final CommandLogPauseState commandLogPauseState) {
+ return new CommandSubscriberForCommandLog(
+ commandLogEntryRepository,
+ null,
+ null,
+ null,
+ commandLogPauseState) {
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+ };
+ }
+}
diff --git a/openspec/changes/reconcile-command-recording-core-policy/tasks.md b/openspec/changes/reconcile-command-recording-core-policy/tasks.md
index b7bbcf3c9a8..70681670026 100644
--- a/openspec/changes/reconcile-command-recording-core-policy/tasks.md
+++ b/openspec/changes/reconcile-command-recording-core-policy/tasks.md
@@ -1,29 +1,29 @@
## 1. Public contracts and configuration
-- [ ] 1.1 Add the Causeway 4 record-based `causeway.extensions.command-log.recording-support` configuration with `ENABLED` and `DISABLED` values, a disabled default, configuration documentation, and focused configuration tests.
-- [ ] 1.2 Add the core applib `CommandRecordingSuppressed`, `PauseCommandLoggingEvent`, and `ResumeCommandLoggingEvent` public contracts with API documentation and focused contract tests where applicable.
+- [x] 1.1 Add the Causeway 4 record-based `causeway.extensions.command-log.recording-support` configuration with `ENABLED` and `DISABLED` values, a disabled default, configuration documentation, and focused configuration tests.
+- [x] 1.2 Add the core applib `CommandRecordingSuppressed`, `PauseCommandLoggingEvent`, and `ResumeCommandLoggingEvent` public contracts with API documentation and focused contract tests where applicable.
## 2. Member execution suppression
-- [ ] 2.1 Adapt `MemberExecutorServiceDefault` so marked interaction owners or targets bypass command preparation while member execution continues normally.
-- [ ] 2.2 Add runtime-service tests covering marked owner, marked target, unmarked target, and unaffected member invocation behaviour.
+- [x] 2.1 Adapt `MemberExecutorServiceDefault` so marked interaction owners or targets bypass command preparation while member execution continues normally.
+- [x] 2.2 Add runtime-service tests covering marked owner, marked target, unmarked target, and unaffected member invocation behaviour.
## 3. Commandlog pause state
-- [ ] 3.1 Add the application-wide atomic pause-depth service and application-event listener, with unit tests for nesting and unmatched resume events.
-- [ ] 3.2 Make commandlog ready, started, and completed subscriber callbacks honour pause state, with focused subscriber or integration tests for creation and synchronization suppression.
+- [x] 3.1 Add the application-wide atomic pause-depth service and application-event listener, with unit tests for nesting and unmatched resume events.
+- [x] 3.2 Make commandlog ready, started, and completed subscriber callbacks honour pause state, with focused subscriber or integration tests for creation and synchronization suppression.
## 4. Framework-owned pause scope
-- [ ] 4.1 Publish paired pause and resume events around initial fixture-script installation using `try/finally` and without adding a commandlog extension dependency.
-- [ ] 4.2 Add fixture-installer tests covering successful execution, failed execution, event ordering, and guaranteed resume publication.
+- [x] 4.1 Publish paired pause and resume events around initial fixture-script installation using `try/finally` and without adding a commandlog extension dependency.
+- [x] 4.2 Add fixture-installer tests covering successful execution, failed execution, event ordering, and guaranteed resume publication.
## 5. Commandlog helper suppression
-- [ ] 5.1 Mark the existing export manager, replay manager, replayable command, and applicable command log entry abstractions as recording-suppressed.
-- [ ] 5.2 Add or adapt commandlog tests proving helper actions are not persisted through recording support while ordinary eligible interactions remain unaffected.
+- [x] 5.1 Mark the existing export manager, replay manager, replayable command, and applicable command log entry abstractions as recording-suppressed.
+- [x] 5.2 Add or adapt commandlog tests proving helper actions are not persisted through recording support while ordinary eligible interactions remain unaffected.
## 6. Verification
-- [ ] 6.1 Run the focused applib, configuration, runtime-services, testing-fixtures, and commandlog test suites and resolve regressions caused by the new contracts.
-- [ ] 6.2 Verify the external configuration property, Jakarta imports, public API documentation, and module dependency direction against the specification and programme ledger.
+- [x] 6.1 Run the focused applib, configuration, runtime-services, testing-fixtures, and commandlog test suites and resolve regressions caused by the new contracts.
+- [x] 6.2 Verify the external configuration property, Jakarta imports, public API documentation, and module dependency direction against the specification and programme ledger.
diff --git a/testing/fixtures/applib/src/main/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstaller.java b/testing/fixtures/applib/src/main/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstaller.java
index 0da1e4be909..262c42b6896 100644
--- a/testing/fixtures/applib/src/main/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstaller.java
+++ b/testing/fixtures/applib/src/main/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstaller.java
@@ -31,6 +31,9 @@
import org.apache.causeway.applib.annotation.PriorityPrecedence;
import org.apache.causeway.applib.events.metamodel.MetamodelEvent;
+import org.apache.causeway.applib.services.command.PauseCommandLoggingEvent;
+import org.apache.causeway.applib.services.command.ResumeCommandLoggingEvent;
+import org.apache.causeway.applib.services.eventbus.EventBusService;
import org.apache.causeway.core.config.CausewayConfiguration;
import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScript;
import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScripts;
@@ -48,15 +51,18 @@
public class InitialFixtureScriptsInstaller {
private final FixtureScripts fixtureScripts;
+ private final EventBusService eventBusService;
private FixtureScript initialFixtureScript;
@Inject
public InitialFixtureScriptsInstaller(
final CausewayConfiguration causewayConfiguration,
- final FixtureScripts fixtureScripts) {
+ final FixtureScripts fixtureScripts,
+ final EventBusService eventBusService) {
this.fixtureScripts = fixtureScripts;
+ this.eventBusService = eventBusService;
final Class<?> initialScript = causewayConfiguration.testing().fixtures().initialScript();
if (initialScript != null
@@ -79,7 +85,12 @@ public void onMetamodelEvent(final MetamodelEvent event) {
&& initialFixtureScript != null) {
log.info("install initial fixtures from script {}", initialFixtureScript.getFriendlyName());
- fixtureScripts.run(initialFixtureScript);
+ try {
+ eventBusService.post(new PauseCommandLoggingEvent(this));
+ fixtureScripts.run(initialFixtureScript);
+ } finally {
+ eventBusService.post(new ResumeCommandLoggingEvent(this));
+ }
}
}
diff --git a/testing/fixtures/applib/src/test/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstallerTest.java b/testing/fixtures/applib/src/test/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstallerTest.java
new file mode 100644
index 00000000000..6fa7e10a6f9
--- /dev/null
+++ b/testing/fixtures/applib/src/test/java/org/apache/causeway/testing/fixtures/applib/services/InitialFixtureScriptsInstallerTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.testing.fixtures.applib.services;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.events.metamodel.MetamodelEvent;
+import org.apache.causeway.applib.services.command.PauseCommandLoggingEvent;
+import org.apache.causeway.applib.services.command.ResumeCommandLoggingEvent;
+import org.apache.causeway.applib.services.eventbus.EventBusService;
+import org.apache.causeway.core.config.CausewayConfiguration;
+import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScript;
+import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScripts;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class InitialFixtureScriptsInstallerTest {
+
+ @Test
+ void publishesPauseRunsFixtureAndPublishesResumeInOrder() {
+ var fixtureScripts = mock(FixtureScripts.class);
+ var eventBusService = mock(EventBusService.class);
+ var installer = installer(fixtureScripts, eventBusService);
+
+ installer.onMetamodelEvent(MetamodelEvent.AFTER_METAMODEL_LOADED);
+
+ var inOrder = inOrder(eventBusService, fixtureScripts);
+ inOrder.verify(eventBusService).post(argThat(PauseCommandLoggingEvent.class::isInstance));
+ inOrder.verify(fixtureScripts).run(any(FixtureScript[].class));
+ inOrder.verify(eventBusService).post(argThat(ResumeCommandLoggingEvent.class::isInstance));
+ }
+
+ @Test
+ void publishesResumeWhenFixtureExecutionFails() {
+ var fixtureScripts = mock(FixtureScripts.class);
+ doThrow(new IllegalStateException("fixture failure"))
+ .when(fixtureScripts).run(any(FixtureScript[].class));
+ var eventBusService = mock(EventBusService.class);
+ var installer = installer(fixtureScripts, eventBusService);
+
+ assertThatThrownBy(() -> installer.onMetamodelEvent(MetamodelEvent.AFTER_METAMODEL_LOADED))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("fixture failure");
+
+ var inOrder = inOrder(eventBusService, fixtureScripts);
+ inOrder.verify(eventBusService).post(argThat(PauseCommandLoggingEvent.class::isInstance));
+ inOrder.verify(fixtureScripts).run(any(FixtureScript[].class));
+ inOrder.verify(eventBusService).post(argThat(ResumeCommandLoggingEvent.class::isInstance));
+ }
+
+ private InitialFixtureScriptsInstaller installer(
+ final FixtureScripts fixtureScripts,
+ final EventBusService eventBusService) {
+ var causewayConfiguration = mock(CausewayConfiguration.class);
+ var fixtures = new CausewayConfiguration.Testing.Fixtures(InitialScript.class, null);
+ when(causewayConfiguration.testing()).thenReturn(new CausewayConfiguration.Testing(fixtures));
+ return new InitialFixtureScriptsInstaller(causewayConfiguration, fixtureScripts, eventBusService);
+ }
+
+ public static class InitialScript extends FixtureScript {
+ @Override
+ protected void execute(final ExecutionContext executionContext) {
+ }
+ }
+}