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 82b45afb8dfd5d0becfadd672f0b6fa2f5e4d907 Author: danhaywood <[email protected]> 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; + [email protected]( + identityType=IdentityType.APPLICATION, + schema = "isiscoreextcommandlog", + table = "Command") [email protected]( { + @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"), +}) [email protected]({ + @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;
