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 7248a4779b90fa69b099481abebde6ec603a1626 Author: danhaywood <[email protected]> AuthorDate: Wed Sep 9 08:31:25 2020 +0100 ISIS-2222: reworking command, lots of stuff here... --- .../examples/services/DomainChangeRecord.java | 155 +++++++++++++++++++++ .../DomainChangeRecord_openTargetObject.java | 61 ++++++++ .../services/command/spi/CommandService.java | 65 --------- .../applib/annotation/CommandReification.java} | 34 +++-- .../applib/services/command/CommandService.java | 92 ++++++++++++ .../command/spi/CommandServiceListener.java} | 27 ++-- .../impl/CommandServiceListenerForJdo.java | 50 ++++--- .../impl/util/StringUtils_trimmed_Test.java | 28 ++++ .../CommandReplayAnalysisService_trimmed_Test.java | 30 ---- 9 files changed, 403 insertions(+), 139 deletions(-) diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/DomainChangeRecord.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/DomainChangeRecord.java new file mode 100644 index 0000000..ea81d3f --- /dev/null +++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/DomainChangeRecord.java @@ -0,0 +1,155 @@ +/* + * 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 javax.inject.Inject; + +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.Setter; +import lombok.val; +import lombok.extern.log4j.Log4j2; + + +/** + * An abstraction of some sort of recorded change to a domain object: commands, audit entries or published events. + */ +public interface DomainChangeRecord extends HasUniqueId, HasUsername { + + enum ChangeType { + COMMAND, + AUDIT_ENTRY, + PUBLISHED_INTERACTION; + @Override + public String toString() { + return name().replace("_", " "); + } + } + + /** + * 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") + ChangeType getType(); + + + /** + * The unique identifier (a GUID) of the transaction in which this change occurred. + */ + @Property + @MemberOrder(name="Identifiers",sequence = "50") + UUID getUniqueId(); + + + /** + * The user that caused the change. + */ + @Property + @MemberOrder(name="Identifiers", sequence = "10") + String getUsername(); + + + /** + * The time that the change occurred. + */ + @Property + @MemberOrder(name="Identifiers", sequence = "20") + Timestamp getTimestamp(); + + + /** + * The object type of the domain object being changed. + */ + @Property + @PropertyLayout(named="Class") + @MemberOrder(name="Target", sequence = "10") + default String getTargetObjectType() { + return getTarget().getObjectType(); + } + + + + /** + * The {@link Bookmark} identifying the domain object that has changed. + */ + @Property + @PropertyLayout(named="Object") + @MemberOrder(name="Target", sequence="30") + Bookmark getTarget(); + + + /** + * 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> + */ + @Property(optionality = Optionality.OPTIONAL) + @PropertyLayout(named="Member", hidden = Where.ALL_EXCEPT_STANDALONE_TABLES) + @MemberOrder(name="Target", sequence = "20") + String getTargetMember(); + + + /** + * The value of the property prior to it being changed. + * + * <p> + * Populated only for audit entries. + * </p> + */ + @Property(optionality = Optionality.OPTIONAL) + @PropertyLayout(hidden = Where.ALL_EXCEPT_STANDALONE_TABLES) + @MemberOrder(name="Detail",sequence = "6") + String getPreValue(); + + + /** + * The value of the property after it has changed. + * + * <p> + * Populated only for audit entries. + * </p> + */ + @Property(optionality = Optionality.MANDATORY) + @PropertyLayout(hidden = Where.ALL_EXCEPT_STANDALONE_TABLES) + @MemberOrder(name="Detail",sequence = "7") + String getPostValue(); + + +} diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/DomainChangeRecord_openTargetObject.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/DomainChangeRecord_openTargetObject.java new file mode 100644 index 0000000..7033bb5 --- /dev/null +++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/DomainChangeRecord_openTargetObject.java @@ -0,0 +1,61 @@ +package org.apache.isis.applib.services; + +import javax.inject.Inject; + +import org.apache.isis.applib.annotation.Action; +import org.apache.isis.applib.annotation.ActionLayout; +import org.apache.isis.applib.annotation.SemanticsOf; +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; + +@Action( + semantics = SemanticsOf.SAFE + , associateWith = "target" + , associateWithSequence = "1" +) +@ActionLayout(named = "Open") +public class DomainChangeRecord_openTargetObject { + + private final DomainChangeRecord domainChangeRecord; + public DomainChangeRecord_openTargetObject(DomainChangeRecord domainChangeRecord) { + this.domainChangeRecord = domainChangeRecord; + } + + @Action(semantics = SemanticsOf.SAFE, associateWith = "target", associateWithSequence = "1") + @ActionLayout(named = "Open") + public Object openTargetObject() { + try { + return bookmarkService != null + ? bookmarkService.lookup(domainChangeRecord.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 domainChangeRecord.getTarget() == null; + } + + public String disableOpenTargetObject() { + final Object targetObject = domainChangeRecord.getTarget(); + if (targetObject == null) { + return null; + } + final BeanSort sortOfObject = metaModelService.sortOf(domainChangeRecord.getTarget(), MetaModelService.Mode.RELAXED); + return !(sortOfObject.isViewModel() || sortOfObject.isEntity()) + ? "Can only open view models or entities" + : null; + } + + @Inject BookmarkService bookmarkService; + @Inject MessageService messageService; + @Inject MetaModelService metaModelService; + +} diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/spi/CommandService.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/spi/CommandService.java deleted file mode 100644 index eb3abce..0000000 --- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/spi/CommandService.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.command.spi; - -import org.apache.isis.applib.services.command.Command; - -/** - * Default factory service for {@link Command}s. - */ -// tag::refguide[] -public interface CommandService { - - // end::refguide[] - /** - * Simply instantiates the appropriate instance of the {@link Command}. - * - * <p> - * Its members will be populated automatically by the framework (the {@link Command}'s - * {@link Command#getTimestamp()}, {@link Command#getUser()} and {@link Command#getUniqueId()}). - * </p> - */ - // tag::refguide[] - Command create(); // <.> - // end::refguide[] - - /** - * Hint for this implementation to eagerly persist the {@link Command}s if possible; influences the behaviour - * of actions annotated to execute in the {@link org.apache.isis.applib.annotation.CommandExecuteIn#BACKGROUND}. - */ - // tag::refguide[] - boolean persistIfPossible(Command command); // <.> - // end::refguide[] - - /** - * "Complete" the command, typically meaning to indicate that the command is completed, and to - * persist it if its {@link Command#getPersistence()} and {@link Command#isPersistHint() persistence hint} - * indicate that it should be. - * - * <p> - * However, not every implementation necessarily {@link #persistIfPossible(Command) supports persistence}. - * - * <p> - * The framework will automatically have set the {@link Command#getCompletedAt()} property. - * </p> - */ - // tag::refguide[] - void complete(final Command command); // <.> -} -// end::refguide[] diff --git a/api/applib/src/main/adoc/modules/applib-ant/examples/annotation/CommandPersistence.java b/api/applib/src/main/java/org/apache/isis/applib/annotation/CommandReification.java similarity index 50% rename from api/applib/src/main/adoc/modules/applib-ant/examples/annotation/CommandPersistence.java rename to api/applib/src/main/java/org/apache/isis/applib/annotation/CommandReification.java index 7f7000a..a2b8b68 100644 --- a/api/applib/src/main/adoc/modules/applib-ant/examples/annotation/CommandPersistence.java +++ b/api/applib/src/main/java/org/apache/isis/applib/annotation/CommandReification.java @@ -19,34 +19,38 @@ package org.apache.isis.applib.annotation; /** - * Whether the command should be persisted. + * The available policies as to whether action invocations are reified into commands. */ // tag::refguide[] -public enum CommandPersistence { - +public enum CommandReification { // end::refguide[] /** - * (If the configured {@link org.apache.isis.applib.services.command.spi.CommandService} supports it), indicates that the - * {@link org.apache.isis.applib.services.command.Command Command} object should be persisted. + * Whether the action should be handled as a command as per the default command configured in <tt>applicationp.properties</tt>. + * + * <p> + * If no command policy is configured, then the action is <i>not</i> treated as a command. + * </p> */ // tag::refguide[] - PERSISTED, + AS_CONFIGURED, // end::refguide[] /** - * (If the configured {@link org.apache.isis.applib.services.command.spi.CommandService} supports it), indicates that the - * {@link org.apache.isis.applib.services.command.Command Command} object should only be persisted if - * another service, such as the {@link org.apache.isis.applib.services.background.BackgroundCommandService}, hints that it should. + * Handle the action as a command, irrespective of any configuration settings. */ // tag::refguide[] - IF_HINTED, + ENABLED, // end::refguide[] /** - * (Even if the configured {@link org.apache.isis.applib.services.command.spi.CommandService} supports it), indicates that the - * {@link org.apache.isis.applib.services.command.Command Command} object should <i>not</i> be persisted (even if - * another service, such as the {@link org.apache.isis.applib.services.background.BackgroundCommandService}, hints that it should). + * Do not handle the action as a command, irrespective of any configuration settings. */ // tag::refguide[] - NOT_PERSISTED - + DISABLED, + // end::refguide[] + /** + * Ignore the value provided by this annotation (meaning that the framework will keep searching, in meta + * annotations or superclasses/interfaces). + */ + // tag::refguide[] + NOT_SPECIFIED } // end::refguide[] diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/command/CommandService.java b/api/applib/src/main/java/org/apache/isis/applib/services/command/CommandService.java new file mode 100644 index 0000000..ce19893 --- /dev/null +++ b/api/applib/src/main/java/org/apache/isis/applib/services/command/CommandService.java @@ -0,0 +1,92 @@ +/* + * 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.command; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Primary; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +import org.apache.isis.applib.annotation.IsisInteractionScope; +import org.apache.isis.applib.annotation.OrderPrecedence; +import org.apache.isis.applib.services.command.Command; +import org.apache.isis.applib.services.command.spi.CommandServiceListener; + +import lombok.extern.log4j.Log4j2; + +@Service +@Named("isisApplib.CommandService") +@Order(OrderPrecedence.MIDPOINT) +@Primary +@Qualifier("Default") +@Log4j2 +// tag::refguide[] +public class CommandService { + + // end::refguide[] + /** + * Simply instantiates the appropriate instance of the {@link Command}. + * + * <p> + * Its members will be populated automatically by the framework (the + * {@link Command}'s {@link Command#getTimestamp()}, + * {@link Command#getUsername()} and {@link Command#getUniqueId()}). + * </p> + */ + // tag::refguide[] + public Command create() { // <.> + return new Command(); + } + + // end::refguide[] + + /** + * "Complete" the command, providing an opportunity ot persist + * a memento of the command if the + * {@link Command#isSystemStateChanged() system state has changed}. + * + * <p> + * The framework will automatically have set the {@link Command#getCompletedAt()} property. + * </p> + */ + // tag::refguide[] + public void complete(final Command command) { // <.> + // ... + // end::refguide[] + + if(command.getLogicalMemberIdentifier() == null) { + // eg if seed fixtures + return; + } + + log.debug("complete: {}, systemStateChanged {}", command.getLogicalMemberIdentifier(), command.isSystemStateChanged()); + + // tag::refguide[] + commandServiceListeners.forEach(x -> x.onComplete(command)); + } + + @Inject List<CommandServiceListener> commandServiceListeners; + +} +// end::refguide[] diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandWithDto.java b/api/applib/src/main/java/org/apache/isis/applib/services/command/spi/CommandServiceListener.java similarity index 63% rename from api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandWithDto.java rename to api/applib/src/main/java/org/apache/isis/applib/services/command/spi/CommandServiceListener.java index cd421ea..5d10793 100644 --- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandWithDto.java +++ b/api/applib/src/main/java/org/apache/isis/applib/services/command/spi/CommandServiceListener.java @@ -16,20 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.isis.applib.services.command; +package org.apache.isis.applib.services.command.spi; -import org.apache.isis.applib.annotation.Programmatic; -import org.apache.isis.schema.cmd.v2.CommandDto; +import org.apache.isis.applib.services.command.Command; +/** + * SPI + */ // tag::refguide[] -public interface CommandWithDto extends Command { - - String USERDATA_KEY_TARGET_CLASS = "targetClass"; - String USERDATA_KEY_TARGET_ACTION = "targetAction"; - String USERDATA_KEY_ARGUMENTS = "arguments"; - String USERDATA_KEY_RETURN_VALUE = "returnValue"; - String USERDATA_KEY_EXCEPTION = "exception"; +public interface CommandServiceListener { - CommandDto asDto(); + /** + * Notifies that the command has completed. + * + * <p> + * This is an opportunity for implementations to process the command, + * for example to persist a representation of it. + * </p> + */ + // tag::refguide[] + void onComplete(final Command command); // <.> } // end::refguide[] diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandServiceDefault.java b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/CommandServiceListenerForJdo.java similarity index 51% rename from core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandServiceDefault.java rename to extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/CommandServiceListenerForJdo.java index 527692f..f3a4d81 100644 --- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandServiceDefault.java +++ b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/CommandServiceListenerForJdo.java @@ -16,39 +16,53 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.isis.core.runtimeservices.command; +package org.apache.isis.extensions.commandlog.impl; +import javax.inject.Inject; import javax.inject.Named; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import org.apache.isis.applib.annotation.OrderPrecedence; import org.apache.isis.applib.services.command.Command; -import org.apache.isis.applib.services.command.spi.CommandService; +import org.apache.isis.applib.services.command.spi.CommandServiceListener; +import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo; +import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdoRepository; + +import lombok.RequiredArgsConstructor; +import lombok.val; +import lombok.extern.log4j.Log4j2; @Service -@Named("isisRuntimeServices.CommandServiceDefault") -@Order(OrderPrecedence.LATE) -@Primary -@Qualifier("Default") -public class CommandServiceDefault implements CommandService { +@Named("isisExtensionsCommandLog.CommandCompletionHook") +@Order(OrderPrecedence.MIDPOINT) // after JdoPersistenceLifecycleService +@Qualifier("Jdo") +@Log4j2 +@RequiredArgsConstructor +public class CommandServiceListenerForJdo implements CommandServiceListener { - @Override - public Command create() { - return new CommandDefault(); - } + @Inject final CommandJdoRepository commandJdoRepository; @Override - public void complete(final Command command) { - // nothing to do - } + public void onComplete(Command command) { - @Override - public boolean persistIfPossible(final Command command) { - return false; + if(!command.isSystemStateChanged()) { + return; + } + + val commandJdo = new CommandJdo(command); + val parent = command.getParent(); + val parentJdo = + parent != null + ? commandJdoRepository + .findByUniqueId(parent.getUniqueId()) + .orElse(null) + : null; + commandJdo.setParent(parentJdo); + + commandJdoRepository.persist(commandJdo); } } diff --git a/extensions/core/command-log/impl/src/test/java/org/apache/isis/extensions/commandlog/impl/util/StringUtils_trimmed_Test.java b/extensions/core/command-log/impl/src/test/java/org/apache/isis/extensions/commandlog/impl/util/StringUtils_trimmed_Test.java new file mode 100644 index 0000000..b991b25 --- /dev/null +++ b/extensions/core/command-log/impl/src/test/java/org/apache/isis/extensions/commandlog/impl/util/StringUtils_trimmed_Test.java @@ -0,0 +1,28 @@ +package org.apache.isis.extensions.commandlog.impl.util; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class StringUtils_trimmed_Test { + + @Test + public void fits() { + Assertions.assertThat(StringUtils.trimmed("abcde", 5)).isEqualTo("abcde"); + } + + @Test + public void needs_to_be_trimmed() { + Assertions.assertThat(StringUtils.trimmed("abcde", 4)).isEqualTo("a..."); + } + + @Test + public void when_null() { + Assertions.assertThat(StringUtils.trimmed(null, 4)).isNull(); + } + + @Test + public void when_empty() { + Assertions.assertThat(StringUtils.trimmed("", 4)).isEqualTo(""); + } + +} \ No newline at end of file diff --git a/extensions/core/command-replay/impl/src/test/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalysisService_trimmed_Test.java b/extensions/core/command-replay/impl/src/test/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalysisService_trimmed_Test.java deleted file mode 100644 index babe14e..0000000 --- a/extensions/core/command-replay/impl/src/test/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalysisService_trimmed_Test.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.apache.isis.extensions.commandreplay.impl.analysis; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -import org.apache.isis.extensions.commandreplay.impl.analysis.CommandReplayAnalysisService; - -public class CommandReplayAnalysisService_trimmed_Test { - - @Test - public void fits() { - Assertions.assertThat(CommandReplayAnalysisService.trimmed("abcde", 5)).isEqualTo("abcde"); - } - - @Test - public void needs_to_be_trimmed() { - Assertions.assertThat(CommandReplayAnalysisService.trimmed("abcde", 4)).isEqualTo("a..."); - } - - @Test - public void when_null() { - Assertions.assertThat(CommandReplayAnalysisService.trimmed(null, 4)).isNull(); - } - - @Test - public void when_empty() { - Assertions.assertThat(CommandReplayAnalysisService.trimmed("", 4)).isEqualTo(""); - } - -} \ No newline at end of file
