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 "real" 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 "real" 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 "real" 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 "real" 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 "real" 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 "TargetMember". Note that the {@link PropertyLayout} already uses
+ * "Member" 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 "real" 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 "real" 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 "real" 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 "real" 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;