You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2020/09/02 22:00:01 UTC

[isis] 01/17: ISIS-2222: brings in incode-platform's command module as 'command-log'.

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

danhaywood pushed a commit to branch ISIS-2222
in repository https://gitbox.apache.org/repos/asf/isis.git

commit 161adad55d194d0fb8eb96cb9196b46da1eb795d
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Wed Aug 26 07:13:34 2020 +0100

    ISIS-2222: brings in incode-platform's command module as 'command-log'.
---
 .../isis/applib/services/DomainChangeAbstract.java | 296 ++++++++
 .../{HasUsername.java => HasTransactionId.java}    |  18 +-
 .../apache/isis/applib/services/HasUsername.java   |   4 +
 .../isis/applib/services/command/Command.java      |  10 +-
 .../applib/services/command/CommandDefault.java    |   4 +
 examples/demo/domain/pom.xml                       |   6 -
 .../command-log/impl/logging-dn-enhance.properties |  29 +
 extensions/core/command-log/impl/pom.xml           |  54 ++
 .../impl/src/main/java/META-INF/persistence.xml    |  18 +
 .../command/IsisModuleExtCommandLogImpl.java       |  48 ++
 ...ndExecutionFromBackgroundCommandServiceJdo.java |  26 +
 .../command/dom/BackgroundCommandServiceJdo.java   |  95 +++
 .../dom/BackgroundCommandServiceJdoRepository.java |  45 ++
 .../isisaddons/module/command/dom/CommandJdo.java  | 830 +++++++++++++++++++++
 .../command/dom/CommandJdo.layout.fallback.xml     | 133 ++++
 .../isisaddons/module/command/dom/CommandJdo.png   | Bin 0 -> 582 bytes
 .../command/dom/CommandJdo_childCommands.java      |  32 +
 .../command/dom/CommandJdo_openResultObject.java   |  58 ++
 .../module/command/dom/CommandJdo_retry.java       |  94 +++
 .../command/dom/CommandJdo_siblingCommands.java    |  41 +
 .../module/command/dom/CommandServiceJdo.java      |  90 +++
 .../command/dom/CommandServiceJdoRepository.java   | 370 +++++++++
 .../module/command/dom/CommandServiceMenu.java     | 100 +++
 .../command/dom/HasTransactionId_command.java      |  63 ++
 .../dom/HasUsername_recentCommandsByUser.java      |  43 ++
 .../module/command/dom/Object_recentCommands.java  |  58 ++
 .../isisaddons/module/command/dom/ReplayState.java |  11 +
 .../module/command/dom/T_backgroundCommands.java   |  50 ++
 extensions/core/command-log/pom.xml                |  45 ++
 extensions/core/command-replay/pom.xml             |  39 +
 extensions/pom.xml                                 |   1 +
 .../applib/teardown/TeardownFixtureAbstract.java   |  91 ++-
 32 files changed, 2739 insertions(+), 63 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/DomainChangeAbstract.java b/api/applib/src/main/java/org/apache/isis/applib/services/DomainChangeAbstract.java
new file mode 100644
index 0000000..88a41ea
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/DomainChangeAbstract.java
@@ -0,0 +1,296 @@
+/*
+ *  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.isis.applib.services;
+
+import java.sql.Timestamp;
+import java.util.UUID;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.Optionality;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.annotation.Property;
+import org.apache.isis.applib.annotation.PropertyLayout;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.bookmark.BookmarkService;
+import org.apache.isis.applib.services.message.MessageService;
+import org.apache.isis.applib.services.metamodel.BeanSort;
+import org.apache.isis.applib.services.metamodel.MetaModelService;
+
+import lombok.Getter;
+import lombok.extern.log4j.Log4j2;
+
+
+/**
+ * An abstraction of some sort of recorded change to a domain object: commands, audit entries or published events.
+ */
+@Log4j2
+public abstract class DomainChangeAbstract
+        implements HasTransactionId, HasUsername {
+
+    public static enum ChangeType {
+        COMMAND,
+        AUDIT_ENTRY,
+        PUBLISHED_INTERACTION;
+        @Override
+        public String toString() {
+            return name().replace("_", " ");
+        }
+    }
+    public DomainChangeAbstract(final ChangeType changeType) {
+        this.type = changeType;
+    }
+
+    /**
+     * Distinguishes commands from audit entries from published events/interactions (when these are shown mixed together in a (standalone) table).
+     */
+    @Property
+    @PropertyLayout(
+            hidden = Where.ALL_EXCEPT_STANDALONE_TABLES
+    )
+    @MemberOrder(name="Identifiers", sequence = "1")
+    @Getter
+    private final ChangeType type;
+
+
+
+    /**
+     * The user that caused the change.
+     *
+     * <p>
+     * This dummy implementation is a trick so that Isis will render the property in a standalone table.  Each of the
+     * subclasses override with the &quot;real&quot; implementation.
+     */
+    @Property
+    @MemberOrder(name="Identifiers", sequence = "10")
+    public String getUser() {
+        return null;
+    }
+
+
+
+    /**
+     * The time that the change occurred.
+     *
+     * <p>
+     * This dummy implementation is a trick so that Isis will render the property in a standalone table.  Each of the
+     * subclasses override with the &quot;real&quot; implementation.
+     */
+    @Property
+    @MemberOrder(name="Identifiers", sequence = "20")
+    public Timestamp getTimestamp() {
+        return null;
+    }
+
+
+    /**
+     * The unique identifier (a GUID) of the transaction in which this change occurred.
+     *
+     * <p>
+     * This dummy implementation is a trick so that Isis will render the property in a standalone table.  Each of the
+     * subclasses override with the &quot;real&quot; implementation.
+     */
+    @Property
+    @MemberOrder(name="Identifiers",sequence = "50")
+    public UUID getTransactionId() {
+        return null;
+    }
+
+
+    /**
+     * The class of the domain object being changed.
+     *
+     * <p>
+     * This dummy implementation is a trick so that Isis will render the property in a standalone table.  Each of the
+     * subclasses override with the &quot;real&quot; implementation.
+     */
+    @Property
+    @PropertyLayout(named="Class")
+    @MemberOrder(name="Target", sequence = "10")
+    public String getTargetClass() {
+        return null;
+    }
+
+
+
+    @Programmatic
+    public Bookmark getTarget() {
+        final String str = getTargetStr();
+        return Bookmark.parse(str).orElse(null);
+    }
+
+    @Programmatic
+    public void setTarget(Bookmark target) {
+        final String targetStr = target != null ? target.toString() : null;
+        setTargetStr(targetStr);
+    }
+
+
+    /**
+     * The member interaction (ie action invocation or property edit) which caused the domain object to be changed.
+     *
+     * <p>
+     *     Populated for commands and for published events that represent action invocations or property edits.
+     * </p>
+     *
+     * <p>
+     * This dummy implementation is a trick so that Isis will render the property in a standalone table.  Each of the
+     * subclasses override with the &quot;real&quot; implementation.
+     * </p>
+     *
+     * <p>
+     *     NB: commands and published events applied only to actions, hence the name of this field.  In a future release
+     *     the name of this field may change to &quot;TargetMember&quot;.  Note that the {@link PropertyLayout} already uses
+     *     &quot;Member&quot; this as a name hint.
+     * </p>
+     *
+     */
+    @Property(optionality = Optionality.OPTIONAL)
+    @PropertyLayout(
+            named="Member",
+            hidden = Where.ALL_EXCEPT_STANDALONE_TABLES
+    )
+    @MemberOrder(name="Target", sequence = "20")
+    @Getter
+    private String targetAction;
+
+
+
+    /**
+     * The (string representation of the) {@link Bookmark} identifying the domain object that has changed.
+     *
+     * <p>
+     * This dummy implementation is a trick so that Isis will render the property in a standalone table.  Each of the
+     * subclasses override with the &quot;real&quot; implementation.
+     */
+    @Property
+    @PropertyLayout(named="Object")
+    @MemberOrder(name="Target", sequence="30")
+    public String getTargetStr() {
+        return null;
+    }
+
+    /**
+     * For {@link #setTarget(Bookmark)} to delegate to.
+     */
+    public abstract void setTargetStr(final String targetStr);
+
+
+
+    @Action(semantics = SemanticsOf.SAFE)
+    @ActionLayout(named = "Open")
+    @MemberOrder(name="TargetStr", sequence="1")
+    public Object openTargetObject() {
+        try {
+            return bookmarkService != null
+                    ? bookmarkService.lookup(getTarget())
+                    : null;
+        } catch(RuntimeException ex) {
+            if(ex.getClass().getName().contains("ObjectNotFoundException")) {
+                messageService.warnUser("Object not found - has it since been deleted?");
+                return null;
+            }
+            throw ex;
+        }
+    }
+
+    public boolean hideOpenTargetObject() {
+        return getTarget() == null;
+    }
+
+    public String disableOpenTargetObject() {
+        final Object targetObject = getTarget();
+        if (targetObject == null) {
+            return null;
+        }
+        final BeanSort sortOfObject = metaModelService.sortOf(getTarget(), MetaModelService.Mode.RELAXED);
+        return !(sortOfObject.isViewModel() || sortOfObject.isEntity())
+                ? "Can only open view models or entities"
+                : null;
+    }
+
+
+
+    /**
+     * The property of the object that was changed.
+     *
+     * <p>
+     * Populated only for audit entries.
+     *
+     * <p>
+     * This dummy implementation is a trick so that Isis will render the property in a standalone table.  Each of the
+     * subclasses override with the &quot;real&quot; implementation.
+     */
+    @Property(optionality = Optionality.OPTIONAL)
+    @PropertyLayout(hidden = Where.ALL_EXCEPT_STANDALONE_TABLES)
+    @MemberOrder(name="Target",sequence = "21")
+    public String getPropertyId() {
+        return null;
+    }
+
+
+    /**
+     * The value of the property prior to it being changed.
+     *
+     * <p>
+     * Populated only for audit entries.
+     *
+     * <p>
+     * This dummy implementation is a trick so that Isis will render the property in a standalone table.  Each of the
+     * subclasses override with the &quot;real&quot; implementation.
+     */
+    @Property(optionality = Optionality.OPTIONAL)
+    @PropertyLayout(hidden = Where.ALL_EXCEPT_STANDALONE_TABLES)
+    @MemberOrder(name="Detail",sequence = "6")
+    public String getPreValue() {
+        return null;
+    }
+
+
+    /**
+     * The value of the property after it has changed.
+     *
+     * <p>
+     * Populated only for audit entries.
+     *
+     * <p>
+     * This dummy implementation is a trick so that Isis will render the property in a standalone table.  Each of the
+     * subclasses override with the &quot;real&quot; implementation.
+     */
+    @Property(optionality = Optionality.MANDATORY)
+    @PropertyLayout(hidden = Where.ALL_EXCEPT_STANDALONE_TABLES)
+    @MemberOrder(name="Detail",sequence = "7")
+    public String getPostValue() {
+        return null;
+    }
+
+
+    @javax.inject.Inject
+    protected BookmarkService bookmarkService;
+
+    @javax.inject.Inject
+    protected MessageService messageService;
+
+    @javax.inject.Inject
+    protected MetaModelService metaModelService;
+
+}
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/HasUsername.java b/api/applib/src/main/java/org/apache/isis/applib/services/HasTransactionId.java
similarity index 75%
copy from api/applib/src/main/java/org/apache/isis/applib/services/HasUsername.java
copy to api/applib/src/main/java/org/apache/isis/applib/services/HasTransactionId.java
index 7f7a240..652a0a3 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/HasUsername.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/HasTransactionId.java
@@ -18,18 +18,22 @@
  */
 package org.apache.isis.applib.services;
 
+import java.util.UUID;
+
+
 /**
  * Mix-in interface for objects (usually created by service implementations) that are be persistable,
- * and so can be associated with a username, usually of the user that has performed some operation.
- *
- * <p>
- * Other services can then use this username as a means to contributed actions/collections to render such additional
- * information relating to the activities of the user.
+ * and so can be associated together using a transaction Id.
  */
 // tag::refguide[]
