You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2021/09/09 06:27:39 UTC

[isis] branch ISIS-2735-command-log updated: ISIS-2735 : wip on Command and DomainChangeRecord

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

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


The following commit(s) were added to refs/heads/ISIS-2735-command-log by this push:
     new c87899e  ISIS-2735 : wip on Command and DomainChangeRecord
c87899e is described below

commit c87899e95229994927632bb61ff4655cc36f48bf
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 9 07:27:26 2021 +0100

    ISIS-2735 : wip on Command and DomainChangeRecord
---
 .../applib/mixins/system/DomainChangeRecord.java   | 225 +++++++++---
 .../extensions/commandlog/applib/dom/Command.java  | 384 +++++++++++++++++----
 2 files changed, 490 insertions(+), 119 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/mixins/system/DomainChangeRecord.java b/api/applib/src/main/java/org/apache/isis/applib/mixins/system/DomainChangeRecord.java
index c1dfe15..cdb985b 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/mixins/system/DomainChangeRecord.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/mixins/system/DomainChangeRecord.java
@@ -45,10 +45,26 @@ import org.apache.isis.applib.services.bookmark.Bookmark;
  */
 public interface DomainChangeRecord extends HasInteractionId, HasUsername {
 
+    /**
+     * Enumerates the different types of changes recognised.
+     *
+     * @since 2.0 {@index}
+     */
+    enum ChangeType {
+        COMMAND,
+        AUDIT_ENTRY,
+        PUBLISHED_INTERACTION;
+        @Override
+        public String toString() {
+            return name().replace("_", " ");
+        }
+    }
+
     @Property(
             domainEvent = ChangeTypeMeta.DomainEvent.class,
             editing = Editing.DISABLED,
-            maxLength = ChangeTypeMeta.MAX_LENGTH
+            maxLength = ChangeTypeMeta.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
     @PropertyLayout(
             hidden = Where.ALL_EXCEPT_STANDALONE_TABLES,
@@ -57,7 +73,8 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
             typicalLength = ChangeTypeMeta.TYPICAL_LENGTH
     )
     @Parameter(
-            maxLength = ChangeTypeMeta.MAX_LENGTH
+            maxLength = ChangeTypeMeta.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
     @ParameterLayout(
             named = "Change Type",
@@ -73,21 +90,6 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
     }
 
     /**
-     * Enumerates the different types of changes recognised.
-     *
-     * @since 2.0 {@index}
-     */
-    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).
      */
     @ChangeTypeMeta
@@ -98,7 +100,8 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
     @Property(
             domainEvent = InteractionId.DomainEvent.class,
             editing = Editing.DISABLED,
-            maxLength = InteractionId.MAX_LENGTH
+            maxLength = InteractionId.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
     @PropertyLayout(
             fieldSetId = "Identifiers",
@@ -106,7 +109,8 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
             typicalLength = InteractionId.TYPICAL_LENGTH
     )
     @Parameter(
-            maxLength = InteractionId.MAX_LENGTH
+            maxLength = InteractionId.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
     @ParameterLayout(
             typicalLength = InteractionId.TYPICAL_LENGTH
@@ -120,7 +124,6 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
         class DomainEvent extends PropertyDomainEvent<DomainChangeRecord, UUID> { }
     }
 
-
     /**
      * The unique identifier (a GUID) of the
      * {@link org.apache.isis.applib.services.iactn.Interaction} within which
@@ -135,7 +138,8 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
     @Property(
             domainEvent = Username.DomainEvent.class,
             editing = Editing.DISABLED,
-            maxLength = Username.MAX_LENGTH
+            maxLength = Username.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
     @PropertyLayout(
             fieldSetId="Identifiers",
@@ -144,7 +148,8 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
             hidden = Where.PARENTED_TABLES
     )
     @Parameter(
-            maxLength = Username.MAX_LENGTH
+            maxLength = Username.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
     @ParameterLayout(
             named = "Username",
@@ -168,25 +173,27 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
 
 
     @Property(
-            domainEvent = DomainChangeRecord.Timestamp.DomainEvent.class,
+            domainEvent = TimestampMeta.DomainEvent.class,
             editing = Editing.DISABLED,
-            maxLength = Timestamp.MAX_LENGTH
+            maxLength = TimestampMeta.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
     @PropertyLayout(
             fieldSetId="Identifiers",
             sequence = "20",
-            typicalLength = Timestamp.TYPICAL_LENGTH
+            typicalLength = TimestampMeta.TYPICAL_LENGTH
     )
     @Parameter(
-            maxLength = Timestamp.MAX_LENGTH
+            maxLength = TimestampMeta.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
     @ParameterLayout(
             named = "Timestamp",
-            typicalLength = Timestamp.TYPICAL_LENGTH
+            typicalLength = TimestampMeta.TYPICAL_LENGTH
     )
     @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
     @Retention(RetentionPolicy.RUNTIME)
-    @interface Timestamp {
+    @interface TimestampMeta {
         int MAX_LENGTH = 32;
         int TYPICAL_LENGTH = 32;
 
@@ -196,35 +203,108 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
     /**
      * The time that the change occurred.
      */
-    @DomainChangeRecord.Timestamp
+    @DomainChangeRecord.TimestampMeta
     java.sql.Timestamp getTimestamp();
 
 
-    /**
-     * The object type of the domain object being changed.
-     */
-    @Property
+
+    @Property(
+            domainEvent = LogicalTypeName.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = LogicalTypeName.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
+    )
     @PropertyLayout(
-            named="Object Type",
+            named="Logical Type Name",
             fieldSetId="Target",
-            sequence = "10")
-    default String getTargetObjectType() {
+            sequence = "10",
+            typicalLength = LogicalTypeName.TYPICAL_LENGTH
+    )
+    @Parameter(
+            maxLength = LogicalTypeName.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
+    )
+    @ParameterLayout(
+            typicalLength = LogicalTypeName.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface LogicalTypeName {
+        int MAX_LENGTH = 1024;
+        int TYPICAL_LENGTH = 128;
+
+        class DomainEvent extends PropertyDomainEvent<DomainChangeRecord, Bookmark> { }
+    }
+
+    /**
+     * The logical type of the domain object being changed.
+     */
+    @LogicalTypeName
+    default String getTargetLogicalTypeName() {
         return getTarget().getLogicalTypeName();
     }
 
 
 
+    @Property(
+            domainEvent = TargetMeta.DomainEvent.class,
+            editing = Editing.DISABLED,
+            optionality = Optionality.MANDATORY
+    )
+    @PropertyLayout(
+            named = "Object",
+            fieldSetId = "Target",
+            sequence = "30"
+    )
+    @Parameter(
+            optionality = Optionality.MANDATORY
+    )
+    @ParameterLayout(
+            named = "Object"
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface TargetMeta {
+        class DomainEvent extends PropertyDomainEvent<DomainChangeRecord, Bookmark> { }
+    }
+
     /**
      * The {@link Bookmark} identifying the domain object that has changed.
      */
-    @Property
-    @PropertyLayout(
-            named="Object",
-            fieldSetId="Target",
-            sequence="30")
+    @DomainChangeRecord.TargetMeta
     Bookmark getTarget();
 
 
+
+    @Property(
+            domainEvent = TargetMember.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = TargetMember.MAX_LENGTH,
+            optionality = Optionality.OPTIONAL
+    )
+    @PropertyLayout(
+            fieldSetId="Target",
+            hidden = Where.ALL_EXCEPT_STANDALONE_TABLES,
+            named = "Member",
+            sequence = "20",
+            typicalLength = TargetMember.TYPICAL_LENGTH
+    )
+    @Parameter(
+            maxLength = TargetMember.MAX_LENGTH,
+            optionality = Optionality.OPTIONAL
+    )
+    @ParameterLayout(
+            named="Member",
+            typicalLength = TargetMember.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface TargetMember {
+        int MAX_LENGTH = 255;
+        int TYPICAL_LENGTH = 30;
+        class DomainEvent extends PropertyDomainEvent<DomainChangeRecord, String> { }
+    }
+
     /**
      * The member interaction (ie action invocation or property edit) which caused the domain object to be changed.
      *
@@ -232,11 +312,36 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
      *     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, fieldSetId="Target", sequence = "20")
+    @DomainChangeRecord.TargetMember
     String getTargetMember();
 
 
+    @Property(
+            domainEvent = PreValue.DomainEvent.class,
+            editing = Editing.DISABLED,
+            optionality = Optionality.OPTIONAL
+    )
+    @PropertyLayout(
+            hidden = Where.ALL_EXCEPT_STANDALONE_TABLES,
+            fieldSetId = "Detail",
+            sequence = "6",
+            typicalLength = PreValue.TYPICAL_LENGTH
+    )
+    @Parameter(
+            maxLength = PreValue.MAX_LENGTH,
+            optionality = Optionality.OPTIONAL
+    )
+    @ParameterLayout(
+            typicalLength = PreValue.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface PreValue {
+        int MAX_LENGTH = 1024;
+        int TYPICAL_LENGTH = 30;
+        class DomainEvent extends PropertyDomainEvent<DomainChangeRecord, String> { }
+    }
+
     /**
      * The value of the property prior to it being changed.
      *
@@ -244,11 +349,37 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
      * Populated only for audit entries.
      * </p>
      */
-    @Property(optionality = Optionality.OPTIONAL)
-    @PropertyLayout(hidden = Where.ALL_EXCEPT_STANDALONE_TABLES, fieldSetId="Detail",sequence = "6")
+    @PreValue
     String getPreValue();
 
 
+
+    @Property(
+            domainEvent = PostValue.DomainEvent.class,
+            editing = Editing.DISABLED,
+            optionality = Optionality.OPTIONAL
+    )
+    @PropertyLayout(
+            hidden = Where.ALL_EXCEPT_STANDALONE_TABLES,
+            fieldSetId = "Detail",
+            sequence = "7",
+            typicalLength = PostValue.TYPICAL_LENGTH
+    )
+    @Parameter(
+            maxLength = PostValue.MAX_LENGTH,
+            optionality = Optionality.OPTIONAL
+    )
+    @ParameterLayout(
+            typicalLength = PostValue.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface PostValue {
+        int MAX_LENGTH = 1024;
+        int TYPICAL_LENGTH = 30;
+        class DomainEvent extends PropertyDomainEvent<DomainChangeRecord, String> { }
+    }
+
     /**
      * The value of the property after it has changed.
      *
@@ -256,9 +387,7 @@ public interface DomainChangeRecord extends HasInteractionId, HasUsername {
      * Populated only for audit entries.
      * </p>
      */
-    @Property(optionality = Optionality.MANDATORY)
-    @PropertyLayout(hidden = Where.ALL_EXCEPT_STANDALONE_TABLES, fieldSetId="Detail",
-    sequence = "7")
+    @PostValue
     String getPostValue();
 
 
diff --git a/extensions/core/command-log/applib/src/main/java/org/apache/isis/extensions/commandlog/applib/dom/Command.java b/extensions/core/command-log/applib/src/main/java/org/apache/isis/extensions/commandlog/applib/dom/Command.java
index 8f73798..27b606f 100644
--- a/extensions/core/command-log/applib/src/main/java/org/apache/isis/extensions/commandlog/applib/dom/Command.java
+++ b/extensions/core/command-log/applib/src/main/java/org/apache/isis/extensions/commandlog/applib/dom/Command.java
@@ -32,16 +32,21 @@ import java.util.Optional;
 import java.util.UUID;
 import java.util.function.Consumer;
 
+import javax.annotation.Nullable;
+
 import org.springframework.context.event.EventListener;
 import org.springframework.stereotype.Service;
 
+import org.apache.isis.applib.annotation.Domain;
 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.MemberSupport;
+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.PriorityPrecedence;
+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.Where;
@@ -62,6 +67,7 @@ import org.apache.isis.extensions.commandlog.applib.util.StringUtils;
 import org.apache.isis.schema.cmd.v2.CommandDto;
 import org.apache.isis.schema.cmd.v2.MapDto;
 
+import lombok.NonNull;
 import lombok.val;
 
 @DomainObject(
@@ -78,18 +84,18 @@ public abstract class Command implements DomainChangeRecord {
 
     public final static String LOGICAL_TYPE_NAME = IsisModuleExtCommandLogApplib.NAMESPACE + ".Command";
 
-    public static class TitleUiEvent extends IsisModuleExtCommandLogApplib.TitleUiEvent<CommandModel> { }
-    public static class IconUiEvent extends IsisModuleExtCommandLogApplib.IconUiEvent<CommandModel> { }
-    public static class CssClassUiEvent extends IsisModuleExtCommandLogApplib.CssClassUiEvent<CommandModel> { }
-    public static class LayoutUiEvent extends IsisModuleExtCommandLogApplib.LayoutUiEvent<CommandModel> { }
+    public static class TitleUiEvent extends IsisModuleExtCommandLogApplib.TitleUiEvent<Command> { }
+    public static class IconUiEvent extends IsisModuleExtCommandLogApplib.IconUiEvent<Command> { }
+    public static class CssClassUiEvent extends IsisModuleExtCommandLogApplib.CssClassUiEvent<Command> { }
+    public static class LayoutUiEvent extends IsisModuleExtCommandLogApplib.LayoutUiEvent<Command> { }
 
-    public static abstract class PropertyDomainEvent<T> extends IsisModuleExtCommandLogApplib.PropertyDomainEvent<CommandModel, T> { }
-    public static abstract class CollectionDomainEvent<T> extends IsisModuleExtCommandLogApplib.CollectionDomainEvent<CommandModel, T> { }
+    public static abstract class PropertyDomainEvent<T> extends IsisModuleExtCommandLogApplib.PropertyDomainEvent<Command, T> { }
+    public static abstract class CollectionDomainEvent<T> extends IsisModuleExtCommandLogApplib.CollectionDomainEvent<Command, T> { }
 
 
     public Command(final org.apache.isis.applib.services.command.Command command) {
 
-        setInteractionIdStr(command.getInteractionId().toString());
+        setInteractionId(command.getInteractionId());
         setUsername(command.getUsername());
         setTimestamp(command.getTimestamp());
 
@@ -119,7 +125,7 @@ public abstract class Command implements DomainChangeRecord {
             final ReplayState replayState,
             final int targetIndex) {
 
-        setInteractionIdStr(commandDto.getInteractionId());
+        setInteractionId(UUID.fromString(commandDto.getInteractionId()));
         setUsername(commandDto.getUser());
         setTimestamp(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(commandDto.getTimestamp()));
 
@@ -156,15 +162,18 @@ public abstract class Command implements DomainChangeRecord {
     public static class TitleProvider {
 
         @EventListener(TitleUiEvent.class)
-        public void on(final TitleUiEvent ev) {
+        public void on(final @NonNull TitleUiEvent ev) {
             if(!Objects.equals(ev.getTitle(), "Command Jdo") || ev.getTranslatableTitle() != null) {
                 return;
             }
-            ev.setTitle(title((Command)ev.getSource()));
+            ev.setTitle(title(ev.getSource()));
         }
 
-        private static String title(final Command source) {
-            // nb: not thread-safe
+        private static String title(final @Nullable Command source) {
+            if(source == null) {
+                return "???"; // shouldn't happen.
+            }
+            // nb: not thread-safe, so need to instantiate each time.
             // formats defined in https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
             val format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
 
@@ -195,7 +204,7 @@ public abstract class Command implements DomainChangeRecord {
     public abstract void setUsername(String username);
 
 
-    @DomainChangeRecord.Timestamp
+    @DomainChangeRecord.TimestampMeta
     @Override
     public abstract java.sql.Timestamp getTimestamp();
     public abstract void setTimestamp(java.sql.Timestamp timestamp);
@@ -203,25 +212,28 @@ public abstract class Command implements DomainChangeRecord {
 
 
     @Property(
-            domainEvent = ReplayState.DomainEvent.class,
+            domainEvent = ReplayStateMeta.DomainEvent.class,
             editing = Editing.DISABLED,
-            maxLength = ReplayState.MAX_LENGTH
+            maxLength = ReplayStateMeta.MAX_LENGTH,
+            optionality = Optionality.OPTIONAL
     )
     @PropertyLayout(
             // fieldSetId = "XXX",  // TODO: fix
             // sequence = "XXX",
-            typicalLength = ReplayState.TYPICAL_LENGTH
+            named = "Replay State",
+            typicalLength = ReplayStateMeta.TYPICAL_LENGTH
     )
     @Parameter(
-            maxLength = ReplayState.MAX_LENGTH
+            maxLength = ReplayStateMeta.MAX_LENGTH,
+            optionality = Optionality.OPTIONAL
     )
     @ParameterLayout(
             named = "Replay State",
-            typicalLength = ReplayState.TYPICAL_LENGTH
+            typicalLength = ReplayStateMeta.TYPICAL_LENGTH
     )
     @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
     @Retention(RetentionPolicy.RUNTIME)
-    public @interface ReplayState {
+    public @interface ReplayStateMeta {
         int MAX_LENGTH = 12;
         int TYPICAL_LENGTH = 12;
 
@@ -232,29 +244,38 @@ public abstract class Command implements DomainChangeRecord {
     /**
      * For a replayed command, what the outcome was.
      */
-    @Command.ReplayState
+    @Command.ReplayStateMeta
     public abstract org.apache.isis.extensions.commandlog.applib.dom.ReplayState getReplayState();
     public abstract void setReplayState(org.apache.isis.extensions.commandlog.applib.dom.ReplayState replayState);
 
 
 
     @Property(
-            domainEvent = ReplayStateFailureReason.DomainEvent.class
+            domainEvent = ReplayStateFailureReason.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = ReplayStateFailureReason.MAX_LENGTH,
+            optionality = Optionality.OPTIONAL
     )
     @PropertyLayout(
             hidden = Where.ALL_TABLES,
-            multiLine = ReplayStateFailureReason.MULTILINE
+            multiLine = ReplayStateFailureReason.MULTI_LINE,
+            typicalLength = ReplayStateFailureReason.TYPICAL_LENGTH
     )
     @Parameter(
+            maxLength = ReplayStateFailureReason.MAX_LENGTH,
+            optionality = Optionality.OPTIONAL
     )
     @ParameterLayout(
-            multiLine = ReplayStateFailureReason.MULTILINE
+            multiLine = ReplayStateFailureReason.MULTI_LINE,
+            typicalLength = ReplayStateFailureReason.TYPICAL_LENGTH
     )
     @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
     @Retention(RetentionPolicy.RUNTIME)
     public @interface ReplayStateFailureReason {
+        int MAX_LENGTH = Integer.MAX_VALUE;
+        int TYPICAL_LENGTH = 4000;
 
-        int MULTILINE = 5;
+        int MULTI_LINE = 5;
         class DomainEvent extends PropertyDomainEvent<String> {}
     }
 
@@ -272,122 +293,288 @@ public abstract class Command implements DomainChangeRecord {
 
 
 
-    public static class ParentDomainEvent extends PropertyDomainEvent<org.apache.isis.applib.services.command.Command> { }
     @Property(
-            domainEvent = ParentDomainEvent.class
+            domainEvent = Parent.DomainEvent.class
     )
     @PropertyLayout(
             hidden = Where.ALL_TABLES
     )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface Parent {
+        class DomainEvent extends PropertyDomainEvent<org.apache.isis.applib.services.command.Command> { }
+    }
+
+    @Parent
     public abstract Command getParent();
     public abstract void setParent(Command parent);
 
 
 
-    public static class TargetDomainEvent extends PropertyDomainEvent<Bookmark> { }
-    @Property(
-            domainEvent = TargetDomainEvent.class
-    )
-    @PropertyLayout(
-            named = "Object"
-    )
+    @DomainChangeRecord.TargetMeta
     public abstract Bookmark getTarget();
     public abstract void setTarget(Bookmark target);
 
+
+
+    @Domain.Exclude
     public String getTargetStr() {
         return Optional.ofNullable(getTarget()).map(Bookmark::toString).orElse(null);
     }
 
+
+
+    @DomainChangeRecord.TargetMember
     @Override
     public String getTargetMember() {
         return getCommandDto().getMember().getLogicalMemberIdentifier();
     }
 
 
-    public static class LocalMemberEvent extends PropertyDomainEvent<String> { }
+
     @Property(
-            domainEvent = LocalMemberEvent.class
+            domainEvent = LocalMember.DomainEvent.class,
+            maxLength = LocalMember.MAX_LENGTH
     )
     @PropertyLayout(
-            named = "Member"
+            named = "Member",
+            typicalLength = LocalMember.TYPICAL_LENGTH
     )
+    @Parameter(
+            maxLength = LocalMember.MAX_LENGTH
+    )
+    @ParameterLayout(
+            typicalLength = LocalMember.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface LocalMember {
+        int MAX_LENGTH = 255;
+        int TYPICAL_LENGTH = 30;
+
+        class DomainEvent extends PropertyDomainEvent<String> { }
+    }
+
+    @LocalMember
     public String getLocalMember() {
         val targetMember = getTargetMember();
         return targetMember.substring(targetMember.indexOf("#") + 1);
     }
 
 
-    public static class LogicalMemberIdentifierDomainEvent extends PropertyDomainEvent<String> { }
+
     @Property(
-            domainEvent = LogicalMemberIdentifierDomainEvent.class
+            domainEvent = LogicalMemberIdentifier.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = LogicalMemberIdentifier.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
     @PropertyLayout(
-            hidden = Where.ALL_TABLES
+            hidden = Where.ALL_TABLES,
+            typicalLength = LogicalMemberIdentifier.TYPICAL_LENGTH
     )
+    @Parameter(
+            maxLength = LogicalMemberIdentifier.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
+    )
+    @ParameterLayout(
+            typicalLength = LogicalMemberIdentifier.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface LogicalMemberIdentifier {
+        int MAX_LENGTH = 1024;
+        int TYPICAL_LENGTH = 128;
+
+        class DomainEvent extends PropertyDomainEvent<String> { }
+    }
+
+    @LogicalMemberIdentifier
     public abstract String getLogicalMemberIdentifier();
     public abstract void setLogicalMemberIdentifier(String logicalMemberIdentifier);
 
 
-    public static class CommandDtoDomainEvent extends PropertyDomainEvent<CommandDto> { }
+
+
     @Property(
-            domainEvent = CommandDtoDomainEvent.class
+            domainEvent = CommandDtoMeta.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = CommandDtoMeta.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
     @PropertyLayout(
-            multiLine = 9
+            multiLine = CommandDtoMeta.MULTI_LINE,
+            typicalLength = CommandDtoMeta.TYPICAL_LENGTH
+    )
+    @Parameter(
+            maxLength = CommandDtoMeta.MAX_LENGTH,
+            optionality = Optionality.MANDATORY
     )
+    @ParameterLayout(
+            multiLine = CommandDtoMeta.MULTI_LINE,
+            typicalLength = CommandDtoMeta.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface CommandDtoMeta {
+        int MAX_LENGTH = Integer.MAX_VALUE;
+        int TYPICAL_LENGTH = 4000;
+
+        int MULTI_LINE = 9;
+        class DomainEvent extends PropertyDomainEvent<CommandDto> { }
+    }
+
+    @CommandDtoMeta
     public abstract CommandDto getCommandDto();
     public abstract void setCommandDto(CommandDto commandDto);
 
 
 
-    public static class StartedAtDomainEvent extends PropertyDomainEvent<Timestamp> { }
+
     @Property(
-            domainEvent = StartedAtDomainEvent.class
+            domainEvent = StartedAt.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = StartedAt.MAX_LENGTH
+    )
+    @PropertyLayout(
+            typicalLength = StartedAt.TYPICAL_LENGTH
+    )
+    @Parameter(
+            maxLength = StartedAt.MAX_LENGTH
+    )
+    @ParameterLayout(
+            typicalLength = StartedAt.TYPICAL_LENGTH
     )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface StartedAt {
+        int MAX_LENGTH = 32;
+        int TYPICAL_LENGTH = 32;
+
+        class DomainEvent extends PropertyDomainEvent<Timestamp> { }
+    }
+
+    @StartedAt
     public abstract Timestamp getStartedAt();
     public abstract void setStartedAt(Timestamp startedAt);
 
 
-    public static class CompletedAtDomainEvent extends PropertyDomainEvent<Timestamp> { }
+
     @Property(
-            domainEvent = CompletedAtDomainEvent.class
+            domainEvent = CompletedAt.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = CompletedAt.MAX_LENGTH
+    )
+    @PropertyLayout(
+            typicalLength = CompletedAt.TYPICAL_LENGTH
     )
+    @Parameter(
+            maxLength = CompletedAt.MAX_LENGTH
+    )
+    @ParameterLayout(
+            typicalLength = CompletedAt.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface CompletedAt {
+        int MAX_LENGTH = 32;
+        int TYPICAL_LENGTH = 32;
+
+        class DomainEvent extends PropertyDomainEvent<Timestamp> { }
+    }
+
+    @CompletedAt
     public abstract Timestamp getCompletedAt();
     public abstract void setCompletedAt(Timestamp completedAt);
 
 
-    public static class DurationDomainEvent extends PropertyDomainEvent<BigDecimal> { }
+
+    @javax.validation.constraints.Digits(
+            integer = Duration.DIGITS_INTEGER,
+            fraction = Duration.DIGITS_FRACTION
+    )
+    @Property(
+            domainEvent = Duration.DomainEvent.class,
+            maxLength = Duration.MAX_LENGTH
+    )
+    @PropertyLayout(
+            typicalLength = Duration.TYPICAL_LENGTH
+    )
+    @Parameter(
+            maxLength = Duration.MAX_LENGTH
+    )
+    @ParameterLayout(
+            typicalLength = Duration.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface Duration {
+        int DIGITS_INTEGER = 5;
+        int DIGITS_FRACTION = 3;
+        int MAX_LENGTH = DIGITS_INTEGER + DIGITS_FRACTION + 1;
+        int TYPICAL_LENGTH = 5;
+
+        class DomainEvent 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
-    )
+    @Duration
     public BigDecimal getDuration() {
         return BigDecimalUtils.durationBetween(getStartedAt(), getCompletedAt());
     }
 
 
-    public static class IsCompleteDomainEvent extends PropertyDomainEvent<Boolean> { }
+
     @Property(
-            domainEvent = IsCompleteDomainEvent.class
+            domainEvent = IsComplete.DomainEvent.class,
+            editing = Editing.DISABLED
     )
     @PropertyLayout(
             hidden = Where.OBJECT_FORMS
     )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface IsComplete {
+        class DomainEvent extends PropertyDomainEvent<Boolean> { }
+    }
+
+    @IsComplete
     public boolean isComplete() {
         return getCompletedAt() != null;
     }
 
 
 
-    public static class ResultSummaryDomainEvent extends PropertyDomainEvent<String> { }
-    @Property(domainEvent = ResultSummaryDomainEvent.class)
-    @PropertyLayout(hidden = Where.OBJECT_FORMS, named = "Result")
+    @Property(
+            domainEvent = ResultSummary.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = ResultSummary.MAX_LENGTH
+    )
+    @PropertyLayout(
+            hidden = Where.OBJECT_FORMS,
+            named = "Result",
+            typicalLength = ResultSummary.TYPICAL_LENGTH
+    )
+    @Parameter(
+            maxLength = ResultSummary.MAX_LENGTH
+    )
+    @ParameterLayout(
+            typicalLength = ResultSummary.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface ResultSummary {
+        int MAX_LENGTH = 20;
+        int TYPICAL_LENGTH = 12;
+        class DomainEvent extends PropertyDomainEvent<String> { }
+    }
+
+    @ResultSummary
     public String getResultSummary() {
         if(getCompletedAt() == null) {
             return "";
@@ -403,19 +590,62 @@ public abstract class Command implements DomainChangeRecord {
     }
 
 
-    public static class ResultDomainEvent extends PropertyDomainEvent<String> { }
+
     @Property(
-            domainEvent = ResultDomainEvent.class
+            domainEvent = ResultMeta.DomainEvent.class,
+            editing = Editing.DISABLED,
+            optionality = Optionality.OPTIONAL
     )
     @PropertyLayout(
             hidden = Where.ALL_TABLES,
-            named = "Result Bookmark"
+            named = "Result"
     )
+    @Parameter(
+            optionality = Optionality.OPTIONAL
+    )
+    @ParameterLayout(
+            named = "Result"
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface ResultMeta {
+        class DomainEvent extends PropertyDomainEvent<Bookmark> { }
+    }
+
+    @ResultMeta
     public abstract Bookmark getResult();
     public abstract void setResult(Bookmark result);
 
 
-    public static class ExceptionDomainEvent extends PropertyDomainEvent<String> { }
+
+    @Property(
+            domainEvent = ExceptionMeta.DomainEvent.class,
+            editing = Editing.DISABLED,
+            maxLength = ExceptionMeta.MAX_LENGTH
+    )
+    @PropertyLayout(
+            hidden = Where.ALL_TABLES,
+            multiLine = ExceptionMeta.MULTI_LINE,
+            named = "Exception (if any)",
+            typicalLength = ExceptionMeta.TYPICAL_LENGTH
+    )
+    @Parameter(
+            maxLength = ExceptionMeta.MAX_LENGTH
+    )
+    @ParameterLayout(
+            multiLine = ExceptionMeta.MULTI_LINE,
+            typicalLength = ExceptionMeta.TYPICAL_LENGTH
+    )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface ExceptionMeta {
+        int MAX_LENGTH = Integer.MAX_VALUE;
+        int TYPICAL_LENGTH = 4000;
+        int MULTI_LINE = 5;
+
+        class DomainEvent extends PropertyDomainEvent<String> { }
+    }
+
     /**
      * Stack trace of any exception that might have occurred if this interaction/transaction aborted.
      *
@@ -423,13 +653,6 @@ public abstract class Command implements DomainChangeRecord {
      * 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.
      */
-    @Property(
-            domainEvent = ExceptionDomainEvent.class
-    )
-    @PropertyLayout(
-            hidden = Where.ALL_TABLES,
-            multiLine = 5, named = "Exception (if any)"
-    )
     public abstract String getException();
     public abstract void setException(final String exception);
 
@@ -438,29 +661,42 @@ public abstract class Command implements DomainChangeRecord {
     }
 
 
-    public static class IsCausedExceptionDomainEvent extends PropertyDomainEvent<Boolean> { }
+
     @Property(
-            domainEvent = IsCausedExceptionDomainEvent.class
+            domainEvent = CausedException.DomainEvent.class,
+            editing = Editing.DISABLED
     )
     @PropertyLayout(
             hidden = Where.OBJECT_FORMS
     )
+    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface CausedException {
+        class DomainEvent extends PropertyDomainEvent<Boolean> { }
+    }
+
+    @CausedException
     public boolean isCausedException() {
         return getException() != null;
     }
 
 
+
+    @DomainChangeRecord.PreValue
     @Override
     public String getPreValue() {
         return null;
     }
 
+
+    @DomainChangeRecord.PostValue
     @Override
     public String getPostValue() {
         return null;
     }
 
 
+    @Programmatic
     public void saveAnalysis(final String analysis) {
         if (analysis == null) {
             setReplayState(ReplayState.OK);
@@ -509,7 +745,6 @@ public abstract class Command implements DomainChangeRecord {
                 Command.this.setResult(resultBookmark.getValue().orElse(null));
                 Command.this.setException(resultBookmark.getFailure().orElse(null));
             }
-
         };
     }
 
@@ -531,9 +766,16 @@ public abstract class Command implements DomainChangeRecord {
 
         private List<String> ordered(final List<String> propertyIds) {
             return Arrays.asList(
-                "timestamp", "target", "targetMember", "username", "complete", "resultSummary", "interactionIdStr"
+                "timestamp",
+                    "target",
+                    "targetMember",
+                    "username",
+                    "complete",
+                    "resultSummary",
+                    "interactionId"
             );
         }
     }
+
 }