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/04 22:46:58 UTC
(causeway) 39/42: CAUSEWAY-4010: adds persistent replay mapping listener
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-4010
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit d9f918630ed69c21c1b6fad9e9dbf40babb8ee2f
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Jun 4 23:11:46 2026 +0100
CAUSEWAY-4010: adds persistent replay mapping listener
---
.../core/config/CausewayConfiguration.java | 28 +++-
.../commandlog/applib/app/CommandLogMenu.java | 27 ++++
.../applib/dom/CommandReplayResultMapping.java | 130 ++++++++++++++++
.../dom/CommandReplayResultMapping.layout.xml | 41 ++++++
.../dom/CommandReplayResultMappingRepository.java | 39 +++++
...mmandReplayResultMappingRepositoryAbstract.java | 76 ++++++++++
.../spi/CommandReplayMappingListenerDefault.java | 6 +
...=> CommandReplayMappingListenerPersistent.java} | 52 ++++---
.../integtest/CommandLog_IntegTestAbstract.java | 35 +++++
.../CommandReplayMappingListenerDefaultTest.java | 14 ++
...ommandReplayMappingListenerPersistentTest.java} | 164 ++++++++++++++++-----
.../CausewayModuleExtCommandLogPersistenceJdo.java | 9 +-
.../jdo/dom/CommandReplayResultMapping.java | 93 ++++++++++++
.../dom/CommandReplayResultMappingRepository.java | 47 ++++++
.../CausewayModuleExtCommandLogPersistenceJpa.java | 12 +-
.../jpa/dom/CommandReplayResultMapping.java | 105 +++++++++++++
.../dom/CommandReplayResultMappingRepository.java | 47 ++++++
.../tasks.md | 56 +++----
18 files changed, 888 insertions(+), 93 deletions(-)
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 350ffe943d4..7dcc74cac90 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
@@ -3984,7 +3984,26 @@ public enum PersistPolicy {
public static class ReplayResultMapping {
/**
- * How the default command replay mapping listener handles a conflicting replay result mapping.
+ * Which built-in command replay mapping listener storage strategy to use.
+ *
+ * <p>
+ * External property: {@code causeway.extensions.command-log.replay-result-mapping.storage-strategy}.
+ * Accepted values are {@code IN_MEMORY} and {@code PERSISTENT}.
+ * </p>
+ */
+ public enum StorageStrategy {
+ /**
+ * Store replay result mappings in the default listener instance only.
+ */
+ IN_MEMORY,
+ /**
+ * Store replay result mappings in the configured command log persistence store.
+ */
+ PERSISTENT;
+ }
+
+ /**
+ * How the built-in command replay mapping listener handles a conflicting replay result mapping.
*
* <p>
* External property: {@code causeway.extensions.command-log.replay-result-mapping.on-conflict-policy}.
@@ -4007,7 +4026,12 @@ public enum OnConflictPolicy {
}
/**
- * If replay result mapping conflicts, what should the default listener do?
+ * Which built-in replay mapping listener storage strategy should be used?
+ */
+ private StorageStrategy storageStrategy = StorageStrategy.IN_MEMORY;
+
+ /**
+ * If replay result mapping conflicts, what should the built-in listener do?
*/
private OnConflictPolicy onConflictPolicy = OnConflictPolicy.THROW_EXCEPTION;
}
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 34172393184..3e4ccd62564 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
@@ -46,6 +46,8 @@
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.CommandReplayResultMapping;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMappingRepository;
import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager;
import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager_changeLimit;
import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager;
@@ -80,6 +82,7 @@ public static abstract class ActionDomainEvent<T>
final CommandLogEntryRepository commandLogEntryRepository;
+ final Optional<CommandReplayResultMappingRepository> commandReplayResultMappingRepository;
final ClockService clockService;
final ReplayContext replayContext;
@@ -166,6 +169,30 @@ public DomainEvent() { }
}
}
+ @Action(
+ commandPublishing = Publishing.DISABLED,
+ domainEvent = findReplayResultMappings.DomainEvent.class,
+ executionPublishing = Publishing.DISABLED,
+ semantics = SemanticsOf.SAFE,
+ typeOf = CommandReplayResultMapping.class
+ )
+ @ActionLayout(cssClassFa = "fa-search", sequence="45")
+ public class findReplayResultMappings {
+ public class DomainEvent extends ActionDomainEvent<findReplayResultMappings> {
+ public DomainEvent() { }
+ }
+
+ @MemberSupport public List<? extends CommandReplayResultMapping> act() {
+ return commandReplayResultMappingRepository
+ .map(CommandReplayResultMappingRepository::findAll)
+ .orElseGet(List::of);
+ }
+
+ @MemberSupport public boolean hideAct() {
+ return commandReplayResultMappingRepository.isEmpty();
+ }
+ }
+
@Action(
commandPublishing = Publishing.DISABLED,
domainEvent = exportManager.DomainEvent.class,
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMapping.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMapping.java
new file mode 100644
index 00000000000..d13ef46c2d3
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMapping.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.extensions.commandlog.applib.dom;
+
+import javax.inject.Named;
+
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.DomainObjectLayout;
+import org.apache.causeway.applib.annotation.Editing;
+import org.apache.causeway.applib.annotation.Programmatic;
+import org.apache.causeway.applib.annotation.Property;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
+
+import lombok.NoArgsConstructor;
+import lombok.experimental.UtilityClass;
+
+@Named(CommandReplayResultMapping.LOGICAL_TYPE_NAME)
+@DomainObject(
+ editing = Editing.DISABLED,
+ entityChangePublishing = Publishing.DISABLED
+)
+@DomainObjectLayout(
+ titleUiEvent = CommandReplayResultMapping.TitleUiEvent.class,
+ iconUiEvent = CommandReplayResultMapping.IconUiEvent.class,
+ cssClassUiEvent = CommandReplayResultMapping.CssClassUiEvent.class,
+ layoutUiEvent = CommandReplayResultMapping.LayoutUiEvent.class
+)
+@NoArgsConstructor
+public abstract class CommandReplayResultMapping {
+
+ public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayResultMapping";
+ public static final String SCHEMA = CausewayModuleExtCommandLogApplib.SCHEMA;
+ public static final String TABLE = "CommandReplayResultMapping";
+
+ public static class TitleUiEvent extends CausewayModuleExtCommandLogApplib.TitleUiEvent<CommandReplayResultMapping> { }
+ public static class IconUiEvent extends CausewayModuleExtCommandLogApplib.IconUiEvent<CommandReplayResultMapping> { }
+ public static class CssClassUiEvent extends CausewayModuleExtCommandLogApplib.CssClassUiEvent<CommandReplayResultMapping> { }
+ public static class LayoutUiEvent extends CausewayModuleExtCommandLogApplib.LayoutUiEvent<CommandReplayResultMapping> { }
+
+ @UtilityClass
+ public static class Nq {
+ public static final String FIND = LOGICAL_TYPE_NAME + ".find";
+ public static final String FIND_BY_RECORDED_BOOKMARK = LOGICAL_TYPE_NAME + ".findByRecordedBookmark";
+ }
+
+ @Programmatic
+ public void init(final Bookmark recordedBookmark, final Bookmark actualBookmark) {
+ setRecordedLogicalTypeName(recordedBookmark.getLogicalTypeName());
+ setRecordedIdentifier(recordedBookmark.getIdentifier());
+ setActualLogicalTypeName(actualBookmark.getLogicalTypeName());
+ setActualIdentifier(actualBookmark.getIdentifier());
+ }
+
+ @Programmatic
+ public Bookmark getRecordedBookmark() {
+ return Bookmark.forLogicalTypeNameAndIdentifier(getRecordedLogicalTypeName(), getRecordedIdentifier());
+ }
+
+ @Programmatic
+ public Bookmark getActualBookmark() {
+ return Bookmark.forLogicalTypeNameAndIdentifier(getActualLogicalTypeName(), getActualIdentifier());
+ }
+
+ @Property
+ @RecordedLogicalTypeName
+ public abstract String getRecordedLogicalTypeName();
+ public abstract void setRecordedLogicalTypeName(String recordedLogicalTypeName);
+
+ @Property
+ @RecordedIdentifier
+ public abstract String getRecordedIdentifier();
+ public abstract void setRecordedIdentifier(String recordedIdentifier);
+
+ @Property
+ @ActualLogicalTypeName
+ public abstract String getActualLogicalTypeName();
+ public abstract void setActualLogicalTypeName(String actualLogicalTypeName);
+
+ @Property
+ @ActualIdentifier
+ public abstract String getActualIdentifier();
+ public abstract void setActualIdentifier(String actualIdentifier);
+
+ public @interface RecordedLogicalTypeName {
+ String NAME = "recordedLogicalTypeName";
+ int MAX_LENGTH = 255;
+ boolean NULLABLE = false;
+ String ALLOWS_NULL = "false";
+ }
+
+ public @interface RecordedIdentifier {
+ String NAME = "recordedIdentifier";
+ int MAX_LENGTH = 2000;
+ boolean NULLABLE = false;
+ String ALLOWS_NULL = "false";
+ }
+
+ public @interface ActualLogicalTypeName {
+ String NAME = "actualLogicalTypeName";
+ int MAX_LENGTH = 255;
+ boolean NULLABLE = false;
+ String ALLOWS_NULL = "false";
+ }
+
+ public @interface ActualIdentifier {
+ String NAME = "actualIdentifier";
+ int MAX_LENGTH = 2000;
+ boolean NULLABLE = false;
+ String ALLOWS_NULL = "false";
+ }
+
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMapping.layout.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMapping.layout.xml
new file mode 100644
index 00000000000..e6d1718bc2b
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMapping.layout.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+ ~ 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.
+ -->
+<bs:grid xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/grid/bootstrap3 https://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd" xmlns:c="https://causeway.apache.org/applib/layout/component" xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+ <bs:row>
+ <bs:col span="12" unreferencedActions="true">
+ <c:domainObject bookmarking="AS_ROOT"/>
+ </bs:col>
+ </bs:row>
+ <bs:row>
+ <bs:col span="6">
+ <c:fieldSet name="Recorded Bookmark" id="recorded">
+ <c:property id="recordedLogicalTypeName"/>
+ <c:property id="recordedIdentifier"/>
+ </c:fieldSet>
+ </bs:col>
+ <bs:col span="6">
+ <c:fieldSet name="Actual Bookmark" id="actual">
+ <c:property id="actualLogicalTypeName"/>
+ <c:property id="actualIdentifier"/>
+ </c:fieldSet>
+ </bs:col>
+ </bs:row>
+</bs:grid>
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMappingRepository.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMappingRepository.java
new file mode 100644
index 00000000000..bf72c63dc24
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMappingRepository.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+
+/**
+ * Provides supporting functionality for querying and persisting replay result mappings.
+ *
+ * @since 2.1 {@index}
+ */
+public interface CommandReplayResultMappingRepository {
+
+ Optional<CommandReplayResultMapping> findByRecordedBookmark(final Bookmark recordedBookmark);
+
+ List<? extends CommandReplayResultMapping> findAll();
+
+ CommandReplayResultMapping createAndPersist(final Bookmark recordedBookmark, final Bookmark actualBookmark);
+
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMappingRepositoryAbstract.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMappingRepositoryAbstract.java
new file mode 100644
index 00000000000..5e32fcb3482
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandReplayResultMappingRepositoryAbstract.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.apache.causeway.applib.query.Query;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.applib.services.factory.FactoryService;
+import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.commons.internal.base._Casts;
+
+/**
+ * Provides supporting functionality for querying and persisting replay result mapping entities.
+ *
+ * @since 2.1 {@index}
+ */
+public abstract class CommandReplayResultMappingRepositoryAbstract<C extends CommandReplayResultMapping>
+implements CommandReplayResultMappingRepository {
+
+ @Inject Provider<RepositoryService> repositoryServiceProvider;
+ @Inject FactoryService factoryService;
+
+ private final Class<C> entityClass;
+
+ protected CommandReplayResultMappingRepositoryAbstract(final Class<C> entityClass) {
+ this.entityClass = entityClass;
+ }
+
+ @Override
+ public Optional<CommandReplayResultMapping> findByRecordedBookmark(final Bookmark recordedBookmark) {
+ return _Casts.uncheckedCast(repositoryService().firstMatch(
+ Query.named(entityClass, CommandReplayResultMapping.Nq.FIND_BY_RECORDED_BOOKMARK)
+ .withParameter("recordedLogicalTypeName", recordedBookmark.getLogicalTypeName())
+ .withParameter("recordedIdentifier", recordedBookmark.getIdentifier())));
+ }
+
+ @Override
+ public List<? extends CommandReplayResultMapping> findAll() {
+ return _Casts.uncheckedCast(repositoryService().allMatches(
+ Query.named(entityClass, CommandReplayResultMapping.Nq.FIND)));
+ }
+
+ @Override
+ public CommandReplayResultMapping createAndPersist(final Bookmark recordedBookmark, final Bookmark actualBookmark) {
+ C mapping = factoryService.detachedEntity(entityClass);
+ mapping.init(recordedBookmark, actualBookmark);
+ repositoryService().persistAndFlush(mapping);
+ return mapping;
+ }
+
+ private RepositoryService repositoryService() {
+ return repositoryServiceProvider.get();
+ }
+
+}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefault.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefault.java
index 39a58bdce6d..47045684b84 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefault.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefault.java
@@ -23,6 +23,7 @@
import java.util.Optional;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -91,6 +92,11 @@ public static class BeanFactory {
@Bean
@ConditionalOnMissingBean(CommandReplayMappingListener.class)
+ @ConditionalOnProperty(
+ prefix = "causeway.extensions.command-log.replay-result-mapping",
+ name = "storage-strategy",
+ havingValue = "IN_MEMORY",
+ matchIfMissing = true)
CommandReplayMappingListener commandReplayMappingListenerDefault(
final CausewayConfiguration causewayConfiguration) {
return new CommandReplayMappingListenerDefault(causewayConfiguration
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefault.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerPersistent.java
similarity index 57%
copy from extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefault.java
copy to extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerPersistent.java
index 39a58bdce6d..77ee5c94f48 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefault.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerPersistent.java
@@ -18,11 +18,11 @@
*/
package org.apache.causeway.extensions.commandlog.applib.spi;
-import java.util.HashMap;
-import java.util.Map;
import java.util.Optional;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -30,25 +30,26 @@
import org.apache.causeway.core.config.CausewayConfiguration;
import org.apache.causeway.core.config.CausewayConfiguration.Extensions.CommandLog.ReplayResultMapping.OnConflictPolicy;
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMapping;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMappingRepository;
import lombok.extern.log4j.Log4j2;
/**
- * Default in-memory implementation of {@link CommandReplayMappingListener}.
+ * Persistent implementation of {@link CommandReplayMappingListener}.
*
* @since 2.1 {@index}
*/
@Log4j2
-public class CommandReplayMappingListenerDefault implements CommandReplayMappingListener {
+public class CommandReplayMappingListenerPersistent implements CommandReplayMappingListener {
- private final Map<Bookmark, Bookmark> actualBookmarkByRecordedBookmark = new HashMap<>();
+ private final CommandReplayResultMappingRepository commandReplayResultMappingRepository;
private final OnConflictPolicy onConflictPolicy;
- public CommandReplayMappingListenerDefault() {
- this(OnConflictPolicy.THROW_EXCEPTION);
- }
-
- CommandReplayMappingListenerDefault(final OnConflictPolicy onConflictPolicy) {
+ public CommandReplayMappingListenerPersistent(
+ final CommandReplayResultMappingRepository commandReplayResultMappingRepository,
+ final OnConflictPolicy onConflictPolicy) {
+ this.commandReplayResultMappingRepository = commandReplayResultMappingRepository;
this.onConflictPolicy = onConflictPolicy;
}
@@ -56,7 +57,8 @@ public CommandReplayMappingListenerDefault() {
public Optional<Bookmark> remap(
final CommandLogEntry commandLogEntry,
final Bookmark recordedBookmark) {
- return Optional.ofNullable(actualBookmarkByRecordedBookmark.get(recordedBookmark));
+ return commandReplayResultMappingRepository.findByRecordedBookmark(recordedBookmark)
+ .map(CommandReplayResultMapping::getActualBookmark);
}
@Override
@@ -67,11 +69,13 @@ public void onReplayResultMapped(
if(recordedResult.equals(actualResult)) {
return;
}
- final Bookmark existingActualResult = actualBookmarkByRecordedBookmark.get(recordedResult);
- if(existingActualResult == null) {
- actualBookmarkByRecordedBookmark.put(recordedResult, actualResult);
+ final Optional<CommandReplayResultMapping> existingMappingIfAny =
+ commandReplayResultMappingRepository.findByRecordedBookmark(recordedResult);
+ if(existingMappingIfAny.isEmpty()) {
+ commandReplayResultMappingRepository.createAndPersist(recordedResult, actualResult);
return;
}
+ final Bookmark existingActualResult = existingMappingIfAny.get().getActualBookmark();
if(!existingActualResult.equals(actualResult)) {
final String message = String.format(
"Recorded result bookmark '%s' was already mapped to actual bookmark '%s', cannot remap to '%s'",
@@ -90,14 +94,22 @@ public void onReplayResultMapped(
public static class BeanFactory {
@Bean
+ @ConditionalOnBean(CommandReplayResultMappingRepository.class)
@ConditionalOnMissingBean(CommandReplayMappingListener.class)
- CommandReplayMappingListener commandReplayMappingListenerDefault(
+ @ConditionalOnProperty(
+ prefix = "causeway.extensions.command-log.replay-result-mapping",
+ name = "storage-strategy",
+ havingValue = "PERSISTENT")
+ CommandReplayMappingListener commandReplayMappingListenerPersistent(
+ final CommandReplayResultMappingRepository commandReplayResultMappingRepository,
final CausewayConfiguration causewayConfiguration) {
- return new CommandReplayMappingListenerDefault(causewayConfiguration
- .getExtensions()
- .getCommandLog()
- .getReplayResultMapping()
- .getOnConflictPolicy());
+ return new CommandReplayMappingListenerPersistent(
+ commandReplayResultMappingRepository,
+ causewayConfiguration
+ .getExtensions()
+ .getCommandLog()
+ .getReplayResultMapping()
+ .getOnConflictPolicy());
}
}
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
index 25f08554460..ed271259221 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
@@ -41,7 +41,10 @@
import org.apache.causeway.core.config.presets.CausewayPresets;
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.CommandReplayResultMappingRepository;
import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState;
+import org.apache.causeway.extensions.commandlog.applib.spi.CommandReplayMappingListenerPersistent;
+import org.apache.causeway.core.config.CausewayConfiguration.Extensions.CommandLog.ReplayResultMapping.OnConflictPolicy;
import org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter;
import org.apache.causeway.extensions.commandlog.applib.integtest.model.CounterRepository;
import org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter_bumpUsingMixin;
@@ -89,6 +92,37 @@ void beforeEach() {
protected abstract Counter newCounter(String name);
+ @Test
+ void replay_result_mapping_repository_and_persistent_listener() {
+ String suffix = UUID.randomUUID().toString();
+ Bookmark recordedResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "recorded-" + suffix);
+ Bookmark actualResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "actual-" + suffix);
+ Bookmark conflictingActualResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "conflicting-" + suffix);
+
+ CommandReplayMappingListenerPersistent listener = new CommandReplayMappingListenerPersistent(
+ commandReplayResultMappingRepository, OnConflictPolicy.THROW_EXCEPTION);
+
+ listener.onReplayResultMapped(recordedResult, actualResult, null);
+ listener.onReplayResultMapped(recordedResult, actualResult, null);
+
+ assertThat(commandReplayResultMappingRepository.findByRecordedBookmark(recordedResult))
+ .isPresent()
+ .get()
+ .extracting("actualBookmark")
+ .isEqualTo(actualResult);
+ assertThat(commandReplayResultMappingRepository.findAll())
+ .anySatisfy(mapping -> assertThat(mapping.getRecordedBookmark()).isEqualTo(recordedResult));
+ assertThat(listener.remap(null, recordedResult)).contains(actualResult);
+
+ Assertions.assertThatThrownBy(() -> listener.onReplayResultMapped(recordedResult, conflictingActualResult, null))
+ .isInstanceOf(IllegalStateException.class);
+ assertThat(commandReplayResultMappingRepository.findByRecordedBookmark(recordedResult))
+ .isPresent()
+ .get()
+ .extracting("actualBookmark")
+ .isEqualTo(actualResult);
+ }
+
@Test
void invoke_mixin() {
@@ -477,6 +511,7 @@ void test_all_the_repository_methods() {
}
@Inject CommandLogEntryRepository commandLogEntryRepository;
+ @Inject CommandReplayResultMappingRepository commandReplayResultMappingRepository;
@Inject SudoService sudoService;
@Inject ClockService clockService;
@Inject InteractionService interactionService;
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefaultTest.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefaultTest.java
index 4cd09e11d6d..3cc3dee61e3 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefaultTest.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefaultTest.java
@@ -22,12 +22,14 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
+import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.apache.causeway.applib.services.bookmark.Bookmark;
@@ -140,6 +142,18 @@ void autoconfiguration_backs_off_when_custom_listener_exists() {
}
}
+ @Test
+ void autoconfiguration_does_not_create_in_memory_listener_when_persistent_storage_is_configured() {
+ try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
+ context.getEnvironment().getPropertySources().addFirst(new MapPropertySource("test", Map.of(
+ "causeway.extensions.command-log.replay-result-mapping.storage-strategy", "PERSISTENT")));
+ context.register(CausewayConfigurationForTest.class, CommandReplayMappingListenerDefault.BeanFactory.class);
+ context.refresh();
+
+ assertThat(context.getBeansOfType(CommandReplayMappingListener.class)).isEmpty();
+ }
+ }
+
@Test
void autoconfiguration_uses_configured_conflict_policy() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefaultTest.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerPersistentTest.java
similarity index 53%
copy from extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefaultTest.java
copy to extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerPersistentTest.java
index 4cd09e11d6d..95d614d84d0 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerDefaultTest.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/spi/CommandReplayMappingListenerPersistentTest.java
@@ -22,24 +22,33 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.core.config.CausewayConfiguration;
import org.apache.causeway.core.config.CausewayConfiguration.Extensions.CommandLog.ReplayResultMapping.OnConflictPolicy;
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMapping;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMappingRepository;
-class CommandReplayMappingListenerDefaultTest {
+class CommandReplayMappingListenerPersistentTest {
@Test
void records_result_mapping_and_returns_it_from_remap() {
- CommandReplayMappingListenerDefault listener = new CommandReplayMappingListenerDefault();
+ FakeRepository repository = new FakeRepository();
+ CommandReplayMappingListenerPersistent listener = new CommandReplayMappingListenerPersistent(
+ repository, OnConflictPolicy.THROW_EXCEPTION);
CommandLogEntry commandLogEntry = mock(CommandLogEntry.class);
Bookmark recordedResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "1");
Bookmark actualResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "2");
@@ -47,22 +56,28 @@ void records_result_mapping_and_returns_it_from_remap() {
listener.onReplayResultMapped(recordedResult, actualResult, commandLogEntry);
assertThat(listener.remap(commandLogEntry, recordedResult)).contains(actualResult);
+ assertThat(repository.findAll()).hasSize(1);
}
@Test
void equal_recorded_and_actual_bookmark_is_not_recorded() {
- CommandReplayMappingListenerDefault listener = new CommandReplayMappingListenerDefault();
+ FakeRepository repository = new FakeRepository();
+ CommandReplayMappingListenerPersistent listener = new CommandReplayMappingListenerPersistent(
+ repository, OnConflictPolicy.THROW_EXCEPTION);
CommandLogEntry commandLogEntry = mock(CommandLogEntry.class);
Bookmark recordedResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "1");
listener.onReplayResultMapped(recordedResult, recordedResult, commandLogEntry);
assertThat(listener.remap(commandLogEntry, recordedResult)).isEmpty();
+ assertThat(repository.findAll()).isEmpty();
}
@Test
void repeated_recorded_bookmark_mapping_with_same_actual_bookmark_is_idempotent() {
- CommandReplayMappingListenerDefault listener = new CommandReplayMappingListenerDefault();
+ FakeRepository repository = new FakeRepository();
+ CommandReplayMappingListenerPersistent listener = new CommandReplayMappingListenerPersistent(
+ repository, OnConflictPolicy.THROW_EXCEPTION);
CommandLogEntry commandLogEntry = mock(CommandLogEntry.class);
Bookmark recordedResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "1");
Bookmark actualResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "2");
@@ -71,11 +86,14 @@ void repeated_recorded_bookmark_mapping_with_same_actual_bookmark_is_idempotent(
listener.onReplayResultMapped(recordedResult, actualResult, commandLogEntry);
assertThat(listener.remap(commandLogEntry, recordedResult)).contains(actualResult);
+ assertThat(repository.findAll()).hasSize(1);
}
@Test
void repeated_recorded_bookmark_mapping_with_different_actual_bookmark_is_rejected_by_default() {
- CommandReplayMappingListenerDefault listener = new CommandReplayMappingListenerDefault();
+ FakeRepository repository = new FakeRepository();
+ CommandReplayMappingListenerPersistent listener = new CommandReplayMappingListenerPersistent(
+ repository, OnConflictPolicy.THROW_EXCEPTION);
CommandLogEntry commandLogEntry = mock(CommandLogEntry.class);
Bookmark recordedResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "1");
Bookmark firstActualResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "2");
@@ -93,7 +111,9 @@ void repeated_recorded_bookmark_mapping_with_different_actual_bookmark_is_reject
@Test
void repeated_recorded_bookmark_mapping_with_different_actual_bookmark_is_logged_and_ignored_when_configured() {
- CommandReplayMappingListenerDefault listener = new CommandReplayMappingListenerDefault(OnConflictPolicy.LOG_AND_CONTINUE);
+ FakeRepository repository = new FakeRepository();
+ CommandReplayMappingListenerPersistent listener = new CommandReplayMappingListenerPersistent(
+ repository, OnConflictPolicy.LOG_AND_CONTINUE);
CommandLogEntry commandLogEntry = mock(CommandLogEntry.class);
Bookmark recordedResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "1");
Bookmark firstActualResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "2");
@@ -103,34 +123,38 @@ void repeated_recorded_bookmark_mapping_with_different_actual_bookmark_is_logged
listener.onReplayResultMapped(recordedResult, secondActualResult, commandLogEntry);
assertThat(listener.remap(commandLogEntry, recordedResult)).contains(firstActualResult);
+ assertThat(repository.findAll()).hasSize(1);
}
@Test
- void unmapped_bookmark_returns_no_replacement() {
- CommandReplayMappingListenerDefault listener = new CommandReplayMappingListenerDefault();
- CommandLogEntry commandLogEntry = mock(CommandLogEntry.class);
- Bookmark recordedBookmark = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "1");
+ void autoconfiguration_creates_persistent_listener_when_configured() {
+ try (AnnotationConfigApplicationContext context = contextWithPersistentStorage(
+ CausewayConfigurationForTest.class,
+ RepositoryConfiguration.class,
+ CommandReplayMappingListenerPersistent.BeanFactory.class)) {
- assertThat(listener.remap(commandLogEntry, recordedBookmark)).isEmpty();
+ assertThat(context.getBean(CommandReplayMappingListener.class))
+ .isInstanceOf(CommandReplayMappingListenerPersistent.class);
+ }
}
@Test
- void autoconfiguration_creates_default_listener_when_missing() {
- try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ void autoconfiguration_does_not_create_persistent_listener_without_repository() {
+ try (AnnotationConfigApplicationContext context = contextWithPersistentStorage(
CausewayConfigurationForTest.class,
- CommandReplayMappingListenerDefault.BeanFactory.class)) {
+ CommandReplayMappingListenerPersistent.BeanFactory.class)) {
- assertThat(context.getBean(CommandReplayMappingListener.class))
- .isInstanceOf(CommandReplayMappingListenerDefault.class);
+ assertThat(context.getBeansOfType(CommandReplayMappingListener.class)).isEmpty();
}
}
@Test
void autoconfiguration_backs_off_when_custom_listener_exists() {
- try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ try (AnnotationConfigApplicationContext context = contextWithPersistentStorage(
CausewayConfigurationForTest.class,
+ RepositoryConfiguration.class,
CustomListenerConfiguration.class,
- CommandReplayMappingListenerDefault.BeanFactory.class)) {
+ CommandReplayMappingListenerPersistent.BeanFactory.class)) {
assertThat(context.getBeansOfType(CommandReplayMappingListener.class))
.hasSize(1)
@@ -140,22 +164,13 @@ void autoconfiguration_backs_off_when_custom_listener_exists() {
}
}
- @Test
- void autoconfiguration_uses_configured_conflict_policy() {
- try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
- LogAndContinueCausewayConfigurationForTest.class,
- CommandReplayMappingListenerDefault.BeanFactory.class)) {
- CommandReplayMappingListener listener = context.getBean(CommandReplayMappingListener.class);
- CommandLogEntry commandLogEntry = mock(CommandLogEntry.class);
- Bookmark recordedResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "1");
- Bookmark firstActualResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "2");
- Bookmark secondActualResult = Bookmark.forLogicalTypeNameAndIdentifier("demoInvoice", "3");
-
- listener.onReplayResultMapped(recordedResult, firstActualResult, commandLogEntry);
- listener.onReplayResultMapped(recordedResult, secondActualResult, commandLogEntry);
-
- assertThat(listener.remap(commandLogEntry, recordedResult)).contains(firstActualResult);
- }
+ private static AnnotationConfigApplicationContext contextWithPersistentStorage(final Class<?>... configClasses) {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ context.getEnvironment().getPropertySources().addFirst(new MapPropertySource("test", Map.of(
+ "causeway.extensions.command-log.replay-result-mapping.storage-strategy", "PERSISTENT")));
+ context.register(configClasses);
+ context.refresh();
+ return context;
}
@Configuration(proxyBeanMethods = false)
@@ -169,14 +184,13 @@ CausewayConfiguration causewayConfiguration() {
}
@Configuration(proxyBeanMethods = false)
- static class LogAndContinueCausewayConfigurationForTest {
+ static class RepositoryConfiguration {
+
+ static final FakeRepository REPOSITORY = new FakeRepository();
@Bean
- CausewayConfiguration causewayConfiguration() {
- CausewayConfiguration causewayConfiguration = new CausewayConfiguration(new StandardEnvironment(), Optional.empty());
- causewayConfiguration.getExtensions().getCommandLog().getReplayResultMapping()
- .setOnConflictPolicy(OnConflictPolicy.LOG_AND_CONTINUE);
- return causewayConfiguration;
+ CommandReplayResultMappingRepository commandReplayResultMappingRepository() {
+ return REPOSITORY;
}
}
@@ -193,4 +207,74 @@ CommandReplayMappingListener customCommandReplayMappingListener() {
}
+ static class FakeRepository implements CommandReplayResultMappingRepository {
+
+ private final Map<Bookmark, CommandReplayResultMapping> mappings = new LinkedHashMap<>();
+
+ @Override
+ public Optional<CommandReplayResultMapping> findByRecordedBookmark(final Bookmark recordedBookmark) {
+ return Optional.ofNullable(mappings.get(recordedBookmark));
+ }
+
+ @Override
+ public List<? extends CommandReplayResultMapping> findAll() {
+ return new ArrayList<>(mappings.values());
+ }
+
+ @Override
+ public CommandReplayResultMapping createAndPersist(final Bookmark recordedBookmark, final Bookmark actualBookmark) {
+ FakeMapping mapping = new FakeMapping();
+ mapping.init(recordedBookmark, actualBookmark);
+ mappings.put(recordedBookmark, mapping);
+ return mapping;
+ }
+ }
+
+ static class FakeMapping extends CommandReplayResultMapping {
+
+ private String recordedLogicalTypeName;
+ private String recordedIdentifier;
+ private String actualLogicalTypeName;
+ private String actualIdentifier;
+
+ @Override
+ public String getRecordedLogicalTypeName() {
+ return recordedLogicalTypeName;
+ }
+
+ @Override
+ public void setRecordedLogicalTypeName(final String recordedLogicalTypeName) {
+ this.recordedLogicalTypeName = recordedLogicalTypeName;
+ }
+
+ @Override
+ public String getRecordedIdentifier() {
+ return recordedIdentifier;
+ }
+
+ @Override
+ public void setRecordedIdentifier(final String recordedIdentifier) {
+ this.recordedIdentifier = recordedIdentifier;
+ }
+
+ @Override
+ public String getActualLogicalTypeName() {
+ return actualLogicalTypeName;
+ }
+
+ @Override
+ public void setActualLogicalTypeName(final String actualLogicalTypeName) {
+ this.actualLogicalTypeName = actualLogicalTypeName;
+ }
+
+ @Override
+ public String getActualIdentifier() {
+ return actualIdentifier;
+ }
+
+ @Override
+ public void setActualIdentifier(final String actualIdentifier) {
+ this.actualIdentifier = actualIdentifier;
+ }
+ }
}
diff --git a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/CausewayModuleExtCommandLogPersistenceJdo.java b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/CausewayModuleExtCommandLogPersistenceJdo.java
index 5d0ce7f1fd2..7d18f2a7d2b 100644
--- a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/CausewayModuleExtCommandLogPersistenceJdo.java
+++ b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/CausewayModuleExtCommandLogPersistenceJdo.java
@@ -22,8 +22,11 @@
import org.springframework.context.annotation.Import;
import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
+import org.apache.causeway.extensions.commandlog.applib.spi.CommandReplayMappingListenerPersistent;
import org.apache.causeway.extensions.commandlog.jdo.dom.CommandLogEntry;
import org.apache.causeway.extensions.commandlog.jdo.dom.CommandLogEntryRepository;
+import org.apache.causeway.extensions.commandlog.jdo.dom.CommandReplayResultMapping;
+import org.apache.causeway.extensions.commandlog.jdo.dom.CommandReplayResultMappingRepository;
import org.apache.causeway.persistence.jdo.datanucleus.CausewayModulePersistenceJdoDatanucleus;
import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScript;
import org.apache.causeway.testing.fixtures.applib.modules.ModuleWithFixtures;
@@ -40,9 +43,12 @@
// @Service's
CommandLogEntryRepository.class,
+ CommandReplayResultMappingRepository.class,
+ CommandReplayMappingListenerPersistent.BeanFactory.class,
// entities
- CommandLogEntry.class
+ CommandLogEntry.class,
+ CommandReplayResultMapping.class
})
public class CausewayModuleExtCommandLogPersistenceJdo {
@@ -57,6 +63,7 @@ public FixtureScript teardownFixture() {
return new TeardownFixtureJdoAbstract() {
@Override
protected void execute(final ExecutionContext executionContext) {
+ deleteFrom(CommandReplayResultMapping.class);
deleteFrom(CommandLogEntry.class);
}
};
diff --git a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandReplayResultMapping.java b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandReplayResultMapping.java
new file mode 100644
index 00000000000..42ac70f9c9a
--- /dev/null
+++ b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandReplayResultMapping.java
@@ -0,0 +1,93 @@
+/*
+ * 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.jdo.dom;
+
+import javax.inject.Named;
+import javax.jdo.annotations.Column;
+import javax.jdo.annotations.IdentityType;
+import javax.jdo.annotations.Index;
+import javax.jdo.annotations.Indices;
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Queries;
+import javax.jdo.annotations.Query;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.Editing;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.jaxb.PersistentEntityAdapter;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMapping.Nq;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@PersistenceCapable(
+ identityType = IdentityType.DATASTORE,
+ schema = CommandReplayResultMapping.SCHEMA,
+ table = CommandReplayResultMapping.TABLE)
+@Indices({
+ @Index(name = "CommandReplayResultMapping__recorded__IDX", members = { "recordedLogicalTypeName", "recordedIdentifier" }, unique = "true"),
+})
+@Queries({
+ @Query(
+ name = Nq.FIND,
+ value = "SELECT "
+ + " FROM " + CommandReplayResultMapping.FQCN + " "
+ + " ORDER BY recordedLogicalTypeName ASC, recordedIdentifier ASC"),
+ @Query(
+ name = Nq.FIND_BY_RECORDED_BOOKMARK,
+ value = "SELECT "
+ + " FROM " + CommandReplayResultMapping.FQCN + " "
+ + " WHERE recordedLogicalTypeName == :recordedLogicalTypeName "
+ + " && recordedIdentifier == :recordedIdentifier")
+})
+@Named(org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMapping.LOGICAL_TYPE_NAME)
+@DomainObject(
+ editing = Editing.DISABLED,
+ entityChangePublishing = Publishing.DISABLED
+)
+@XmlJavaTypeAdapter(PersistentEntityAdapter.class)
+@NoArgsConstructor
+public class CommandReplayResultMapping
+extends org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMapping {
+
+ public static final String FQCN = "org.apache.causeway.extensions.commandlog.jdo.dom.CommandReplayResultMapping";
+
+ @Column(allowsNull = RecordedLogicalTypeName.ALLOWS_NULL, length = RecordedLogicalTypeName.MAX_LENGTH)
+ @RecordedLogicalTypeName
+ @Getter @Setter
+ private String recordedLogicalTypeName;
+
+ @Column(allowsNull = RecordedIdentifier.ALLOWS_NULL, length = RecordedIdentifier.MAX_LENGTH)
+ @RecordedIdentifier
+ @Getter @Setter
+ private String recordedIdentifier;
+
+ @Column(allowsNull = ActualLogicalTypeName.ALLOWS_NULL, length = ActualLogicalTypeName.MAX_LENGTH)
+ @ActualLogicalTypeName
+ @Getter @Setter
+ private String actualLogicalTypeName;
+
+ @Column(allowsNull = ActualIdentifier.ALLOWS_NULL, length = ActualIdentifier.MAX_LENGTH)
+ @ActualIdentifier
+ @Getter @Setter
+ private String actualIdentifier;
+
+}
diff --git a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandReplayResultMappingRepository.java b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandReplayResultMappingRepository.java
new file mode 100644
index 00000000000..5484d3dd9e8
--- /dev/null
+++ b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandReplayResultMappingRepository.java
@@ -0,0 +1,47 @@
+/*
+ * 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.jdo.dom;
+
+import javax.inject.Named;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMappingRepositoryAbstract;
+import org.apache.causeway.extensions.commandlog.jdo.CausewayModuleExtCommandLogPersistenceJdo;
+
+/**
+ * Provides supporting functionality for querying and persisting replay result mappings.
+ */
+@Service
+@Named(CommandReplayResultMappingRepository.LOGICAL_TYPE_NAME)
+@javax.annotation.Priority(PriorityPrecedence.MIDPOINT)
+@Qualifier("Jdo")
+public class CommandReplayResultMappingRepository
+extends CommandReplayResultMappingRepositoryAbstract<CommandReplayResultMapping> {
+
+ public static final String LOGICAL_TYPE_NAME =
+ CausewayModuleExtCommandLogPersistenceJdo.NAMESPACE + ".CommandReplayResultMappingRepository";
+
+ public CommandReplayResultMappingRepository() {
+ super(CommandReplayResultMapping.class);
+ }
+
+}
diff --git a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/CausewayModuleExtCommandLogPersistenceJpa.java b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/CausewayModuleExtCommandLogPersistenceJpa.java
index 681c811a2f7..cc7c60cd1f0 100644
--- a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/CausewayModuleExtCommandLogPersistenceJpa.java
+++ b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/CausewayModuleExtCommandLogPersistenceJpa.java
@@ -23,9 +23,12 @@
import org.springframework.context.annotation.Import;
import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
+import org.apache.causeway.extensions.commandlog.applib.spi.CommandReplayMappingListenerPersistent;
import org.apache.causeway.extensions.commandlog.jpa.dom.CommandLogEntry;
import org.apache.causeway.extensions.commandlog.jpa.dom.CommandLogEntryPK;
import org.apache.causeway.extensions.commandlog.jpa.dom.CommandLogEntryRepository;
+import org.apache.causeway.extensions.commandlog.jpa.dom.CommandReplayResultMapping;
+import org.apache.causeway.extensions.commandlog.jpa.dom.CommandReplayResultMappingRepository;
import org.apache.causeway.persistence.jpa.eclipselink.CausewayModulePersistenceJpaEclipselink;
import org.apache.causeway.testing.fixtures.applib.CausewayModuleTestingFixturesApplib;
import org.apache.causeway.testing.fixtures.applib.fixturescripts.FixtureScript;
@@ -45,12 +48,16 @@
// @Service's
CommandLogEntryRepository.class,
CommandLogEntryPK.Semantics.class,
+ CommandReplayResultMappingRepository.class,
+ CommandReplayMappingListenerPersistent.BeanFactory.class,
// entities
- CommandLogEntry.class
+ CommandLogEntry.class,
+ CommandReplayResultMapping.class
})
@EntityScan(basePackageClasses = {
- CommandLogEntry.class
+ CommandLogEntry.class,
+ CommandReplayResultMapping.class
})
public class CausewayModuleExtCommandLogPersistenceJpa {
@@ -65,6 +72,7 @@ public FixtureScript teardownFixture() {
return new TeardownFixtureJpaAbstract() {
@Override
protected void execute(final ExecutionContext executionContext) {
+ deleteFrom(CommandReplayResultMapping.class);
deleteFrom(CommandLogEntry.class);
}
};
diff --git a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandReplayResultMapping.java b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandReplayResultMapping.java
new file mode 100644
index 00000000000..857361881c9
--- /dev/null
+++ b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandReplayResultMapping.java
@@ -0,0 +1,105 @@
+/*
+ * 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.jpa.dom;
+
+import javax.inject.Named;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EntityListeners;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Index;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.Editing;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.jaxb.PersistentEntityAdapter;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMapping.Nq;
+import org.apache.causeway.persistence.jpa.applib.integration.CausewayEntityListener;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(
+ schema = CommandReplayResultMapping.SCHEMA,
+ name = CommandReplayResultMapping.TABLE,
+ indexes = {
+ @Index(name = "CommandReplayResultMapping__recorded__IDX", columnList = "recordedLogicalTypeName, recordedIdentifier")
+ },
+ uniqueConstraints = {
+ @UniqueConstraint(name = "CommandReplayResultMapping__recorded__UNQ", columnNames = { "recordedLogicalTypeName", "recordedIdentifier" })
+ }
+)
+@NamedQueries({
+ @NamedQuery(
+ name = Nq.FIND,
+ query = "SELECT m "
+ + " FROM CommandReplayResultMapping m "
+ + " ORDER BY m.recordedLogicalTypeName ASC, m.recordedIdentifier ASC"),
+ @NamedQuery(
+ name = Nq.FIND_BY_RECORDED_BOOKMARK,
+ query = "SELECT m "
+ + " FROM CommandReplayResultMapping m "
+ + " WHERE m.recordedLogicalTypeName = :recordedLogicalTypeName "
+ + " AND m.recordedIdentifier = :recordedIdentifier")
+})
+@Named(org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMapping.LOGICAL_TYPE_NAME)
+@DomainObject(
+ editing = Editing.DISABLED,
+ entityChangePublishing = Publishing.DISABLED
+)
+@XmlJavaTypeAdapter(PersistentEntityAdapter.class)
+@EntityListeners(CausewayEntityListener.class)
+@NoArgsConstructor
+public class CommandReplayResultMapping
+extends org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMapping {
+
+ @Id
+ @GeneratedValue
+ @Getter @Setter
+ private Long id;
+
+ @Column(nullable = RecordedLogicalTypeName.NULLABLE, length = RecordedLogicalTypeName.MAX_LENGTH)
+ @RecordedLogicalTypeName
+ @Getter @Setter
+ private String recordedLogicalTypeName;
+
+ @Column(nullable = RecordedIdentifier.NULLABLE, length = RecordedIdentifier.MAX_LENGTH)
+ @RecordedIdentifier
+ @Getter @Setter
+ private String recordedIdentifier;
+
+ @Column(nullable = ActualLogicalTypeName.NULLABLE, length = ActualLogicalTypeName.MAX_LENGTH)
+ @ActualLogicalTypeName
+ @Getter @Setter
+ private String actualLogicalTypeName;
+
+ @Column(nullable = ActualIdentifier.NULLABLE, length = ActualIdentifier.MAX_LENGTH)
+ @ActualIdentifier
+ @Getter @Setter
+ private String actualIdentifier;
+
+}
diff --git a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandReplayResultMappingRepository.java b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandReplayResultMappingRepository.java
new file mode 100644
index 00000000000..8e69e512f54
--- /dev/null
+++ b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandReplayResultMappingRepository.java
@@ -0,0 +1,47 @@
+/*
+ * 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.jpa.dom;
+
+import javax.inject.Named;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandReplayResultMappingRepositoryAbstract;
+import org.apache.causeway.extensions.commandlog.jpa.CausewayModuleExtCommandLogPersistenceJpa;
+
+/**
+ * Provides supporting functionality for querying and persisting replay result mappings.
+ */
+@Service
+@Named(CommandReplayResultMappingRepository.LOGICAL_TYPE_NAME)
+@javax.annotation.Priority(PriorityPrecedence.MIDPOINT)
+@Qualifier("Jpa")
+public class CommandReplayResultMappingRepository
+extends CommandReplayResultMappingRepositoryAbstract<CommandReplayResultMapping> {
+
+ public static final String LOGICAL_TYPE_NAME =
+ CausewayModuleExtCommandLogPersistenceJpa.NAMESPACE + ".CommandReplayResultMappingRepository";
+
+ public CommandReplayResultMappingRepository() {
+ super(CommandReplayResultMapping.class);
+ }
+
+}
diff --git a/openspec/changes/persistent-command-replay-mapping-listener/tasks.md b/openspec/changes/persistent-command-replay-mapping-listener/tasks.md
index e6b3852fb73..0855f0e232b 100644
--- a/openspec/changes/persistent-command-replay-mapping-listener/tasks.md
+++ b/openspec/changes/persistent-command-replay-mapping-listener/tasks.md
@@ -1,45 +1,45 @@
## 1. Configuration and Conditional Beans
-- [ ] 1.1 Add a built-in replay mapping listener storage strategy enum with `IN_MEMORY` and `PERSISTENT` values.
-- [ ] 1.2 Add the storage strategy property to `CausewayConfiguration` with `IN_MEMORY` as the default.
-- [ ] 1.3 Update the existing in-memory listener bean factory so it is created only for `IN_MEMORY`, only when no custom listener exists, and still receives the conflict policy.
-- [ ] 1.4 Add tests for in-memory listener autoconfiguration, persistent-storage back-off, and custom listener back-off.
+- [x] 1.1 Add a built-in replay mapping listener storage strategy enum with `IN_MEMORY` and `PERSISTENT` values.
+- [x] 1.2 Add the storage strategy property to `CausewayConfiguration` with `IN_MEMORY` as the default.
+- [x] 1.3 Update the existing in-memory listener bean factory so it is created only for `IN_MEMORY`, only when no custom listener exists, and still receives the conflict policy.
+- [x] 1.4 Add tests for in-memory listener autoconfiguration, persistent-storage back-off, and custom listener back-off.
## 2. Applib Persistent Mapping Model
-- [ ] 2.1 Add an abstract applib replay result mapping entity with recorded and actual bookmark logical type and identifier properties.
-- [ ] 2.2 Add bookmark conversion helpers on the applib entity for recorded and actual bookmarks.
-- [ ] 2.3 Add an abstract applib replay result mapping repository contract for lookup by recorded bookmark, listing all mappings, and creating mappings.
-- [ ] 2.4 Add a fallback layout XML for the replay result mapping entity.
-- [ ] 2.5 Add a command log menu action that lists persisted replay result mappings through the applib repository.
+- [x] 2.1 Add an abstract applib replay result mapping entity with recorded and actual bookmark logical type and identifier properties.
+- [x] 2.2 Add bookmark conversion helpers on the applib entity for recorded and actual bookmarks.
+- [x] 2.3 Add an abstract applib replay result mapping repository contract for lookup by recorded bookmark, listing all mappings, and creating mappings.
+- [x] 2.4 Add a fallback layout XML for the replay result mapping entity.
+- [x] 2.5 Add a command log menu action that lists persisted replay result mappings through the applib repository.
## 3. Persistent Listener Implementation
-- [ ] 3.1 Add a persistent `CommandReplayMappingListener` implementation that depends on the applib repository and conflict policy.
-- [ ] 3.2 Implement remapping from persisted recorded bookmark mappings without resolving domain objects.
-- [ ] 3.3 Implement result mapping persistence for first non-identity mappings.
-- [ ] 3.4 Implement idempotent repeated mapping handling.
-- [ ] 3.5 Implement conflict handling that throws or logs-and-continues according to the existing conflict policy.
-- [ ] 3.6 Add conditional bean creation for the persistent listener when storage strategy is `PERSISTENT`, no custom listener exists, and a repository is available.
-- [ ] 3.7 Add tests for persistent listener remapping, recording, idempotency, strict conflicts, lenient conflicts, and conditional bean creation.
+- [x] 3.1 Add a persistent `CommandReplayMappingListener` implementation that depends on the applib repository and conflict policy.
+- [x] 3.2 Implement remapping from persisted recorded bookmark mappings without resolving domain objects.
+- [x] 3.3 Implement result mapping persistence for first non-identity mappings.
+- [x] 3.4 Implement idempotent repeated mapping handling.
+- [x] 3.5 Implement conflict handling that throws or logs-and-continues according to the existing conflict policy.
+- [x] 3.6 Add conditional bean creation for the persistent listener when storage strategy is `PERSISTENT`, no custom listener exists, and a repository is available.
+- [x] 3.7 Add tests for persistent listener remapping, recording, idempotency, strict conflicts, lenient conflicts, and conditional bean creation.
## 4. JDO Persistence Implementation
-- [ ] 4.1 Add a concrete JDO replay result mapping entity following the command log persistence JDO conventions.
-- [ ] 4.2 Add a concrete JDO replay result mapping repository implementing the applib repository contract.
-- [ ] 4.3 Register the JDO entity and repository with the command log JDO persistence module.
-- [ ] 4.4 Add JDO tests for create, find by recorded bookmark, list all, idempotent repeated mapping, and conflict preservation.
+- [x] 4.1 Add a concrete JDO replay result mapping entity following the command log persistence JDO conventions.
+- [x] 4.2 Add a concrete JDO replay result mapping repository implementing the applib repository contract.
+- [x] 4.3 Register the JDO entity and repository with the command log JDO persistence module.
+- [x] 4.4 Add JDO tests for create, find by recorded bookmark, list all, idempotent repeated mapping, and conflict preservation.
## 5. JPA Persistence Implementation
-- [ ] 5.1 Add a concrete JPA replay result mapping entity following the command log persistence JPA conventions.
-- [ ] 5.2 Add a concrete JPA replay result mapping repository implementing the applib repository contract.
-- [ ] 5.3 Register the JPA entity and repository with the command log JPA persistence module.
-- [ ] 5.4 Add JPA tests for create, find by recorded bookmark, list all, idempotent repeated mapping, and conflict preservation.
+- [x] 5.1 Add a concrete JPA replay result mapping entity following the command log persistence JPA conventions.
+- [x] 5.2 Add a concrete JPA replay result mapping repository implementing the applib repository contract.
+- [x] 5.3 Register the JPA entity and repository with the command log JPA persistence module.
+- [x] 5.4 Add JPA tests for create, find by recorded bookmark, list all, idempotent repeated mapping, and conflict preservation.
## 6. Validation
-- [ ] 6.1 Run targeted command log applib tests covering both built-in listener implementations and menu action behavior.
-- [ ] 6.2 Run targeted command log JDO persistence tests for the new replay result mapping entity and repository.
-- [ ] 6.3 Run targeted command log JPA persistence tests for the new replay result mapping entity and repository.
-- [ ] 6.4 Run `openspec validate persistent-command-replay-mapping-listener --type change --strict`.
+- [x] 6.1 Run targeted command log applib tests covering both built-in listener implementations and menu action behavior.
+- [x] 6.2 Run targeted command log JDO persistence tests for the new replay result mapping entity and repository.
+- [x] 6.3 Run targeted command log JPA persistence tests for the new replay result mapping entity and repository.
+- [x] 6.4 Run `openspec validate persistent-command-replay-mapping-listener --type change --strict`.