-public interface HasUsername {
+public interface HasTransactionId {
 
-    String getUsername();
+    // end::refguide[]
+    /**
+     * The unique identifier (a GUID) of the request/interaction/transaction.
+     */
+    // tag::refguide[]
+    UUID getTransactionId();
 
 }
 // end::refguide[]
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/HasUsername.java b/api/applib/src/main/java/org/apache/isis/applib/services/HasUsername.java
index 7f7a240..4860b92 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/HasUsername.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/HasUsername.java
@@ -29,6 +29,10 @@ package org.apache.isis.applib.services;
 // tag::refguide[]
 public interface HasUsername {
 
+    /**
+     * The user that created this object.
+     * @return
+     */
     String getUsername();
 
 }
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/command/Command.java b/api/applib/src/main/java/org/apache/isis/applib/services/command/Command.java
index f030cb6..aa30c57 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/command/Command.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/command/Command.java
@@ -31,6 +31,7 @@ import org.apache.isis.applib.services.bookmark.Bookmark;
 import org.apache.isis.applib.services.bookmark.BookmarkService;
 import org.apache.isis.applib.services.iactn.Interaction;
 import org.apache.isis.applib.services.wrapper.WrapperFactory;
+import org.apache.isis.applib.services.wrapper.control.AsyncControl;
 import org.apache.isis.schema.cmd.v2.CommandDto;
 
 /**
@@ -250,7 +251,7 @@ public interface Command extends HasUniqueId {
     // end::refguide[]
 
     /**
-     * For actions created through the {@link BackgroundService} and {@link BackgroundCommandService},
+     * For actions created through the {@link WrapperFactory} and {@link BackgroundCommandService},
      * captures the parent action.
      */
     // tag::refguide[]
@@ -303,7 +304,7 @@ public interface Command extends HasUniqueId {
      * {@link org.apache.isis.applib.annotation.Action#commandPersistence() persistence} attribute to
      * {@link org.apache.isis.applib.annotation.CommandPersistence#NOT_PERSISTED}, or it can be set to
      * {@link org.apache.isis.applib.annotation.CommandPersistence#IF_HINTED}, meaning it is dependent
-     * on whether {@link #setPersistHint(boolean) a hint has been set} by some other means.
+     * on whether {@link #isPersistHint() a hint has been set} by some other means.
      *
      * <p>
      * For example, a {@link BackgroundCommandService} implementation that creates persisted background commands ought
@@ -386,6 +387,11 @@ public interface Command extends HasUniqueId {
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
          */
+        void setExecuteIn(CommandExecuteIn executeIn);
+
+        /**
+         * <b>NOT API</b>: intended to be called only by the framework.
+         */
         void setResult(Bookmark resultBookmark);
 
         /**
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/command/CommandDefault.java b/api/applib/src/main/java/org/apache/isis/applib/services/command/CommandDefault.java
index 069acd4..da4e3e4 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/command/CommandDefault.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/command/CommandDefault.java
@@ -172,6 +172,10 @@ public class CommandDefault implements Command {
         public void setExecutor(Executor executor) {
             CommandDefault.this.executor = executor;
         }
+        @Override
+        public void setExecuteIn(CommandExecuteIn executeIn) {
+            CommandDefault.this.executeIn = executeIn;
+        }
     };
 
     @Override
diff --git a/examples/demo/domain/pom.xml b/examples/demo/domain/pom.xml
index b78d1c6..60787ed 100644
--- a/examples/demo/domain/pom.xml
+++ b/examples/demo/domain/pom.xml
@@ -45,7 +45,6 @@
 				</includes>
 			</resource>
 		</resources>
-
 	</build>
 
 	<dependencies>
@@ -88,11 +87,6 @@
 		
 		<!-- OTHER DEPENDENCIES -->
 
-<!-- 		<dependency> -->
-<!-- 			<groupId>org.hsqldb</groupId> -->
-<!-- 			<artifactId>hsqldb</artifactId> -->
-<!-- 		</dependency> -->
-
 		<dependency>
 			<groupId>com.h2database</groupId>
 			<artifactId>h2</artifactId>
diff --git a/extensions/core/command-log/impl/logging-dn-enhance.properties b/extensions/core/command-log/impl/logging-dn-enhance.properties
new file mode 100644
index 0000000..bec30be
--- /dev/null
+++ b/extensions/core/command-log/impl/logging-dn-enhance.properties
@@ -0,0 +1,29 @@
+log4j.appender.A1=org.apache.log4j.FileAppender
+log4j.appender.A1.File=datanucleus.log
+log4j.appender.A1.layout=org.apache.log4j.PatternLayout
+log4j.appender.A1.layout.ConversionPattern=%d{HH:mm:ss,SSS} (%t) %-5p [%c] - %m%n
+
+
+# overriding all those below... 
+log4j.category.DataNucleus=ERROR
+
+log4j.category.DataNucleus.Persistence=INFO, A1
+log4j.category.DataNucleus.Transaction=INFO, A1
+log4j.category.DataNucleus.Connection=INFO, A1
+log4j.category.DataNucleus.Query=INFO, A1
+log4j.category.DataNucleus.Cache=INFO, A1
+log4j.category.DataNucleus.MetaData=INFO, A1
+log4j.category.DataNucleus.Datastore=INFO, A1
+log4j.category.DataNucleus.Datastore.Schema=INFO, A1
+log4j.category.DataNucleus.Datastore.Persist=INFO, A1
+log4j.category.DataNucleus.Datastore.Retrieve=INFO, A1
+#Log of all 'native' statements sent to the datastore
+log4j.category.DataNucleus.Datastore.Native=INFO, A1 
+log4j.category.DataNucleus.General=INFO, A1
+#All messages relating to object lifecycle changes
+log4j.category.DataNucleus.Lifecycle=INFO, A1
+log4j.category.DataNucleus.ValueGeneration=INFO, A1
+log4j.category.DataNucleus.Enhancer=INFO, A1
+log4j.category.DataNucleus.SchemaTool=INFO, A1
+log4j.category.DataNucleus.JDO=INFO, A1
+ 
\ No newline at end of file
diff --git a/extensions/core/command-log/impl/pom.xml b/extensions/core/command-log/impl/pom.xml
new file mode 100644
index 0000000..2647f5a
--- /dev/null
+++ b/extensions/core/command-log/impl/pom.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.isis.extensions</groupId>
+        <artifactId>isis-extensions-command-log</artifactId>
+        <version>2.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>isis-extensions-command-log-impl</artifactId>
+    <name>Apache Isis Ext - Command Log Implementation</name>
+
+    <properties>
+        <jar-plugin.automaticModuleName>org.apache.isis.extensions.commandlog.impl</jar-plugin.automaticModuleName>
+        <git-plugin.propertiesDir>org/apache/isis/extensions/commandlog/impl</git-plugin.propertiesDir>
+    </properties>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.apache.isis.core</groupId>
+            <artifactId>isis-applib</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.isis.core</groupId>
+            <artifactId>isis-core-config</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.isis.persistence</groupId>
+            <artifactId>isis-persistence-jdo-applib</artifactId>
+        </dependency>
+
+        <dependency>
+	    	<groupId>org.apache.isis.testing</groupId>
+        	<artifactId>isis-testing-fixtures-applib</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>
diff --git a/extensions/core/command-log/impl/src/main/java/META-INF/persistence.xml b/extensions/core/command-log/impl/src/main/java/META-INF/persistence.xml
new file mode 100644
index 0000000..889caf7
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/META-INF/persistence.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!-- 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. -->
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
+
+    <persistence-unit name="org-isisaddons-module-command-dom">
+    </persistence-unit>
+</persistence>
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/IsisModuleExtCommandLogImpl.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/IsisModuleExtCommandLogImpl.java
new file mode 100644
index 0000000..6f4fb33
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/IsisModuleExtCommandLogImpl.java
@@ -0,0 +1,48 @@
+package org.isisaddons.module.command;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import org.apache.isis.testing.fixtures.applib.fixturescripts.FixtureScript;
+import org.apache.isis.testing.fixtures.applib.modules.ModuleWithFixtures;
+import org.apache.isis.testing.fixtures.applib.teardown.TeardownFixtureAbstract;
+
+import org.isisaddons.module.command.dom.BackgroundCommandExecutionFromBackgroundCommandServiceJdo;
+import org.isisaddons.module.command.dom.CommandJdo;
+
+@Configuration
+@Import({
+        // @Service's
+        BackgroundCommandExecutionFromBackgroundCommandServiceJdo.class
+})
+public class IsisModuleExtCommandLogImpl implements ModuleWithFixtures {
+
+    public abstract static class ActionDomainEvent<S>
+            extends org.apache.isis.applib.events.domain.ActionDomainEvent<S> { }
+
+    public abstract static class CollectionDomainEvent<S,T>
+            extends org.apache.isis.applib.events.domain.CollectionDomainEvent<S,T> { }
+
+    public abstract static class PropertyDomainEvent<S,T>
+            extends org.apache.isis.applib.events.domain.PropertyDomainEvent<S,T> { }
+
+    @Override
+    public FixtureScript getTeardownFixture() {
+        // can't delete from CommandJdo, is searched for during teardown (IsisSession#close)
+        return FixtureScript.NOOP;
+    }
+
+    /**
+     * For tests that need to delete the command table first.
+     * Should be run in the @Before of the test.
+     */
+    public FixtureScript getTeardownFixtureWillDelete() {
+        return new TeardownFixtureAbstract() {
+            @Override
+            protected void execute(final ExecutionContext executionContext) {
+                deleteFrom(CommandJdo.class);
+            }
+        };
+    }
+
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/BackgroundCommandExecutionFromBackgroundCommandServiceJdo.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/BackgroundCommandExecutionFromBackgroundCommandServiceJdo.java
new file mode 100644
index 0000000..b66477e
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/BackgroundCommandExecutionFromBackgroundCommandServiceJdo.java
@@ -0,0 +1,26 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.List;
+
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.services.command.Command;
+import org.apache.isis.applib.services.command.CommandExecutorService;
+import org.apache.isis.core.runtimeservices.background.BackgroundCommandExecution;
+
+@Service
+public class BackgroundCommandExecutionFromBackgroundCommandServiceJdo
+        extends BackgroundCommandExecution {
+
+    public BackgroundCommandExecutionFromBackgroundCommandServiceJdo() {
+        super(CommandExecutorService.SudoPolicy.NO_SWITCH);
+    }
+
+    @Override
+    protected List<? extends Command> findBackgroundCommandsToExecute() {
+        return backgroundCommandRepository.findBackgroundCommandsNotYetStarted();
+    }
+
+    @javax.inject.Inject
+    BackgroundCommandServiceJdoRepository backgroundCommandRepository;
+}
\ No newline at end of file
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/BackgroundCommandServiceJdo.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/BackgroundCommandServiceJdo.java
new file mode 100644
index 0000000..121e770
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/BackgroundCommandServiceJdo.java
@@ -0,0 +1,95 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.annotation.CommandExecuteIn;
+import org.apache.isis.applib.services.background.BackgroundCommandService;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.clock.ClockService;
+import org.apache.isis.applib.services.command.Command;
+import org.apache.isis.applib.services.factory.FactoryService;
+import org.apache.isis.applib.util.schema.CommandDtoUtils;
+import org.apache.isis.schema.cmd.v2.CommandDto;
+import org.apache.isis.schema.common.v2.OidDto;
+
+/**
+ * Persists a memento-ized action such that it can be executed asynchronously,
+ * for example through a Quartz scheduler (using
+ * {@link BackgroundCommandExecutionFromBackgroundCommandServiceJdo}).
+ */
+@Service()
+public class BackgroundCommandServiceJdo implements BackgroundCommandService {
+
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LoggerFactory.getLogger(BackgroundCommandServiceJdo.class);
+
+    @Override
+    public void schedule(
+            final CommandDto dto,
+            final Command parentCommand,
+            final String targetClassName,
+            final String targetActionName,
+            final String targetArgs) {
+
+        final CommandJdo backgroundCommand =
+                newBackgroundCommand(parentCommand, targetClassName, targetActionName, targetArgs);
+
+        final OidDto firstTarget = dto.getTargets().getOid().get(0);
+        backgroundCommand.setTargetStr(Bookmark.from(firstTarget).toString());
+        backgroundCommand.internal().setMemento(CommandDtoUtils.toXml(dto));
+        backgroundCommand.setMemberIdentifier(dto.getMember().getMemberIdentifier());
+
+        commandServiceJdoRepository.persist(backgroundCommand);
+    }
+
+    private CommandJdo newBackgroundCommand(
+            final Command parentCommand,
+            final String targetClassName,
+            final String targetActionName,
+            final String targetArgs) {
+
+        final CommandJdo backgroundCommand = factoryService.instantiate(CommandJdo.class);
+
+        backgroundCommand.internal().setParent(parentCommand);
+
+        // workaround for ISIS-1472; parentCommand not properly set up if invoked via RO viewer
+        if(parentCommand.getMemberIdentifier() == null) {
+            backgroundCommand.internal().setParent(null);
+        }
+
+        final UUID transactionId = UUID.randomUUID();
+        final String user = parentCommand.getUser();
+
+        backgroundCommand.setTransactionId(transactionId);
+
+        backgroundCommand.internal().setUser(user);
+        backgroundCommand.internal().setTimestamp(clockService.nowAsJavaSqlTimestamp());
+        backgroundCommand.internal().setExecuteIn(CommandExecuteIn.BACKGROUND);
+
+        backgroundCommand.setTargetClass(targetClassName);
+        backgroundCommand.setTargetAction(targetActionName);
+
+        backgroundCommand.internal().setArguments(targetArgs);
+        backgroundCommand.internal().setPersistHint(true);
+
+        return backgroundCommand;
+    }
+
+
+    @Inject
+    CommandServiceJdoRepository commandServiceJdoRepository;
+
+    @Inject
+    FactoryService factoryService;
+
+    @Inject
+    ClockService clockService;
+
+}
+
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/BackgroundCommandServiceJdoRepository.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/BackgroundCommandServiceJdoRepository.java
new file mode 100644
index 0000000..2865e2d
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/BackgroundCommandServiceJdoRepository.java
@@ -0,0 +1,45 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.Programmatic;
+
+/**
+ * Provides supporting functionality for querying
+ * {@link CommandJdo command} entities that have been persisted
+ * to execute in the background.
+ *
+ * <p>
+ * This supporting service with no UI and no side-effects, and is there are no other implementations of the service,
+ * thus has been annotated with {@link org.apache.isis.applib.annotation.DomainService}.  This means that there is no
+ * need to explicitly register it as a service (eg in <tt>isis.properties</tt>).
+ */
+@DomainService(
+        nature = NatureOfService.DOMAIN
+)
+public class BackgroundCommandServiceJdoRepository {
+
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LoggerFactory.getLogger(BackgroundCommandServiceJdoRepository.class);
+
+    @Programmatic
+    public List<CommandJdo> findByParent(CommandJdo parent) {
+        return commandServiceRepository.findBackgroundCommandsByParent(parent);
+    }
+
+    @Programmatic
+    public List<CommandJdo> findBackgroundCommandsNotYetStarted() {
+        return commandServiceRepository.findBackgroundCommandsNotYetStarted();
+    }
+
+    @Inject
+    CommandServiceJdoRepository commandServiceRepository;
+
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo.java
new file mode 100644
index 0000000..ffb37de
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo.java
@@ -0,0 +1,830 @@
+package org.isisaddons.module.command.dom;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.jdo.annotations.IdentityType;
+import javax.jdo.annotations.NotPersistent;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.CommandExecuteIn;
+import org.apache.isis.applib.annotation.CommandPersistence;
+import org.apache.isis.applib.annotation.DomainObject;
+import org.apache.isis.applib.annotation.DomainObjectLayout;
+import org.apache.isis.applib.annotation.Editing;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.Optionality;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.annotation.Property;
+import org.apache.isis.applib.annotation.PropertyLayout;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.applib.services.DomainChangeAbstract;
+import org.apache.isis.applib.services.HasUsername;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.bookmark.BookmarkService;
+import org.apache.isis.applib.services.command.Command;
+import org.apache.isis.applib.services.command.CommandDefault;
+import org.apache.isis.applib.services.command.CommandWithDto;
+import org.apache.isis.applib.services.jaxb.JaxbService;
+import org.apache.isis.applib.services.message.MessageService;
+import org.apache.isis.applib.types.MemberIdentifierType;
+import org.apache.isis.applib.types.TargetActionType;
+import org.apache.isis.applib.types.TargetClassType;
+import org.apache.isis.applib.util.ObjectContracts;
+import org.apache.isis.applib.util.TitleBuffer;
+import org.apache.isis.schema.cmd.v2.CommandDto;
+
+import org.isisaddons.module.command.IsisModuleExtCommandLogImpl;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.log4j.Log4j2;
+
+@javax.jdo.annotations.PersistenceCapable(
+        identityType=IdentityType.APPLICATION,
+        schema = "isiscoreextcommandlog",
+        table = "Command")
+@javax.jdo.annotations.Queries( {
+    @javax.jdo.annotations.Query(
+            name="findByTransactionId",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE transactionId == :transactionId "),
+    @javax.jdo.annotations.Query(
+            name="findBackgroundCommandsByParent",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE parent == :parent "
+                    + "&& executeIn == 'BACKGROUND'"),
+    @javax.jdo.annotations.Query(
+            name="findCurrent",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE completedAt == null "
+                    + "ORDER BY this.timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findCompleted",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE completedAt != null "
+                    + "&& executeIn == 'FOREGROUND' "
+                    + "ORDER BY this.timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findRecentBackgroundByTarget",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE targetStr == :targetStr "
+                    + "&& executeIn == 'BACKGROUND' "
+                    + "ORDER BY this.timestamp DESC, transactionId DESC "
+                    + "RANGE 0,30"),
+    @javax.jdo.annotations.Query(
+            name="findByTargetAndTimestampBetween",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE targetStr == :targetStr " 
+                    + "&& timestamp >= :from " 
+                    + "&& timestamp <= :to "
+                    + "ORDER BY this.timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTargetAndTimestampAfter",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE targetStr == :targetStr " 
+                    + "&& timestamp >= :from "
+                    + "ORDER BY this.timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTargetAndTimestampBefore",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE targetStr == :targetStr " 
+                    + "&& timestamp <= :to "
+                    + "ORDER BY this.timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTarget",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE targetStr == :targetStr " 
+                    + "ORDER BY this.timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTimestampBetween",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE timestamp >= :from " 
+                    + "&&    timestamp <= :to "
+                    + "ORDER BY this.timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTimestampAfter",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE timestamp >= :from "
+                    + "ORDER BY this.timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findByTimestampBefore",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE timestamp <= :to "
+                    + "ORDER BY this.timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="find",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "ORDER BY this.timestamp DESC"),
+    @javax.jdo.annotations.Query(
+            name="findRecentByUser",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE user == :user "
+                    + "ORDER BY this.timestamp DESC "
+                    + "RANGE 0,30"),
+    @javax.jdo.annotations.Query(
+            name="findRecentByTarget",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE targetStr == :targetStr "
+                    + "ORDER BY this.timestamp DESC, transactionId DESC "
+                    + "RANGE 0,30"),
+    @javax.jdo.annotations.Query(
+            name="findForegroundFirst",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE executeIn == 'FOREGROUND' "
+                    + "   && timestamp   != null "
+                    + "   && startedAt   != null "
+                    + "   && completedAt != null "
+                    + "ORDER BY this.timestamp ASC "
+                    + "RANGE 0,2"),
+        // this should be RANGE 0,1 but results in DataNucleus submitting "FETCH NEXT ROW ONLY"
+        // which SQL Server doesn't understand.  However, as workaround, SQL Server *does* understand FETCH NEXT 2 ROWS ONLY
+    @javax.jdo.annotations.Query(
+            name="findForegroundSince",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE executeIn == 'FOREGROUND' "
+                    + "   && timestamp > :timestamp "
+                    + "   && startedAt != null "
+                    + "   && completedAt != null "
+                    + "ORDER BY this.timestamp ASC"),
+    @javax.jdo.annotations.Query(
+            name="findReplayableHwm",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE executeIn == 'REPLAYABLE' "
+                    + "ORDER BY this.timestamp DESC "
+                    + "RANGE 0,2"),
+        // this should be RANGE 0,1 but results in DataNucleus submitting "FETCH NEXT ROW ONLY"
+        // which SQL Server doesn't understand.  However, as workaround, SQL Server *does* understand FETCH NEXT 2 ROWS ONLY
+    @javax.jdo.annotations.Query(
+            name="findForegroundHwm",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE executeIn == 'FOREGROUND' "
+                    + "   && startedAt   != null "
+                    + "   && completedAt != null "
+                    + "ORDER BY this.timestamp DESC "
+                    + "RANGE 0,2"),
+        // this should be RANGE 0,1 but results in DataNucleus submitting "FETCH NEXT ROW ONLY"
+        // which SQL Server doesn't understand.  However, as workaround, SQL Server *does* understand FETCH NEXT 2 ROWS ONLY
+    @javax.jdo.annotations.Query(
+            name="findBackgroundCommandsNotYetStarted",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE executeIn == 'BACKGROUND' "
+                    + "   && startedAt == null "
+                    + "ORDER BY this.timestamp ASC "),
+        @javax.jdo.annotations.Query(
+                name="findReplayableInErrorMostRecent",
+                value="SELECT "
+                        + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                        + "WHERE executeIn   == 'REPLAYABLE' "
+                        + "  && (replayState != 'PENDING' || "
+                        + "      replayState != 'OK'      || "
+                        + "      replayState != 'EXCLUDED'   ) "
+                        + "ORDER BY this.timestamp DESC "
+                        + "RANGE 0,2"),
+    @javax.jdo.annotations.Query(
+            name="findReplayableMostRecentStarted",
+            value="SELECT "
+                    + "FROM org.isisaddons.module.command.dom.CommandJdo "
+                    + "WHERE executeIn == 'REPLAYABLE' "
+                    + "   && startedAt != null "
+                    + "ORDER BY this.timestamp DESC "
+                    + "RANGE 0,20"),
+})
+@javax.jdo.annotations.Indices({
+        @javax.jdo.annotations.Index(name = "CommandJdo_timestamp_e_s_IDX", members = {"timestamp", "executeIn", "startedAt"}),
+        @javax.jdo.annotations.Index(name = "CommandJdo_startedAt_e_c_IDX", members = {"startedAt", "executeIn", "completedAt"}),
+})
+@DomainObject(
+        objectType = "isisextcorecommandlog.Command",
+        editing = Editing.DISABLED
+)
+@DomainObjectLayout(named = "Command")
+@Log4j2
+public class CommandJdo extends DomainChangeAbstract
+        implements Command, CommandWithDto, HasUsername, Comparable<CommandJdo> {
+
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LoggerFactory.getLogger(CommandJdo.class);
+
+
+    public static abstract class PropertyDomainEvent<T> extends IsisModuleExtCommandLogImpl.PropertyDomainEvent<CommandJdo, T> { }
+    public static abstract class CollectionDomainEvent<T> extends IsisModuleExtCommandLogImpl.CollectionDomainEvent<CommandJdo, T> { }
+    public static abstract class ActionDomainEvent extends IsisModuleExtCommandLogImpl.ActionDomainEvent<CommandJdo> { }
+
+    public CommandJdo() {
+        super(DomainChangeAbstract.ChangeType.COMMAND);
+        this.uniqueId = UUID.randomUUID();
+    }
+
+
+    public String title() {
+        // nb: not thread-safe
+        // formats defined in https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
+        final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+
+        final TitleBuffer buf = new TitleBuffer();
+        buf.append(format.format(getTimestamp()));
+        buf.append(" ").append(getMemberIdentifier());
+        return buf.toString();
+    }
+
+
+    public static class UniqueIdDomainEvent extends PropertyDomainEvent<UUID> { }
+    @javax.jdo.annotations.Persistent
+    @javax.jdo.annotations.Column(allowsNull="false")
+    private UUID uniqueId;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = UniqueIdDomainEvent.class)
+    @Override
+    public UUID getUniqueId() {
+        return uniqueId;
+    }
+
+
+    public static class UserDomainEvent extends PropertyDomainEvent<String> { }
+    @javax.jdo.annotations.Column(allowsNull="false", length = 50)
+    private String username;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = UserDomainEvent.class)
+    @Override
+    public String getUsername() {
+        return username;
+    }
+
+
+    public static class TimestampDomainEvent extends PropertyDomainEvent<Timestamp> { }
+    @javax.jdo.annotations.Persistent
+    @javax.jdo.annotations.Column(allowsNull="false")
+    private Timestamp timestamp;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = TimestampDomainEvent.class)
+    @Override
+    public Timestamp getTimestamp() {
+        return timestamp;
+    }
+
+
+    public static class ExecutorDomainEvent extends PropertyDomainEvent<Executor> { }
+    @javax.jdo.annotations.NotPersistent
+    private Executor executor;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = ExecutorDomainEvent.class)
+    @Override
+    public Executor getExecutor() {
+        return executor;
+    }
+
+
+    public static class ExecuteInDomainEvent extends PropertyDomainEvent<CommandExecuteIn> { }
+    @javax.jdo.annotations.Column(allowsNull="false", length = CommandExecuteIn.Type.Meta.MAX_LEN)
+    private CommandExecuteIn executeIn;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = ExecuteInDomainEvent.class)
+    @Override
+    public CommandExecuteIn getExecuteIn() {
+        return executeIn;
+    }
+
+
+    public static class ReplayStateDomainEvent extends PropertyDomainEvent<ReplayState> { }
+    /**
+     * For a replayed command, what the outcome was.
+     *
+     * NOT API.
+     */
+    @javax.jdo.annotations.Column(allowsNull="true", length=10)
+    @Property(domainEvent = ReplayStateDomainEvent.class)
+    @Getter @Setter
+    private ReplayState replayState;
+
+
+    public static class ReplayStateFailureReasonDomainEvent extends PropertyDomainEvent<ReplayState> { }
+    /**
+     * For a {@link ReplayState#FAILED failed} replayed command, what the reason was for the failure.
+     *
+     * <b>NOT API</b>.
+     */
+    @javax.jdo.annotations.Column(allowsNull="true", length=255)
+    @Property(domainEvent = ReplayStateFailureReasonDomainEvent.class)
+    @PropertyLayout(hidden = Where.ALL_TABLES, multiLine = 5)
+    @Getter @Setter
+    private String replayStateFailureReason;
+    public boolean hideReplayStateFailureReason() {
+        return getReplayState() == null || !getReplayState().isFailed();
+    }
+
+
+    public static class ParentDomainEvent extends PropertyDomainEvent<Command> { }
+    @javax.jdo.annotations.Persistent
+    @javax.jdo.annotations.Column(name="parentTransactionId", allowsNull="true")
+    private Command parent;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = ParentDomainEvent.class)
+    @PropertyLayout(hidden = Where.ALL_TABLES)
+    @Override
+    public Command getParent() {
+        return parent;
+    }
+
+
+    public static class TransactionIdDomainEvent extends PropertyDomainEvent<UUID> { }
+    @javax.jdo.annotations.PrimaryKey
+    @javax.jdo.annotations.Column(allowsNull="false", length = 36)
+    @Setter
+    private UUID transactionId;
+    /**
+     * {@inheritDoc}
+     *
+     * <p>
+     * Implementation notes: copied over from the Isis transaction when the command is persisted.
+     */
+    @Property(domainEvent = TransactionIdDomainEvent.class)
+    @Override
+    public UUID getTransactionId() {
+        return transactionId;
+    }
+
+
+    public static class TargetClassDomainEvent extends PropertyDomainEvent<String> { }
+    @javax.jdo.annotations.Column(allowsNull="false", length = TargetClassType.Meta.MAX_LEN)
+    private String targetClass;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = TargetClassDomainEvent.class)
+    @PropertyLayout(named="Class")
+    @Override
+    public String getTargetClass() {
+        return targetClass;
+    }
+    public void setTargetClass(final String targetClass) {
+        this.targetClass = abbreviated(targetClass, TargetClassType.Meta.MAX_LEN);
+    }
+
+
+    public static class TargetActionDomainEvent extends PropertyDomainEvent<String> { }
+    @javax.jdo.annotations.Column(allowsNull="false", length = TargetActionType.Meta.MAX_LEN)
+    private String targetAction;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = TargetActionDomainEvent.class, optionality = Optionality.MANDATORY)
+    @PropertyLayout(hidden = Where.NOWHERE, named = "Action")
+    @Override
+    public String getTargetAction() {
+        return targetAction;
+    }
+    public void setTargetAction(final String targetAction) {
+        this.targetAction = abbreviated(targetAction, TargetActionType.Meta.MAX_LEN);
+    }
+
+
+    public static class TargetStrDomainEvent extends PropertyDomainEvent<String> { }
+    @javax.jdo.annotations.Column(allowsNull="true", length = 2000, name="target")
+    private String targetStr;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = TargetStrDomainEvent.class)
+    @PropertyLayout(hidden = Where.REFERENCES_PARENT, named = "Object")
+    @Override
+    public String getTargetStr() {
+        return targetStr;
+    }
+    @Override
+    public void setTargetStr(String targetStr) {
+        this.targetStr = targetStr;
+    }
+
+    public static class ArgumentsDomainEvent extends PropertyDomainEvent<String> { }
+    @javax.jdo.annotations.Column(allowsNull="true", jdbcType="CLOB", sqlType="LONGVARCHAR")
+    private String arguments;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = ArgumentsDomainEvent.class)
+    @PropertyLayout(multiLine = 7, hidden = Where.ALL_TABLES)
+    @Override
+    public String getArguments() {
+        return arguments;
+    }
+
+
+    public static class MemberIdentifierDomainEvent extends PropertyDomainEvent<String> { }
+    @javax.jdo.annotations.Column(allowsNull="false", length = MemberIdentifierType.Meta.MAX_LEN)
+    private String memberIdentifier;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = MemberIdentifierDomainEvent.class)
+    @PropertyLayout(hidden = Where.ALL_TABLES)
+    @Override
+    public String getMemberIdentifier() {
+        return memberIdentifier;
+    }
+    public void setMemberIdentifier(final String memberIdentifier) {
+        this.memberIdentifier = abbreviated(memberIdentifier, MemberIdentifierType.Meta.MAX_LEN);
+    }
+
+
+    public static class MementoDomainEvent extends PropertyDomainEvent<String> { }
+    @javax.jdo.annotations.Column(allowsNull="true", jdbcType="CLOB")
+    private String memento;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = MementoDomainEvent.class)
+    @PropertyLayout(multiLine = 9, hidden = Where.ALL_TABLES)
+    @Override
+    public String getMemento() {
+        return memento;
+    }
+
+
+    // locally cached
+    private transient CommandDto commandDto;
+
+    @Override
+    public CommandDto asDto() {
+        if(commandDto == null) {
+            this.commandDto = buildCommandDto();
+        }
+        return this.commandDto;
+    }
+
+    private CommandDto buildCommandDto() {
+        if(getMemento() == null) {
+            return null;
+        }
+
+        return jaxbService.fromXml(CommandDto.class, getMemento());
+    }
+
+
+    public static class StartedAtDomainEvent extends PropertyDomainEvent<Timestamp> { }
+    @javax.jdo.annotations.Persistent
+    @javax.jdo.annotations.Column(allowsNull="true")
+    private Timestamp startedAt;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = StartedAtDomainEvent.class)
+    @Override
+    public Timestamp getStartedAt() {
+        return startedAt;
+    }
+
+
+    public static class CompletedAtDomainEvent extends PropertyDomainEvent<Timestamp> { }
+    @javax.jdo.annotations.Persistent
+    @javax.jdo.annotations.Column(allowsNull="true")
+    private Timestamp completedAt;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = CompletedAtDomainEvent.class)
+    @Override
+    public Timestamp getCompletedAt() {
+        return completedAt;
+    }
+
+
+    public static class DurationDomainEvent extends PropertyDomainEvent<BigDecimal> { }
+    /**
+     * The number of seconds (to 3 decimal places) that this interaction lasted.
+     * 
+     * <p>
+     * Populated only if it has {@link #getCompletedAt() completed}.
+     */
+    @javax.validation.constraints.Digits(integer=5, fraction=3)
+    @Property(domainEvent = DurationDomainEvent.class)
+    public BigDecimal getDuration() {
+        return durationBetween(getStartedAt(), getCompletedAt());
+    }
+
+
+    public static class IsCompleteDomainEvent extends PropertyDomainEvent<Boolean> { }
+    @javax.jdo.annotations.NotPersistent
+    @Property(domainEvent = IsCompleteDomainEvent.class)
+    @PropertyLayout(hidden = Where.OBJECT_FORMS)
+    public boolean isComplete() {
+        return getCompletedAt() != null;
+    }
+
+
+    public static class ResultSummaryDomainEvent extends PropertyDomainEvent<String> { }
+    @javax.jdo.annotations.NotPersistent
+    @Property(domainEvent = ResultSummaryDomainEvent.class)
+    @PropertyLayout(hidden = Where.OBJECT_FORMS, named = "Result")
+    public String getResultSummary() {
+        if(getCompletedAt() == null) {
+            return "";
+        }
+        if(getException() != null) {
+            return "EXCEPTION";
+        } 
+        if(getResultStr() != null) {
+            return "OK";
+        } else {
+            return "OK (VOID)";
+        }
+    }
+
+
+    @Programmatic
+    @Override
+    public Bookmark getResult() {
+        return bookmarkFor(getResultStr());
+    }
+    @Programmatic
+    public void setResult(final Bookmark result) {
+        setResultStr(asString(result));
+    }
+
+
+    public static class ResultStrDomainEvent extends PropertyDomainEvent<String> { }
+    @javax.jdo.annotations.Column(allowsNull="true", length = 2000, name="result")
+    @Property(domainEvent = ResultStrDomainEvent.class)
+    @PropertyLayout(hidden = Where.ALL_TABLES, named = "Result Bookmark")
+    @Getter @Setter
+    private String resultStr;
+
+
+    public static class ExceptionDomainEvent extends PropertyDomainEvent<String> { }
+    /**
+     * Stack trace of any exception that might have occurred if this interaction/transaction aborted.
+     * 
+     * <p>
+     * Not part of the applib API, because the default implementation is not persistent
+     * and so there's no object that can be accessed to be annotated.
+     */
+    @javax.jdo.annotations.Column(allowsNull="true", jdbcType="CLOB")
+    private String exception;
+    /**
+     * {@inheritDoc}
+     */
+    @Property(domainEvent = ExceptionDomainEvent.class)
+    @PropertyLayout(hidden = Where.ALL_TABLES, multiLine = 5, named = "Exception (if any)")
+    @Override
+    public String getException() {
+        return exception;
+    }
+
+
+    public static class IsCausedExceptionDomainEvent extends PropertyDomainEvent<Boolean> { }
+    @javax.jdo.annotations.NotPersistent
+    @Property(domainEvent = IsCausedExceptionDomainEvent.class)
+    @PropertyLayout(hidden = Where.OBJECT_FORMS)
+    public boolean isCausedException() {
+        return getException() != null;
+    }
+
+
+
+    private final LinkedList<org.apache.isis.applib.events.domain.ActionDomainEvent<?>> actionDomainEvents = new LinkedList<>();
+    @Programmatic
+    public org.apache.isis.applib.events.domain.ActionDomainEvent<?> peekActionDomainEvent() {
+        return actionDomainEvents.isEmpty()? null: actionDomainEvents.getLast();
+    }
+    @Programmatic
+    public void pushActionDomainEvent(final org.apache.isis.applib.events.domain.ActionDomainEvent<?> event) {
+        if(peekActionDomainEvent() == event) {
+            return;
+        }
+        this.actionDomainEvents.add(event);
+    }
+    @Programmatic
+    public org.apache.isis.applib.events.domain.ActionDomainEvent<?> popActionDomainEvent() {
+        return !actionDomainEvents.isEmpty()
+                ? actionDomainEvents.removeLast() : null;
+    }
+    @Programmatic
+    public List<org.apache.isis.applib.events.domain.ActionDomainEvent<?>> flushActionDomainEvents() {
+        final List<org.apache.isis.applib.events.domain.ActionDomainEvent<?>> events =
+                Collections.unmodifiableList(new ArrayList<>(actionDomainEvents));
+        actionDomainEvents.clear();
+        return events;
+    }
+
+
+    private final Map<String, AtomicInteger> sequenceByName = new HashMap<>();
+    @Programmatic
+    public int next(final String sequenceAbbr) {
+        AtomicInteger next = sequenceByName.get(sequenceAbbr);
+        if(next == null) {
+            next = new AtomicInteger(0);
+            sequenceByName.put(sequenceAbbr, next);
+        } else {
+            next.incrementAndGet();
+        }
+        return next.get();
+    }
+
+
+
+    @javax.jdo.annotations.NotPersistent
+    @Programmatic
+    private CommandPersistence persistence;
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public CommandPersistence getPersistence() {
+        return persistence;
+    }
+
+
+    @javax.jdo.annotations.NotPersistent
+    @Programmatic
+    private boolean persistHint;
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isPersistHint() {
+        return persistHint;
+    }
+
+
+    boolean shouldPersist() {
+        switch (getPersistence()) {
+            case PERSISTED:
+                return true;
+            case IF_HINTED:
+                return isPersistHint();
+            default:
+                return false;
+        }
+    }
+
+
+    private final Command.Internal INTERNAL = new Command.Internal() {
+        @Override
+        public void setMemberIdentifier(String actionIdentifier) {
+            CommandJdo.this.memberIdentifier = actionIdentifier;
+        }
+        @Override
+        public void setTargetClass(String targetClass) {
+            CommandJdo.this.targetClass = targetClass;
+        }
+        @Override
+        public void setTargetAction(String targetAction) {
+            CommandJdo.this.targetAction = targetAction;
+        }
+        @Override
+        public void setArguments(String arguments) {
+            CommandJdo.this.arguments = arguments;
+        }
+        @Override
+        public void setMemento(String memento) {
+            CommandJdo.this.memento = memento;
+        }
+        @Override
+        public void setTarget(Bookmark target) {
+            CommandJdo.this.setTarget(target);
+        }
+        @Override
+        public void setTimestamp(Timestamp timestamp) {
+            CommandJdo.this.timestamp = timestamp;
+        }
+        @Override
+        public void setStartedAt(Timestamp startedAt) {
+            CommandJdo.this.startedAt = startedAt;
+        }
+        @Override
+        public void setCompletedAt(final Timestamp completed) {
+            CommandJdo.this.completedAt = completed;
+        }
+        @Override
+        public void setUser(String user) {
+            CommandJdo.this.username = user;
+        }
+        @Override
+        public void setParent(Command parent) {
+            CommandJdo.this.parent = parent;
+        }
+        @Override
+        public void setResult(final Bookmark result) {
+            CommandJdo.this.setResult(result);
+        }
+        @Override
+        public void setException(final String exceptionStackTrace) {
+            CommandJdo.this.exception = exceptionStackTrace;
+        }
+        @Override
+        public void setPersistence(CommandPersistence persistence) {
+            CommandJdo.this.persistence = persistence;
+        }
+        @Override
+        public void setPersistHint(boolean persistHint) {
+            CommandJdo.this.persistHint = persistHint;
+        }
+        @Override
+        public void setExecutor(Executor executor) {
+            CommandJdo.this.executor = executor;
+        }
+        @Override
+        public void setExecuteIn(CommandExecuteIn executeIn) {
+            CommandJdo.this.executeIn = executeIn;
+        }
+    };
+
+    @Override
+    public Command.Internal internal() {
+        return INTERNAL;
+    }
+
+
+    @Override
+    public String toString() {
+        return "CommandJdo{" +
+                "targetStr='" + targetStr + '\'' +
+                ", memberIdentifier='" + memberIdentifier + '\'' +
+                ", username='" + username + '\'' +
+                ", startedAt=" + startedAt +
+                ", completedAt=" + completedAt +
+                ", transactionId=" + transactionId +
+                '}';
+    }
+
+    @Override
+    public int compareTo(final CommandJdo other) {
+        return this.getTimestamp().compareTo(other.getTimestamp());
+    }
+
+
+    private static String abbreviated(String str, int maxLength) {
+        return str != null
+                ? (str.length() < maxLength ? str : str.substring(0, maxLength - 3) + "...")
+                : null;
+    }
+
+    private static Bookmark bookmarkFor(String str) {
+        return Bookmark.parse(str).orElse(null);
+    }
+
+    private static String asString(Bookmark bookmark) {
+        return bookmark != null ? bookmark.toString() : null;
+    }
+
+    private static BigDecimal durationBetween(Timestamp startedAt, Timestamp completedAt) {
+        if (completedAt == null) {
+            return null;
+        } else {
+            long millis = completedAt.getTime() - startedAt.getTime();
+            return (new BigDecimal(millis)).divide(new BigDecimal(1000)).setScale(3, RoundingMode.HALF_EVEN);
+        }
+    }
+
+
+    @javax.inject.Inject
+    JaxbService jaxbService;
+
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo.layout.fallback.xml b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo.layout.fallback.xml
new file mode 100644
index 0000000..fd7c2b2
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo.layout.fallback.xml
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<bs3:grid xsi:schemaLocation="http://isis.apache.org/applib/layout/component http://isis.apache.org/applib/layout/component/component.xsd http://isis.apache.org/applib/layout/grid/bootstrap3 http://isis.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd" xmlns:bs3="http://isis.apache.org/applib/layout/grid/bootstrap3" xmlns:cpt="http://isis.apache.org/applib/layout/component" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <bs3:row>
+        <bs3:col span="12">
+            <!-- if command replay module also enabled -->
+            <cpt:collection id="replayQueue" paged="5"/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="12" unreferencedActions="true">
+            <cpt:domainObject/>
+            <cpt:action id="links"/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="4">
+            <bs3:row>
+                <bs3:col span="12">
+                    <cpt:fieldSet name="Identifiers" id="identifiers" unreferencedProperties="true">
+                        <cpt:action id="recentAuditEntries" position="PANEL_DROPDOWN"/>
+                        <cpt:action id="findChangesByDate" position="PANEL_DROPDOWN"/>
+                        <cpt:action id="recentChanges" position="PANEL_DROPDOWN"/>
+                        <cpt:action id="clearHints" position="PANEL_DROPDOWN"/>
+                        <cpt:action id="downloadLayoutXml" position="PANEL_DROPDOWN"/>
+                        <cpt:action id="downloadJdoMetadata" position="PANEL_DROPDOWN"/>
+                        <cpt:action id="rebuildMetamodel" position="PANEL_DROPDOWN"/>
+                        <cpt:property id="type"/>
+                        <cpt:property id="transactionId"/>
+                        <cpt:property id="memberIdentifier"/>
+                        <cpt:property id="user"/>
+                        <cpt:property id="timestamp"/>
+                    </cpt:fieldSet>
+                </bs3:col>
+            </bs3:row>
+            <bs3:row>
+                <bs3:col span="12">
+                    <bs3:tabGroup>
+                        <bs3:tab name="Target">
+                            <bs3:row>
+                                <bs3:col span="12">
+                                    <cpt:fieldSet name="Target" id="target">
+                                        <cpt:property id="targetClass"/>
+                                        <cpt:property id="targetAction"/>
+                                        <cpt:property id="propertyId"/>
+                                        <cpt:property id="targetStr" hidden="ALL_TABLES"/>
+                                    </cpt:fieldSet>
+                                    <cpt:fieldSet name="Notes" id="notes"/>
+                                </bs3:col>
+                            </bs3:row>
+                        </bs3:tab>
+                        <bs3:tab name="Target Audit Entries">
+                            <bs3:row>
+                                <bs3:col span="12">
+                                    <cpt:collection id="targetAuditEntries" paged="5">
+                                        <cpt:named>Target audit entries</cpt:named>
+                                        <cpt:describedAs>All audit entries for the target of this command</cpt:describedAs>
+                                    </cpt:collection>
+                                </bs3:col>
+                            </bs3:row>
+                        </bs3:tab>
+                    </bs3:tabGroup>
+                </bs3:col>
+            </bs3:row>
+        </bs3:col>
+        <bs3:col span="8">
+            <bs3:row>
+                <bs3:col span="6">
+                    <cpt:fieldSet name="Arguments" id="arguments">
+                        <cpt:property id="arguments" labelPosition="TOP"/>
+                        <cpt:property id="preValue"  hidden="ALL_TABLES"/>
+                        <cpt:property id="postValue" hidden="ALL_TABLES"/>
+                        <cpt:property id="memento" labelPosition="TOP" multiLine="20"/>
+                    </cpt:fieldSet>
+                </bs3:col>
+                <bs3:col span="6">
+                    <cpt:fieldSet name="Execution" id="execution">
+                        <cpt:action id="retry" cssClassFa="fa-repeat" cssClass="btn-warning"/>
+                        <cpt:action id="exclude" cssClassFa="fa-ban" cssClass="btn-warning"/>
+                        <cpt:action id="replayNext" cssClassFa="fa-step-forward" cssClass="btn-success"/>
+                        <cpt:property id="executeIn"/>
+                        <cpt:property id="parent"/>
+                        <cpt:property id="replayState"/>
+                        <cpt:property id="replayStateFailureReason"/>
+                    </cpt:fieldSet>
+                    <cpt:fieldSet name="Timings" id="timings">
+                        <cpt:property id="startedAt"/>
+                        <cpt:property id="completedAt"/>
+                        <cpt:property id="duration"/>
+                        <cpt:property id="complete"/>
+                    </cpt:fieldSet>
+                    <cpt:fieldSet name="Results" id="results">
+                        <cpt:property id="resultSummary"/>
+                        <cpt:property id="resultStr"/>
+                        <cpt:property id="exception" labelPosition="TOP"/>
+                    </cpt:fieldSet>
+                </bs3:col>
+            </bs3:row>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="12">
+            <bs3:tabGroup>
+                <bs3:tab name="Audit">
+                    <bs3:row>
+                        <bs3:col span="12">
+                            <cpt:collection id="auditEntriesInTransaction"/>
+                        </bs3:col>
+                    </bs3:row>
+                </bs3:tab>
+                <bs3:tab name="Publishing">
+                    <bs3:row>
+                        <bs3:col span="12">
+                            <cpt:collection id="publishedEventsInTransaction"/>
+                            <cpt:collection id="statusMessagesInTransaction"/>
+                        </bs3:col>
+                    </bs3:row>
+                </bs3:tab>
+                <bs3:tab name="Commands">
+                    <bs3:row>
+                        <bs3:col span="12">
+                            <cpt:collection id="childCommands"/>
+                            <cpt:collection id="siblingCommands"/>
+                        </bs3:col>
+                    </bs3:row>
+                </bs3:tab>
+            </bs3:tabGroup>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="12" unreferencedCollections="true">
+        </bs3:col>
+    </bs3:row>
+</bs3:grid>
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo.png b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo.png
new file mode 100644
index 0000000..7545614
Binary files /dev/null and b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo.png differ
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_childCommands.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_childCommands.java
new file mode 100644
index 0000000..f3a3caf
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_childCommands.java
@@ -0,0 +1,32 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.List;
+
+import org.apache.isis.applib.annotation.Collection;
+import org.apache.isis.applib.annotation.CollectionLayout;
+import org.apache.isis.applib.annotation.MemberOrder;
+
+import org.isisaddons.module.command.IsisModuleExtCommandLogImpl;
+
+
+@Collection(domainEvent = CommandJdo_childCommands.CollectionDomainEvent.class)
+@CollectionLayout(defaultView = "table")
+public class CommandJdo_childCommands {
+
+    public static class CollectionDomainEvent
+            extends IsisModuleExtCommandLogImpl.CollectionDomainEvent<CommandJdo_childCommands, CommandJdo> { }
+
+    private final CommandJdo commandJdo;
+    public CommandJdo_childCommands(final CommandJdo commandJdo) {
+        this.commandJdo = commandJdo;
+    }
+
+    @MemberOrder(sequence = "100.100")
+    public List<CommandJdo> coll() {
+        return backgroundCommandRepository.findByParent(commandJdo);
+    }
+
+    @javax.inject.Inject
+    private BackgroundCommandServiceJdoRepository backgroundCommandRepository;
+    
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_openResultObject.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_openResultObject.java
new file mode 100644
index 0000000..13133eb
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_openResultObject.java
@@ -0,0 +1,58 @@
+package org.isisaddons.module.command.dom;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.bookmark.BookmarkService;
+import org.apache.isis.applib.services.message.MessageService;
+
+import org.isisaddons.module.command.IsisModuleExtCommandLogImpl;
+
+@Action(
+    semantics = SemanticsOf.SAFE
+    , domainEvent = CommandJdo_openResultObject.ActionDomainEvent.class
+    , associateWith = "result"
+)
+@ActionLayout(named = "Open")
+public class CommandJdo_openResultObject {
+
+    public static abstract class ActionDomainEvent
+            extends IsisModuleExtCommandLogImpl.ActionDomainEvent<CommandJdo_openResultObject> { }
+
+    private final CommandJdo commandJdo;
+    public CommandJdo_openResultObject(CommandJdo commandJdo) {
+        this.commandJdo = commandJdo;
+    }
+
+    @MemberOrder(name="ResultStr", sequence="1")
+    public Object act() {
+        return lookupBookmark(commandJdo.getResult());
+    }
+    public boolean hideAct() {
+        return commandJdo.getResult() == null;
+    }
+
+    private Object lookupBookmark(Bookmark bookmark) {
+        try {
+            return bookmarkService != null ? bookmarkService.lookup(bookmark) : null;
+        } catch (RuntimeException ex) {
+            if (ex.getClass().getName().contains("ObjectNotFoundException")) {
+                messageService.warnUser("Object not found - has it since been deleted?");
+                return null;
+            } else {
+                throw ex;
+            }
+        }
+    }
+
+    @javax.inject.Inject
+    BookmarkService bookmarkService;
+
+    @javax.inject.Inject
+    MessageService messageService;
+
+
+
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_retry.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_retry.java
new file mode 100644
index 0000000..aa8dee5
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_retry.java
@@ -0,0 +1,94 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.CommandExecuteIn;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.command.CommandContext;
+import org.apache.isis.applib.services.jaxb.JaxbService;
+import org.apache.isis.schema.cmd.v2.CommandDto;
+
+import org.isisaddons.module.command.IsisModuleExtCommandLogImpl;
+
+@Action(
+    semantics = SemanticsOf.NON_IDEMPOTENT_ARE_YOU_SURE
+    , domainEvent = CommandJdo_retry.ActionDomainEvent.class
+)
+public class CommandJdo_retry {
+
+    public static enum Mode {
+        SCHEDULE_NEW,
+        REUSE
+    }
+
+    private final CommandJdo commandJdo;
+    public CommandJdo_retry(CommandJdo commandJdo) {
+        this.commandJdo = commandJdo;
+    }
+
+
+    public static class ActionDomainEvent extends IsisModuleExtCommandLogImpl.ActionDomainEvent<CommandJdo_retry> { }
+    @MemberOrder(name = "executeIn", sequence = "1")
+    public CommandJdo act(final Mode mode) {
+
+        switch (mode) {
+        case SCHEDULE_NEW:
+            final String memento = commandJdo.getMemento();
+            final CommandDto dto = jaxbService.fromXml(CommandDto.class, memento);
+            backgroundCommandServiceJdo.schedule(
+                    dto, commandContext.getCommand(), commandJdo.getTargetClass(), commandJdo.getTargetAction(), commandJdo.getArguments());
+            break;
+        case REUSE:
+            // will cause it to be picked up next time around
+            commandJdo.internal().setStartedAt(null);
+            commandJdo.internal().setException(null);
+            commandJdo.internal().setCompletedAt(null);
+            commandJdo.setResult(null);
+            commandJdo.setReplayState(null);
+            break;
+        default:
+            // shouldn't occur
+            throw new IllegalStateException(String.format("Probable framework error, unknown mode: %s", mode));
+        }
+        return commandJdo;
+    }
+
+    public List<Mode> choices0Act() {
+        CommandExecuteIn executeIn = commandJdo.getExecuteIn();
+        switch (executeIn){
+            case FOREGROUND:
+            case BACKGROUND:
+                return Arrays.asList(Mode.SCHEDULE_NEW, Mode.REUSE);
+            case REPLAYABLE:
+                return Collections.singletonList(Mode.REUSE);
+            default:
+                // shouldn't occur
+                throw new IllegalStateException(String.format("Probable framework error, unknown executeIn: %s", executeIn));
+        }
+    }
+
+    public Mode default0Act() {
+        return choices0Act().get(0);
+    }
+    public String disableAct() {
+        if (!commandJdo.isComplete()) {
+            return "Not yet completed";
+        }
+        return null;
+    }
+
+
+    @Inject
+    CommandContext commandContext;
+    @Inject
+    BackgroundCommandServiceJdo backgroundCommandServiceJdo;
+    @Inject
+    JaxbService jaxbService;
+
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_siblingCommands.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_siblingCommands.java
new file mode 100644
index 0000000..2b13c4c
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandJdo_siblingCommands.java
@@ -0,0 +1,41 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.isis.applib.annotation.Collection;
+import org.apache.isis.applib.annotation.CollectionLayout;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.services.command.Command;
+
+import org.isisaddons.module.command.IsisModuleExtCommandLogImpl;
+
+@Collection(domainEvent = CommandJdo_siblingCommands.CollectionDomainEvent.class)
+@CollectionLayout(defaultView = "table")
+public class CommandJdo_siblingCommands {
+
+    public static class CollectionDomainEvent
+            extends IsisModuleExtCommandLogImpl.CollectionDomainEvent<CommandJdo_siblingCommands, CommandJdo> { }
+
+    private final CommandJdo commandJdo;
+    public CommandJdo_siblingCommands(final CommandJdo commandJdo) {
+        this.commandJdo = commandJdo;
+    }
+
+    @MemberOrder(sequence = "100.110")
+    public List<CommandJdo> coll() {
+        final Command parent = commandJdo.getParent();
+        if(!(parent instanceof CommandJdo)) {
+            return Collections.emptyList();
+        }
+        final CommandJdo parentJdo = (CommandJdo) parent;
+        final List<CommandJdo> siblingCommands = backgroundCommandRepository.findByParent(parentJdo);
+        siblingCommands.remove(commandJdo);
+        return siblingCommands;
+    }
+
+
+    @javax.inject.Inject
+    private BackgroundCommandServiceJdoRepository backgroundCommandRepository;
+    
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandServiceJdo.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandServiceJdo.java
new file mode 100644
index 0000000..ffac567
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandServiceJdo.java
@@ -0,0 +1,90 @@
+package org.isisaddons.module.command.dom;
+
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.isis.applib.annotation.CommandExecuteIn;
+import org.apache.isis.applib.annotation.CommandPersistence;
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.services.command.Command;
+import org.apache.isis.applib.services.command.Command.Executor;
+import org.apache.isis.applib.services.command.spi.CommandService;
+import org.apache.isis.applib.services.factory.FactoryService;
+import org.apache.isis.applib.services.repository.RepositoryService;
+
+@DomainService()
+public class CommandServiceJdo implements CommandService {
+
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LoggerFactory.getLogger(CommandServiceJdo.class);
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Command create() {
+        CommandJdo command = factoryService.instantiate(CommandJdo.class);
+        command.internal().setExecutor(Executor.OTHER);
+        command.internal().setPersistence(CommandPersistence.IF_HINTED);
+        return command;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void complete(final Command command) {
+        final CommandJdo commandJdo = asUserInitiatedCommandJdo(command);
+        if(commandJdo == null) {
+            return;
+        }
+        commandServiceJdoRepository.persistIfHinted(commandJdo);
+
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean persistIfPossible(Command command) {
+        if(!(command instanceof CommandJdo)) {
+            // ought not to be the case, since this service created the object in the #create() method
+            return false;
+        }
+        final CommandJdo commandJdo = (CommandJdo)command;
+        repositoryService.persist(commandJdo);
+        return true;
+    }
+
+
+    /**
+     * Not API, also used by {@link CommandServiceJdoRepository}.
+     */
+    CommandJdo asUserInitiatedCommandJdo(final Command command) {
+        if(!(command instanceof CommandJdo)) {
+            // ought not to be the case, since this service created the object in the #create() method
+            return null;
+        }
+        if(command.getExecuteIn() != CommandExecuteIn.FOREGROUND) {
+            return null;
+        } 
+        final CommandJdo commandJdo = (CommandJdo) command;
+        return commandJdo.shouldPersist()? commandJdo: null;
+    }
+
+
+
+    @Inject
+    RepositoryService repositoryService;
+
+    @Inject
+    CommandServiceJdoRepository commandServiceJdoRepository;
+
+    @Inject
+    FactoryService factoryService;
+
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandServiceJdoRepository.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandServiceJdoRepository.java
new file mode 100644
index 0000000..02ee5b3
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandServiceJdoRepository.java
@@ -0,0 +1,370 @@
+package org.isisaddons.module.command.dom;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import javax.jdo.JDOQLTypedQuery;
+
+
+import org.joda.time.LocalDate;
+
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
+import org.apache.isis.applib.query.Query;
+import org.apache.isis.applib.query.QueryDefault;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.command.Command;
+import org.apache.isis.applib.services.command.CommandContext;
+import org.apache.isis.applib.services.command.CommandWithDto;
+import org.apache.isis.applib.services.repository.RepositoryService;
+import org.apache.isis.applib.util.schema.CommandDtoUtils;
+import org.apache.isis.persistence.jdo.applib.services.IsisJdoSupport_v3_2;
+import org.apache.isis.schema.cmd.v2.CommandDto;
+import org.apache.isis.schema.cmd.v2.CommandsDto;
+import org.apache.isis.schema.cmd.v2.MapDto;
+import org.apache.isis.schema.common.v2.OidDto;
+
+import lombok.val;
+import lombok.var;
+
+/**
+ * Provides supporting functionality for querying and persisting
+ * {@link CommandJdo command} entities.
+ */
+@DomainService()
+public class CommandServiceJdoRepository {
+
+    public List<CommandJdo> findByFromAndTo(
+            final LocalDate from, final LocalDate to) {
+        final Timestamp fromTs = toTimestampStartOfDayWithOffset(from, 0);
+        final Timestamp toTs = toTimestampStartOfDayWithOffset(to, 1);
+        
+        final Query<CommandJdo> query;
+        if(from != null) {
+            if(to != null) {
+                query = new QueryDefault<>(CommandJdo.class,
+                        "findByTimestampBetween", 
+                        "from", fromTs,
+                        "to", toTs);
+            } else {
+                query = new QueryDefault<>(CommandJdo.class,
+                        "findByTimestampAfter", 
+                        "from", fromTs);
+            }
+        } else {
+            if(to != null) {
+                query = new QueryDefault<>(CommandJdo.class,
+                        "findByTimestampBefore", 
+                        "to", toTs);
+            } else {
+                query = new QueryDefault<>(CommandJdo.class,
+                        "find");
+            }
+        }
+        return repositoryService.allMatches(query);
+    }
+
+
+    public Optional<CommandJdo> findByTransactionId(final UUID transactionId) {
+        persistCurrentCommandIfRequired();
+        return repositoryService.firstMatch(
+                new QueryDefault<>(CommandJdo.class,
+                        "findByTransactionId", 
+                        "transactionId", transactionId));
+    }
+
+
+    public List<CommandJdo> findCurrent() {
+        persistCurrentCommandIfRequired();
+        return repositoryService.allMatches(
+                new QueryDefault<>(CommandJdo.class, "findCurrent"));
+    }
+
+
+    public List<CommandJdo> findCompleted() {
+        persistCurrentCommandIfRequired();
+        return repositoryService.allMatches(
+                new QueryDefault<>(CommandJdo.class, "findCompleted"));
+    }
+
+
+    private void persistCurrentCommandIfRequired() {
+        if(commandContext == null || commandService == null) {
+            return;
+        } 
+        final Command command = commandContext.getCommand();
+        final CommandJdo commandJdo = commandService.asUserInitiatedCommandJdo(command);
+        if(commandJdo == null) {
+            return;
+        }
+        repositoryService.persist(commandJdo);
+    }
+
+
+    public List<CommandJdo> findByTargetAndFromAndTo(
+            final Bookmark target
+            , final LocalDate from
+            , final LocalDate to) {
+        final String targetStr = target.toString();
+        final Timestamp fromTs = toTimestampStartOfDayWithOffset(from, 0);
+        final Timestamp toTs = toTimestampStartOfDayWithOffset(to, 1);
+        
+        final Query<CommandJdo> query;
+        if(from != null) {
+            if(to != null) {
+                query = new QueryDefault<>(CommandJdo.class,
+                        "findByTargetAndTimestampBetween", 
+                        "targetStr", targetStr,
+                        "from", fromTs,
+                        "to", toTs);
+            } else {
+                query = new QueryDefault<>(CommandJdo.class,
+                        "findByTargetAndTimestampAfter", 
+                        "targetStr", targetStr,
+                        "from", fromTs);
+            }
+        } else {
+            if(to != null) {
+                query = new QueryDefault<>(CommandJdo.class,
+                        "findByTargetAndTimestampBefore", 
+                        "targetStr", targetStr,
+                        "to", toTs);
+            } else {
+                query = new QueryDefault<>(CommandJdo.class,
+                        "findByTarget", 
+                        "targetStr", targetStr);
+            }
+        }
+        return repositoryService.allMatches(query);
+    }
+
+    private static Timestamp toTimestampStartOfDayWithOffset(final LocalDate dt, int daysOffset) {
+        return dt!=null
+                ?new java.sql.Timestamp(dt.toDateTimeAtStartOfDay().plusDays(daysOffset).getMillis())
+                :null;
+    }
+
+
+    public List<CommandJdo> findRecentByUser(final String user) {
+        return repositoryService.allMatches(
+                new QueryDefault<>(CommandJdo.class, "findRecentByUser", "user", user));
+    }
+
+
+    public List<CommandJdo> findRecentByTarget(final Bookmark target) {
+        final String targetStr = target.toString();
+        return repositoryService.allMatches(
+                new QueryDefault<>(CommandJdo.class, "findRecentByTarget", "targetStr", targetStr));
+    }
+
+
+    public List<CommandJdo> findRecentBackgroundByTarget(Bookmark target) {
+        final String targetStr = target.toString();
+        return repositoryService.allMatches(
+                new QueryDefault<>(CommandJdo.class, "findRecentBackgroundByTarget", "targetStr", targetStr));
+    }
+
+
+    /**
+     * Intended to support the replay of commands on a slave instance of the application.
+     *
+     * This finder returns all (completed) {@link CommandJdo}s started after the command with the specified
+     * transaction Id.  The number of commands returned can be limited so that they can be applied in batches.
+     *
+     * If the provided transactionId is null, then only a single {@link CommandJdo command} is returned.  This is
+     * intended to support the case when the slave does not yet have any {@link CommandJdo command}s replicated.
+     * In practice this is unlikely; typically we expect that the slave will be set up to run against a copy of the
+     * master instance's DB (restored from a backup), in which case there will already be a {@link CommandJdo command}
+     * representing the current high water mark on the slave.
+     *
+     * If the transaction id is not null but the corresponding {@link CommandJdo command} is not found, then
+     * <tt>null</tt> is returned. In the replay scenario the caller will probably interpret this as an error because
+     * it means that the high water mark on the slave is inaccurate, referring to a non-existent
+     * {@link CommandJdo command} on the master.
+     *
+     * @param transactionId - the identifier of the {@link CommandJdo command} being the replay hwm (using {@link #findReplayHwm()} on the slave), or null if no HWM was found there.
+     * @param batchSize - to restrict the number returned (so that replay commands can be batched).
+     *
+     * @return
+     */
+    public List<CommandJdo> findForegroundSince(final UUID transactionId, final Integer batchSize) {
+        if(transactionId == null) {
+            return findForegroundFirst();
+        }
+        final CommandJdo from = findByTransactionIdElseNull(transactionId);
+        if(from == null) {
+            return null;
+        }
+        return findForegroundSince(from.getTimestamp(), batchSize);
+    }
+
+    private List<CommandJdo> findForegroundFirst() {
+        Optional<CommandJdo> firstCommandIfAny = repositoryService.firstMatch(
+                new QueryDefault<>(CommandJdo.class, "findForegroundFirst"));
+        return firstCommandIfAny
+                .map(Collections::singletonList)
+                .orElse(Collections.emptyList());
+    }
+
+
+    private CommandJdo findByTransactionIdElseNull(final UUID transactionId) {
+        var q = isisJdoSupport.newTypesafeQuery(CommandJdo.class);
+        val cand = QCommandJdo.candidate();
+        q = q.filter(
+                cand.transactionId.eq(q.parameter("transactionId", UUID.class))
+        );
+        q.setParameter("transactionId", transactionId);
+        return q.executeUnique();
+    }
+
+    private List<CommandJdo> findForegroundSince(final Timestamp timestamp, final Integer batchSize) {
+        val q = new QueryDefault<>(
+                CommandJdo.class,
+                "findForegroundSince",
+                "timestamp", timestamp);
+
+        // DN generates incorrect SQL for SQL Server if count set to 1; so we set to 2 and then trim
+        if(batchSize != null) {
+            q.withCount(batchSize == 1 ? 2 : batchSize);
+        }
+        final List<CommandJdo> commandJdos = repositoryService.allMatches(q);
+        return batchSize != null && batchSize == 1 && commandJdos.size() > 1
+                    ? commandJdos.subList(0,1)
+                    : commandJdos;
+    }
+
+
+    public CommandJdo findReplayHwm() {
+
+        // most recent replayable command, replicated from master to slave
+        // this may or may not
+        Optional<CommandJdo> replayableHwm = repositoryService.firstMatch(
+                new QueryDefault<>(CommandJdo.class, "findReplayableHwm"));
+
+        return replayableHwm
+                .orElseGet(() -> {
+            // otherwise, the most recent completed command, run in the foreground
+            // on the slave, this corresponds to a command restored from a copy of the production database
+            Optional<CommandJdo> restoredFromDbHwm = repositoryService.firstMatch(
+                    new QueryDefault<>(CommandJdo.class, "findForegroundHwm"));
+
+            return restoredFromDbHwm.orElse(null);
+        });
+
+    }
+
+
+    public List<CommandJdo> findBackgroundCommandsNotYetStarted() {
+        return repositoryService.allMatches(
+                new QueryDefault<>(CommandJdo.class,
+                        "findBackgroundCommandsNotYetStarted"));
+    }
+
+
+    @Programmatic
+    public List<CommandJdo> findBackgroundCommandsByParent(final CommandJdo parent) {
+        return repositoryService.allMatches(
+                new QueryDefault<>(CommandJdo.class,
+                        "findBackgroundCommandsByParent",
+                        "parent", parent));
+    }
+
+
+    public List<CommandJdo> findReplayedOnSlave() {
+        return repositoryService.allMatches(
+                new QueryDefault<>(CommandJdo.class, "findReplayableMostRecentStarted"));
+    }
+
+
+    public List<CommandJdo> saveForReplay(final CommandsDto commandsDto) {
+        List<CommandDto> commandDto = commandsDto.getCommandDto();
+        List<CommandJdo> commands = new ArrayList<>();
+        for (final CommandDto dto : commandDto) {
+            commands.add(saveForReplay(dto));
+        }
+        return commands;
+    }
+
+    @Programmatic
+    public CommandJdo saveForReplay(final CommandDto dto) {
+
+        final MapDto userData = dto.getUserData();
+        if (userData == null ) {
+            throw new IllegalStateException(String.format(
+                    "Can only persist DTOs with additional userData; got: \n%s",
+                    CommandDtoUtils.toXml(dto)));
+        }
+
+        final CommandJdo commandJdo = new CommandJdo();
+
+        commandJdo.setTransactionId(UUID.fromString(dto.getTransactionId()));
+        commandJdo.internal().setTimestamp(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(dto.getTimestamp()));
+        commandJdo.internal().setUser(dto.getUser());
+        commandJdo.internal().setExecuteIn(org.apache.isis.applib.annotation.CommandExecuteIn.REPLAYABLE);
+
+        commandJdo.setTargetClass(CommandDtoUtils.getUserData(dto, CommandWithDto.USERDATA_KEY_TARGET_CLASS));
+        commandJdo.setTargetAction(CommandDtoUtils.getUserData(dto, CommandWithDto.USERDATA_KEY_TARGET_ACTION));
+        commandJdo.internal().setArguments(CommandDtoUtils.getUserData(dto, CommandWithDto.USERDATA_KEY_ARGUMENTS));
+
+        commandJdo.setReplayState(ReplayState.PENDING);
+        commandJdo.internal().setPersistHint(true);
+
+        final OidDto firstTarget = dto.getTargets().getOid().get(0);
+        commandJdo.setTargetStr(Bookmark.from(firstTarget).toString());
+        commandJdo.internal().setMemento(CommandDtoUtils.toXml(dto));
+        commandJdo.setMemberIdentifier(dto.getMember().getMemberIdentifier());
+
+        persist(commandJdo);
+
+        return commandJdo;
+    }
+
+    public void persist(final CommandJdo commandJdo) {
+        withSafeTargetStr(commandJdo);
+        withSafeResultStr(commandJdo);
+        repositoryService.persist(commandJdo);
+    }
+
+    public void persistIfHinted(final CommandJdo commandJdo) {
+        withSafeTargetStr(commandJdo);
+        withSafeResultStr(commandJdo);
+        if(commandJdo.shouldPersist()) {
+            repositoryService.persist(commandJdo);
+        }
+    }
+
+    private static void withSafeTargetStr(final CommandJdo commandJdo) {
+        if (tooLong(commandJdo.getTargetStr())) {
+            commandJdo.setTargetStr(null);
+        }
+    }
+    private static void withSafeResultStr(final CommandJdo commandJdo) {
+        if (tooLong(commandJdo.getResultStr())) {
+            commandJdo.setResultStr(null);
+        }
+    }
+
+    private static boolean tooLong(final String str) {
+        return str != null && str.length() > 2000;
+    }
+
+
+
+    @javax.inject.Inject
+    CommandServiceJdo commandService;
+
+    @javax.inject.Inject
+    CommandContext commandContext;
+
+    @javax.inject.Inject
+    RepositoryService repositoryService;
+
+    @javax.inject.Inject
+    IsisJdoSupport_v3_2 isisJdoSupport;
+
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandServiceMenu.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandServiceMenu.java
new file mode 100644
index 0000000..a6b76cd
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/CommandServiceMenu.java
@@ -0,0 +1,100 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.BookmarkPolicy;
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.DomainServiceLayout;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.Optionality;
+import org.apache.isis.applib.annotation.Parameter;
+import org.apache.isis.applib.annotation.ParameterLayout;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.clock.ClockService;
+
+import org.isisaddons.module.command.IsisModuleExtCommandLogImpl;
+
+@DomainService(
+        nature = NatureOfService.VIEW,
+        objectType = "isisextcorecommandlog.CommandServiceMenu"
+)
+@DomainServiceLayout(
+        named = "Activity"
+        , menuBar = DomainServiceLayout.MenuBar.SECONDARY
+)
+public class CommandServiceMenu {
+
+    public static abstract class PropertyDomainEvent<T>
+            extends IsisModuleExtCommandLogImpl.PropertyDomainEvent<CommandServiceMenu, T> { }
+    public static abstract class CollectionDomainEvent<T>
+            extends IsisModuleExtCommandLogImpl.CollectionDomainEvent<CommandServiceMenu, T> { }
+    public static abstract class ActionDomainEvent
+            extends IsisModuleExtCommandLogImpl.ActionDomainEvent<CommandServiceMenu> {
+    }
+
+
+    public static class ActiveCommandsDomainEvent extends ActionDomainEvent { }
+    @Action(domainEvent = ActiveCommandsDomainEvent.class, semantics = SemanticsOf.SAFE)
+    @ActionLayout(bookmarking = BookmarkPolicy.AS_ROOT, cssClassFa = "fa-bolt")
+    @MemberOrder(sequence="10")
+    public List<CommandJdo> activeCommands() {
+        return commandServiceRepository.findCurrent();
+    }
+    public boolean hideActiveCommands() {
+        return commandServiceRepository == null;
+    }
+
+
+    public static class FindCommandsDomainEvent extends ActionDomainEvent { }
+    @Action(domainEvent = FindCommandsDomainEvent.class, semantics = SemanticsOf.SAFE)
+    @ActionLayout(cssClassFa = "fa-search")
+    @MemberOrder(sequence="20")
+    public List<CommandJdo> findCommands(
+            @Parameter(optionality= Optionality.OPTIONAL)
+            @ParameterLayout(named="From")
+            final LocalDate from,
+            @Parameter(optionality= Optionality.OPTIONAL)
+            @ParameterLayout(named="To")
+            final LocalDate to) {
+        return commandServiceRepository.findByFromAndTo(from, to);
+    }
+    public boolean hideFindCommands() {
+        return commandServiceRepository == null;
+    }
+    public LocalDate default0FindCommands() {
+        return clockService.nowAsJodaLocalDate().minusDays(7);
+    }
+    public LocalDate default1FindCommands() {
+        return clockService.nowAsJodaLocalDate();
+    }
+
+
+    public static class FindCommandByIdDomainEvent extends ActionDomainEvent { }
+    @Action(domainEvent = FindCommandByIdDomainEvent.class, semantics = SemanticsOf.SAFE)
+    @ActionLayout(cssClassFa = "fa-crosshairs")
+    @MemberOrder(sequence="30")
+    public CommandJdo findCommandById(
+            @ParameterLayout(named="Transaction Id")
+            final UUID transactionId) {
+        return commandServiceRepository.findByTransactionId(transactionId).orElse(null);
+    }
+    public boolean hideFindCommandById() {
+        return commandServiceRepository == null;
+    }
+
+
+
+    @javax.inject.Inject
+    CommandServiceJdoRepository commandServiceRepository;
+
+    @javax.inject.Inject
+    ClockService clockService;
+
+}
+
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/HasTransactionId_command.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/HasTransactionId_command.java
new file mode 100644
index 0000000..59c5ead
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/HasTransactionId_command.java
@@ -0,0 +1,63 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.UUID;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.Contributed;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.Mixin;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.HasTransactionId;
+import org.apache.isis.applib.services.command.Command;
+
+import org.isisaddons.module.command.IsisModuleExtCommandLogImpl;
+
+
+/**
+ * This mixin contributes a <tt>command</tt> action to any (non-command) implementation of
+ * {@link org.apache.isis.applib.services.HasTransactionId}; that is: audit entries, and published events.  Thus, it
+ * is possible to navigate from the effect back to the cause.
+ */
+@Action(
+    semantics = SemanticsOf.SAFE
+    , domainEvent = HasTransactionId_command.ActionDomainEvent.class
+)
+public class HasTransactionId_command {
+
+    public static class ActionDomainEvent
+            extends IsisModuleExtCommandLogImpl.ActionDomainEvent<HasTransactionId_command> { }
+
+    private final HasTransactionId hasTransactionId;
+    public HasTransactionId_command(final HasTransactionId hasTransactionId) {
+        this.hasTransactionId = hasTransactionId;
+    }
+
+
+    @MemberOrder(name="transactionId", sequence="1")
+    public CommandJdo act() {
+        return findCommand();
+    }
+    /**
+     * Hide if the contributee is a {@link Command}, because {@link Command}s already have a
+     * {@link Command#getParent() parent} property.
+     */
+    public boolean hide$$() {
+        return (hasTransactionId instanceof Command);
+    }
+    public String disable$$() {
+        return findCommand() == null ? "No command found for transaction Id": null;
+    }
+
+    private CommandJdo findCommand() {
+        final UUID transactionId = hasTransactionId.getTransactionId();
+        return commandServiceRepository
+                .findByTransactionId(transactionId)
+                .orElse(null);
+    }
+
+
+    @javax.inject.Inject
+    CommandServiceJdoRepository commandServiceRepository;
+
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/HasUsername_recentCommandsByUser.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/HasUsername_recentCommandsByUser.java
new file mode 100644
index 0000000..d46108b
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/HasUsername_recentCommandsByUser.java
@@ -0,0 +1,43 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.isis.applib.annotation.Collection;
+import org.apache.isis.applib.annotation.CollectionLayout;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.services.HasUsername;
+
+import org.isisaddons.module.command.IsisModuleExtCommandLogImpl;
+
+
+@Collection(
+    domainEvent = HasUsername_recentCommandsByUser.CollectionDomainEvent.class
+)
+@CollectionLayout(
+    defaultView = "table"
+)
+public class HasUsername_recentCommandsByUser {
+
+    public static class CollectionDomainEvent
+            extends IsisModuleExtCommandLogImpl.CollectionDomainEvent<HasUsername_recentCommandsByUser, CommandJdo> { }
+
+    private final HasUsername hasUsername;
+    public HasUsername_recentCommandsByUser(final HasUsername hasUsername) {
+        this.hasUsername = hasUsername;
+    }
+
+    @MemberOrder(name="user", sequence = "3")
+    public List<CommandJdo> coll() {
+        final String username = hasUsername.getUsername();
+        return username != null
+                ? commandServiceRepository.findRecentByUser(username)
+                : Collections.emptyList();
+    }
+    public boolean hideColl() {
+        return hasUsername.getUsername() == null;
+    }
+
+    @javax.inject.Inject
+    private CommandServiceJdoRepository commandServiceRepository;
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/Object_recentCommands.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/Object_recentCommands.java
new file mode 100644
index 0000000..9d59bcb
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/Object_recentCommands.java
@@ -0,0 +1,58 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.List;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.Mixin;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.HasTransactionId;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.bookmark.BookmarkService;
+
+import org.isisaddons.module.command.IsisModuleExtCommandLogImpl;
+
+/**
+ * This mixin contributes a <tt>recentCommands</tt> action to any domain object
+ * (unless also a {@link HasTransactionId} - cmmands don't themselves have commands).
+ */
+@Mixin(method = "act")
+@Action(
+    semantics = SemanticsOf.SAFE,
+    domainEvent = Object_recentCommands.ActionDomainEvent.class
+)
+@ActionLayout(
+    cssClassFa = "fa-bolt",
+    position = ActionLayout.Position.PANEL_DROPDOWN
+)
+public class Object_recentCommands {
+
+    public static class ActionDomainEvent
+            extends IsisModuleExtCommandLogImpl.ActionDomainEvent<Object_recentCommands> { }
+
+    private final Object domainObject;
+    public Object_recentCommands(final Object domainObject) {
+        this.domainObject = domainObject;
+    }
+
+    @MemberOrder(name = "datanucleusIdLong", sequence = "900.1")
+    public List<CommandJdo> act() {
+        final Bookmark bookmark = bookmarkService.bookmarkFor(domainObject);
+        return commandServiceRepository.findRecentByTarget(bookmark);
+    }
+    /**
+     * Hide if the contributee is itself {@link HasTransactionId}
+     * (commands don't have commands).
+     */
+    public boolean hideAct() {
+        return (domainObject instanceof HasTransactionId);
+    }
+
+    @javax.inject.Inject
+    CommandServiceJdoRepository commandServiceRepository;
+
+    @javax.inject.Inject
+    BookmarkService bookmarkService;
+
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/ReplayState.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/ReplayState.java
new file mode 100644
index 0000000..ddd115f
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/ReplayState.java
@@ -0,0 +1,11 @@
+package org.isisaddons.module.command.dom;
+
+public enum ReplayState {
+    PENDING,
+    OK,
+    FAILED,
+    EXCLUDED,
+    ;
+
+    public boolean isFailed() { return this == FAILED;}
+}
diff --git a/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/T_backgroundCommands.java b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/T_backgroundCommands.java
new file mode 100644
index 0000000..1194dca
--- /dev/null
+++ b/extensions/core/command-log/impl/src/main/java/org/isisaddons/module/command/dom/T_backgroundCommands.java
@@ -0,0 +1,50 @@
+package org.isisaddons.module.command.dom;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.isis.applib.annotation.Collection;
+import org.apache.isis.applib.annotation.CollectionLayout;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.bookmark.BookmarkService;
+import org.apache.isis.applib.services.queryresultscache.QueryResultsCache;
+
+import org.isisaddons.module.command.IsisModuleExtCommandLogImpl;
+
+@Collection(
+    domainEvent = T_backgroundCommands.CollectionDomainEvent.class
+)
+@CollectionLayout(
+    defaultView = "table"
+)
+public abstract class T_backgroundCommands<T> {
+
+    public static class CollectionDomainEvent extends IsisModuleExtCommandLogImpl.CollectionDomainEvent<T_backgroundCommands, CommandJdo> { }
+
+    private final T domainObject;
+    public T_backgroundCommands(final T domainObject) {
+        this.domainObject = domainObject;
+    }
+
+    public List<CommandJdo> $$() {
+        return findRecentBackground();
+    }
+
+    private List<CommandJdo> findRecentBackground() {
+        final Bookmark bookmark = bookmarkService.bookmarkFor(domainObject);
+        return queryResultsCache.execute(
+                () -> commandServiceJdoRepository.findRecentBackgroundByTarget(bookmark)
+                , T_backgroundCommands.class
+                , "findRecentBackground"
+                , domainObject);
+    }
+
+    @Inject
+    CommandServiceJdoRepository commandServiceJdoRepository;
+    @Inject
+    BookmarkService bookmarkService;
+    @Inject
+    QueryResultsCache queryResultsCache;
+
+}
diff --git a/extensions/core/command-log/pom.xml b/extensions/core/command-log/pom.xml
new file mode 100644
index 0000000..4af0511
--- /dev/null
+++ b/extensions/core/command-log/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.isis.extensions</groupId>
+		<artifactId>isis-extensions</artifactId>
+		<version>2.0.0-SNAPSHOT</version>
+		<relativePath>../../pom.xml</relativePath>
+	</parent>
+
+	<artifactId>isis-extensions-command-log</artifactId>
+	<name>Apache Isis Ext - Command Log</name>
+	<description>Logs commands</description>
+
+	<packaging>pom</packaging>
+
+	<dependencyManagement>
+		<dependencies>
+			<dependency>
+				<groupId>org.apache.isis.testing</groupId>
+				<artifactId>isis-testing</artifactId>
+				<version>2.0.0-SNAPSHOT</version>
+				<scope>import</scope>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+
+	<modules>
+		<module>impl</module>
+	</modules>
+
+</project>
diff --git a/extensions/core/command-replay/pom.xml b/extensions/core/command-replay/pom.xml
new file mode 100644
index 0000000..a82a5e6
--- /dev/null
+++ b/extensions/core/command-replay/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.isis.extensions</groupId>
+		<artifactId>isis-extensions</artifactId>
+		<version>2.0.0-SNAPSHOT</version>
+		<relativePath>../../pom.xml</relativePath>
+	</parent>
+
+	<artifactId>isis-extensions-command-replay</artifactId>
+	<name>Apache Isis Ext - Command Replay</name>
+	<description>Replays commands to secondary system</description>
+
+	<packaging>pom</packaging>
+
+	<dependencyManagement>
+		<dependencies>
+		</dependencies>
+	</dependencyManagement>
+
+	<modules>
+		<module>impl</module>
+	</modules>
+
+</project>
diff --git a/extensions/pom.xml b/extensions/pom.xml
index c67e3d7..ead5240 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -169,6 +169,7 @@
 	</dependencies>
 
 	<modules>
+		<module>core/command-log</module>
 		<module>core/flyway</module>
 		<module>core/model-annotation</module>
 		<module>security/secman</module>
diff --git a/testing/fixtures/applib/src/main/java/org/apache/isis/testing/fixtures/applib/teardown/TeardownFixtureAbstract.java b/testing/fixtures/applib/src/main/java/org/apache/isis/testing/fixtures/applib/teardown/TeardownFixtureAbstract.java
index 5fa401c..c963cf3 100644
--- a/testing/fixtures/applib/src/main/java/org/apache/isis/testing/fixtures/applib/teardown/TeardownFixtureAbstract.java
+++ b/testing/fixtures/applib/src/main/java/org/apache/isis/testing/fixtures/applib/teardown/TeardownFixtureAbstract.java
@@ -40,61 +40,70 @@ public abstract class TeardownFixtureAbstract extends FixtureScript {
         preDeleteFrom(cls);
 
         final String value = discriminatorValueOf(cls);
-
         if(value == null) {
-
-            doDeleteFrom(cls);
-
+            final TypeMetadata metadata = isisJdoSupport.getJdoPersistenceManager()
+                            .getPersistenceManagerFactory().getMetadata(cls.getName());
+            if(metadata == null) {
+                // fall-back
+                deleteFrom(cls.getSimpleName());
+            } else {
+                final String schema = metadata.getSchema();
+                String table = metadata.getTable();
+                if(_Strings.isNullOrEmpty(table)) {
+                    table = cls.getSimpleName();
+                }
+                if(_Strings.isNullOrEmpty(schema)) {
+                    deleteFrom(table);
+                } else {
+                    deleteFrom(schema, table);
+                }
+            }
         } else {
-
             final String column = discriminatorColumnOf(cls);
-
             final String schema = schemaOf(cls);
             final String table = tableOf(cls);
 
-            if (_Strings.isNullOrEmpty(schema)) {
-                deleteFromWhere(table, column, value);
-            } else {
-                deleteFromWhere(schema, table, column, value);
-            }
+            deleteFromWhere(schema, table, column, value);
         }
 
         postDeleteFrom(cls);
     }
 
-    private void doDeleteFrom(Class<?> cls) {
-        final TypeMetadata metadata = isisJdoSupport.getJdoPersistenceManager()
-                        .getPersistenceManagerFactory().getMetadata(cls.getName());
-        if(metadata == null) {
-            // fall-back
-            deleteFrom(cls.getSimpleName());
-        } else {
-            final String schema = metadata.getSchema();
-            String table = metadata.getTable();
-            if(_Strings.isNullOrEmpty(table)) {
-                table = cls.getSimpleName();
-            }
-            if(_Strings.isNullOrEmpty(schema)) {
-                deleteFrom(table);
-            } else {
-                deleteFrom(schema, table);
-            }
-        }
-    }
-
     protected void preDeleteFrom(final Class<?> cls) {}
 
     protected void postDeleteFrom(final Class<?> cls) {}
 
 
     protected Integer deleteFrom(final String schema, final String table) {
-        return isisJdoSupport.executeUpdate(String.format("DELETE FROM \"%s\".\"%s\"", schema, table));
+        if (_Strings.isNullOrEmpty(schema)) {
+            return deleteFrom(table);
+        } else {
+            return isisJdoSupport.executeUpdate(String.format("DELETE FROM \"%s\".\"%s\"", schema, table));
+        }
+    }
+
+    protected Integer deleteFrom(final String table) {
+        return isisJdoSupport.executeUpdate(String.format("DELETE FROM \"%s\"", table));
     }
 
-    protected void deleteFrom(final String table) {
-        isisJdoSupport.executeUpdate(String.format("DELETE FROM \"%s\"", table));
+
+    protected Integer deleteFromWhere(String schema, String table, String column, String value) {
+        if (_Strings.isNullOrEmpty(schema)) {
+            return deleteFromWhere(table, column, value);
+        } else {
+            final String sql = String.format(
+                    "DELETE FROM \"%s\".\"%s\" WHERE \"%s\"='%s'",
+                    schema, table, column, value);
+            return this.isisJdoSupport.executeUpdate(sql);
+        }
     }
 
+    protected Integer deleteFromWhere(String table, String column, String value) {
+        final String sql = String.format(
+                "DELETE FROM \"%s\" WHERE \"%s\"='%s'",
+                table, column, value);
+        return this.isisJdoSupport.executeUpdate(sql);
+    }
 
 
     private String schemaOf(final Class<?> cls) {
@@ -167,20 +176,6 @@ public abstract class TeardownFixtureAbstract extends FixtureScript {
         return isisJdoSupport.getJdoPersistenceManager().getPersistenceManagerFactory();
     }
 
-    protected Integer deleteFromWhere(String schema, String table, String column, String value) {
-        final String sql = String.format(
-                "DELETE FROM \"%s\".\"%s\" WHERE \"%s\"='%s'",
-                schema, table, column, value);
-        return this.isisJdoSupport.executeUpdate(sql);
-    }
-
-    protected void deleteFromWhere(String table, String column, String value) {
-        final String sql = String.format(
-                "DELETE FROM \"%s\" WHERE \"%s\"='%s'",
-                table, column, value);
-        this.isisJdoSupport.executeUpdate(sql);
-    }
-
     @Inject private IsisJdoSupport isisJdoSupport;