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

ahuber pushed a commit to branch v3
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/v3 by this push:
     new b832e0a6b31 CAUSEWAY-3982: extend auditing to only log on updates, not 
create or delete (cherrypicked from v4)
b832e0a6b31 is described below

commit b832e0a6b3188055a2598705f7fcc44f863793dd
Author: andi-huber <[email protected]>
AuthorDate: Fri Mar 27 06:36:07 2026 +0100

    CAUSEWAY-3982: extend auditing to only log on updates, not create or
    delete (cherrypicked from v4)
---
 .../causeway/applib/annotation/Publishing.java     |  21 +-
 .../CommandPublishingFacetForActionAnnotation.java |  55 +-
 ...ommandPublishingFacetForPropertyAnnotation.java |  80 +-
 ...xecutionPublishingFacetForActionAnnotation.java |  46 +-
 ...cutionPublishingFacetForPropertyAnnotation.java |  82 +-
 ...gePublishingFacetForDomainObjectAnnotation.java |  35 +-
 ...FacetForDomainObjectAnnotationAsConfigured.java |   2 +-
 ...tityChangePublishingFacetFromConfiguration.java |   2 +-
 .../entitychange/EntityChangePublishingFacet.java  |  45 +-
 .../EntityChangePublishingFacetAbstract.java       |  12 +-
 .../EntityPropertyChangePublishingPolicyFacet.java |  11 +-
 ...PublishingPolicyFacetForPropertyAnnotation.java |   6 +-
 .../metamodel/facets/FacetFactoryTestAbstract.java | 484 +++++++++++
 .../DomainObjectAnnotationFacetFactoryTest.java    | 768 ++++++++++++++++++
 .../PropertyAnnotationFacetFactoryTest.java        | 885 +++++++++++++++++++++
 .../changetracking/EntityChangeTrackerDefault.java |  44 +-
 16 files changed, 2356 insertions(+), 222 deletions(-)

diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/annotation/Publishing.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/annotation/Publishing.java
index b0fcc085537..c6125513cdb 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/annotation/Publishing.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/annotation/Publishing.java
@@ -45,8 +45,8 @@ public enum Publishing {
      * Publishing of data triggered by interaction with this object
      * should be handled as per the default publishing policy
      * configured in <tt>application.properties</tt>.
-     * <p>
-     * If no publishing policy is configured, then publishing is disabled.
+     *
+     * <p>If no publishing policy is configured, then publishing is disabled.
      */
     AS_CONFIGURED,
 
@@ -55,6 +55,23 @@ public enum Publishing {
      */
     ENABLED,
 
+    /**
+     * Applies only to {@link EntityPropertyChangeSubscriber}, whereby events 
are published for modifications to the
+     * object, but no events are published for the initial creation of an 
object.
+     *
+     * <p>In the case of audit trail extension,
+     * this effectively suppresses all of the "[NEW] -> value" entries that 
are created for every property of the
+     * entity when it is being created, and also all of the "value -> 
[DELETED]" entries that are created for every property of the
+     * entity when it is being deleted.
+     *
+     * <p>This variant is intended only where the application code has enough 
traceability built into the domain
+     * (perhaps to provide visibility to the end-users) that the technical 
auditing is overkill.  It will also
+     * of course reduce the volume of auditing, so improves performance 
(likely both response times and throughput).
+     *
+     * <p>For other subscribers, behaviour is the same as {@link #ENABLED}.
+     */
+    ENABLED_FOR_UPDATES_ONLY,
+
     /**
      * Do <b>not</b> publish data triggered by interaction with this object
      * (even if otherwise configured to enable publishing).
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java
index c4ec4abb732..b7f35a5f8ae 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java
@@ -62,40 +62,33 @@ public static Optional<CommandPublishingFacet> create(
         var publishingPolicy = 
ActionConfigOptions.actionCommandPublishingPolicy(configuration);
 
         return actionsIfAny
-                .filter(action -> action.commandPublishing() != 
Publishing.NOT_SPECIFIED)
-                .map(action -> {
-                    Publishing publishing = action.commandPublishing();
+            .filter(action -> action.commandPublishing() != 
Publishing.NOT_SPECIFIED)
+            .map(action -> {
+                Publishing publishing = action.commandPublishing();
 
-                    final Class<? extends CommandDtoProcessor> processorClass 
= action.commandDtoProcessor();
-                    final CommandDtoProcessor processor = 
newProcessorElseNull(processorClass);
+                final Class<? extends CommandDtoProcessor> processorClass = 
action.commandDtoProcessor();
+                final CommandDtoProcessor processor = 
newProcessorElseNull(processorClass);
 
-                    if(processor != null) {
-                        publishing = Publishing.ENABLED;
-                    }
+                if(processor != null) {
+                    publishing = Publishing.ENABLED;
+                }
 
-                    switch (publishing) {
-                        case AS_CONFIGURED:
-                            switch (publishingPolicy) {
-                                case NONE:
-                                    return new 
CommandPublishingFacetForActionAnnotationAsConfigured.None(holder, 
servicesInjector);
-                                case IGNORE_QUERY_ONLY:
-                                case IGNORE_SAFE:
-                                    return Facets.hasSafeSemantics(holder)
-                                            ? new 
CommandPublishingFacetForActionAnnotationAsConfigured.IgnoreSafe(holder, 
servicesInjector)
-                                            : new 
CommandPublishingFacetForActionAnnotationAsConfigured.IgnoreSafeYetNot(holder, 
servicesInjector);
-                                case ALL:
-                                    return new 
CommandPublishingFacetForActionAnnotationAsConfigured.All(holder, 
servicesInjector);
-                                default:
-                                    throw new 
IllegalStateException(String.format("configured action.commandPublishing policy 
'%s' not recognised", publishingPolicy));
-                            }
-                        case DISABLED:
-                            return new 
CommandPublishingFacetForActionAnnotation.Disabled(processor, holder, 
servicesInjector);
-                        case ENABLED:
-                            return new 
CommandPublishingFacetForActionAnnotation.Enabled(processor, holder, 
servicesInjector);
-                        default:
-                            throw new 
IllegalStateException(String.format("@Action#commandPublishing '%s' not 
recognised", publishing));
-                    }
-                });
+                return switch (publishing) {
+                    case AS_CONFIGURED -> switch (publishingPolicy) {
+                        case NONE -> new 
CommandPublishingFacetForActionAnnotationAsConfigured.None(holder, 
servicesInjector);
+                        case IGNORE_QUERY_ONLY, IGNORE_SAFE -> 
Facets.hasSafeSemantics(holder)
+                            ? new 
CommandPublishingFacetForActionAnnotationAsConfigured.IgnoreSafe(holder, 
servicesInjector)
+                            : new 
CommandPublishingFacetForActionAnnotationAsConfigured.IgnoreSafeYetNot(holder, 
servicesInjector);
+                        case ALL -> new 
CommandPublishingFacetForActionAnnotationAsConfigured.All(holder, 
servicesInjector);
+                        default -> throw new IllegalStateException(
+                                String.format("configured 
action.commandPublishing policy '%s' not recognised", publishingPolicy));
+                    };
+                    case DISABLED -> new 
CommandPublishingFacetForActionAnnotation.Disabled(processor, holder, 
servicesInjector);
+                    case ENABLED, ENABLED_FOR_UPDATES_ONLY -> new 
CommandPublishingFacetForActionAnnotation.Enabled(processor, holder, 
servicesInjector);
+                    default -> throw new IllegalStateException(
+                            String.format("@Action#commandPublishing '%s' not 
recognised", publishing));
+                };
+            });
     }
 
     CommandPublishingFacetForActionAnnotation(
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForPropertyAnnotation.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForPropertyAnnotation.java
index 873470d9746..642a9edd462 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForPropertyAnnotation.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForPropertyAnnotation.java
@@ -68,7 +68,7 @@ public static CommandPublishingFacet create(
 
         return propertyIfAny
             .filter(property -> property.commandPublishing() != 
Publishing.NOT_SPECIFIED)
-            .map(property -> {
+            .<CommandPublishingFacet>map(property -> {
                 Publishing publishing = property.commandPublishing();
 
                 var processorClass = property.commandDtoProcessor();
@@ -78,64 +78,54 @@ public static CommandPublishingFacet create(
                     publishing = Publishing.ENABLED;
                 }
 
-                switch (publishing) {
-                    case AS_CONFIGURED:
-                        switch (publishingPolicy) {
-                            case NONE:
-                                return (CommandPublishingFacet)new 
CommandPublishingFacetForPropertyAnnotationAsConfigured.None(holder, 
servicesInjector);
-                            case ALL:
-                                return new 
CommandPublishingFacetForPropertyAnnotationAsConfigured.All(holder, 
servicesInjector);
-                            default:
-                                throw new 
IllegalStateException(String.format("configured property.commandpublishing 
policy '%s' not recognised", publishingPolicy));
-                        }
-                    case DISABLED:
-                        return new 
CommandPublishingFacetForPropertyAnnotation.Disabled(processor, holder, 
servicesInjector);
-                    case ENABLED:
-                        return new 
CommandPublishingFacetForPropertyAnnotation.Enabled(processor, holder, 
servicesInjector);
-                    default:
-                        throw new 
IllegalStateException(String.format("@Property#commandPublishing '%s' not 
recognised", publishing));
-                }
+                return switch (publishing) {
+                    case AS_CONFIGURED -> switch (publishingPolicy) {
+                        case NONE -> new 
CommandPublishingFacetForPropertyAnnotationAsConfigured.None(holder, 
servicesInjector);
+                        case ALL -> new 
CommandPublishingFacetForPropertyAnnotationAsConfigured.All(holder, 
servicesInjector);
+                        default -> throw new IllegalStateException(
+                                String.format("configured 
property.commandpublishing policy '%s' not recognised", publishingPolicy));
+                    };
+                    case DISABLED -> new 
CommandPublishingFacetForPropertyAnnotation.Disabled(processor, holder, 
servicesInjector);
+                    case ENABLED, ENABLED_FOR_UPDATES_ONLY -> new 
CommandPublishingFacetForPropertyAnnotation.Enabled(processor, holder, 
servicesInjector);
+                    default -> throw new IllegalStateException(
+                            String.format("@Property#commandPublishing '%s' 
not recognised", publishing));
+                };
             })
             .orElseGet(() -> {
                 // there is no publishing facet from either @Action or 
@Property, so use the appropriate configuration to install a default
-                if (representsProperty(holder)) {
+                if (representsProperty(holder))
                     // we are dealing with a property
-                    switch (publishingPolicy) {
-                        case NONE:
-                            return new 
CommandPublishingFacetForPropertyFromConfiguration.None(holder, 
servicesInjector);
-                        case ALL:
-                            return new 
CommandPublishingFacetForPropertyFromConfiguration.All(holder, 
servicesInjector);
-                        default:
-                            throw new 
IllegalStateException(String.format("configured property.commandPublishing 
policy '%s' not recognised", publishingPolicy));
-                    }
-                } else {
+                    return switch (publishingPolicy) {
+                        case NONE -> new 
CommandPublishingFacetForPropertyFromConfiguration.None(holder, 
servicesInjector);
+                        case ALL -> new 
CommandPublishingFacetForPropertyFromConfiguration.All(holder, 
servicesInjector);
+                        default -> throw new 
IllegalStateException(String.format("configured property.commandPublishing 
policy '%s' not recognised", publishingPolicy));
+                    };
+                else {
                     // we are dealing with an action
                     var actionPublishingPolicy = 
ActionConfigOptions.actionCommandPublishingPolicy(configuration);
-                    switch (actionPublishingPolicy) {
-                        case NONE:
-                            return new 
CommandPublishingFacetForActionFromConfiguration.None(holder, servicesInjector);
-                        case IGNORE_QUERY_ONLY:
-                        case IGNORE_SAFE:
-                            return Facets.hasSafeSemantics(holder)
-                                    ? new 
CommandPublishingFacetForActionFromConfiguration.IgnoreSafe(holder, 
servicesInjector)
-                                    : new 
CommandPublishingFacetForActionFromConfiguration.IgnoreSafeYetNot(holder, 
servicesInjector);
-                        case ALL:
-                            return new 
CommandPublishingFacetForActionFromConfiguration.All(holder, servicesInjector);
-                        default:
-                            throw new 
IllegalStateException(String.format("configured action.commandPublishing policy 
'%s' not recognised", actionPublishingPolicy));
-                    }
+                    return switch (actionPublishingPolicy) {
+                        case NONE -> new 
CommandPublishingFacetForActionFromConfiguration.None(holder, servicesInjector);
+                        case IGNORE_QUERY_ONLY, IGNORE_SAFE -> 
Facets.hasSafeSemantics(holder)
+                                                            ? new 
CommandPublishingFacetForActionFromConfiguration.IgnoreSafe(holder, 
servicesInjector)
+                                                            : new 
CommandPublishingFacetForActionFromConfiguration.IgnoreSafeYetNot(holder, 
servicesInjector);
+                        case ALL -> new 
CommandPublishingFacetForActionFromConfiguration.All(holder, servicesInjector);
+                        default -> throw new 
IllegalStateException(String.format("configured action.commandPublishing policy 
'%s' not recognised", actionPublishingPolicy));
+                    };
                 }
             });
+
     }
 
     private static boolean representsProperty(final FacetHolder holder) {
         // a property
-        if (holder instanceof TypedFacetHolder && 
((TypedFacetHolder)holder).featureType() == FeatureType.PROPERTY) {
+        if (holder instanceof TypedFacetHolder typedFacetHolder
+                && typedFacetHolder.featureType() == FeatureType.PROPERTY)
             return true;
-        }
         // or a mixin
-        return  holder.containsFacet(ContributingFacet.class) &&
-                holder.getFacet(ContributingFacet.class).contributed() == 
MixinFacet.Contributing.AS_PROPERTY;
+        return holder.lookupFacet(ContributingFacet.class)
+                .map(ContributingFacet::contributed)
+                .map(MixinFacet.Contributing.AS_PROPERTY::equals)
+                .orElse(false);
     }
 
     CommandPublishingFacetForPropertyAnnotation(
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForActionAnnotation.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForActionAnnotation.java
index 14b780a8d5f..67b52d6bdb8 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForActionAnnotation.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForActionAnnotation.java
@@ -58,34 +58,24 @@ public static Optional<ExecutionPublishingFacet> create(
 
         var publishingPolicy = 
ActionConfigOptions.actionExecutionPublishingPolicy(configuration);
 
-        return
-        actionsIfAny
-                .filter(action -> action.executionPublishing() != 
Publishing.NOT_SPECIFIED)
-                .map(Action::executionPublishing)
-                .<ExecutionPublishingFacet>map(publishing -> {
-                    switch (publishing) {
-                        case AS_CONFIGURED:
-                            switch (publishingPolicy) {
-                                case NONE:
-                                    return new 
ExecutionPublishingFacetForActionAnnotationAsConfigured.None(holder);
-                                case IGNORE_QUERY_ONLY:
-                                case IGNORE_SAFE:
-                                    return Facets.hasSafeSemantics(holder)
-                                            ? new 
ExecutionPublishingFacetForActionAnnotationAsConfigured.IgnoreSafe(holder)
-                                            : new 
ExecutionPublishingFacetForActionAnnotationAsConfigured.IgnoreSafeYetNot(holder);
-                                case ALL:
-                                    return new 
ExecutionPublishingFacetForActionAnnotationAsConfigured.All(holder);
-                                default:
-                                    throw new 
IllegalStateException(String.format("configured action.executionPublishing 
policy '%s' not recognised", publishingPolicy));
-                                }
-                        case DISABLED:
-                            return new 
ExecutionPublishingFacetForActionAnnotation.Disabled(holder);
-                        case ENABLED:
-                            return new 
ExecutionPublishingFacetForActionAnnotation.Enabled(holder);
-                        default:
-                            throw new 
IllegalStateException(String.format("@Action#executionPublishing '%s' not 
recognised", publishing));
-                    }
-                });
+        return actionsIfAny
+            .filter(action -> action.executionPublishing() != 
Publishing.NOT_SPECIFIED)
+            .map(Action::executionPublishing)
+            .<ExecutionPublishingFacet>map(publishing -> (switch (publishing) {
+                case AS_CONFIGURED -> switch (publishingPolicy) {
+                    case NONE -> new 
ExecutionPublishingFacetForActionAnnotationAsConfigured.None(holder);
+                    case IGNORE_QUERY_ONLY, IGNORE_SAFE -> 
Facets.hasSafeSemantics(holder)
+                        ? new 
ExecutionPublishingFacetForActionAnnotationAsConfigured.IgnoreSafe(holder)
+                        : new 
ExecutionPublishingFacetForActionAnnotationAsConfigured.IgnoreSafeYetNot(holder);
+                    case ALL -> new 
ExecutionPublishingFacetForActionAnnotationAsConfigured.All(holder);
+                    default -> throw new IllegalStateException(
+                            String.format("configured 
action.executionPublishing policy '%s' not recognised", publishingPolicy));
+                };
+                case DISABLED -> new 
ExecutionPublishingFacetForActionAnnotation.Disabled(holder);
+                case ENABLED, ENABLED_FOR_UPDATES_ONLY -> new 
ExecutionPublishingFacetForActionAnnotation.Enabled(holder);
+                default -> throw new IllegalStateException(
+                        String.format("@Action#executionPublishing '%s' not 
recognised", publishing));
+            }));
     }
 
     ExecutionPublishingFacetForActionAnnotation(final FacetHolder holder) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForPropertyAnnotation.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForPropertyAnnotation.java
index ec9fcc705df..f08b54d6e34 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForPropertyAnnotation.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForPropertyAnnotation.java
@@ -67,66 +67,54 @@ public static ExecutionPublishingFacet create(
         return propertyIfAny
             .map(Property::executionPublishing)
             .filter(publishing -> publishing != Publishing.NOT_SPECIFIED)
-            .map(publishing -> {
-
-                switch (publishing) {
-                    case AS_CONFIGURED:
-                        switch (publishingPolicy) {
-                            case NONE:
-                                return (ExecutionPublishingFacet)new 
ExecutionPublishingFacetForPropertyAnnotationAsConfigured.None(holder);
-                            case ALL:
-                                return new 
ExecutionPublishingFacetForPropertyAnnotationAsConfigured.All(holder);
-                            default:
-                                throw new 
IllegalStateException(String.format("configured property.executionPublishing 
policy '%s' not recognised", publishingPolicy));
-                        }
-                    case DISABLED:
-                        return new 
ExecutionPublishingFacetForPropertyAnnotation.Disabled(holder);
-                    case ENABLED:
-                        return new 
ExecutionPublishingFacetForPropertyAnnotation.Enabled(holder);
-                    default:
-                        throw new 
IllegalStateException(String.format("@Property#executionPublishing '%s' not 
recognised", publishing));
-                }
-            })
+            .<ExecutionPublishingFacet>map(publishing -> (switch (publishing) {
+            case AS_CONFIGURED -> switch (publishingPolicy) {
+                case NONE -> new 
ExecutionPublishingFacetForPropertyAnnotationAsConfigured.None(holder);
+                case ALL -> new 
ExecutionPublishingFacetForPropertyAnnotationAsConfigured.All(holder);
+                default -> throw new IllegalStateException(
+                        String.format("configured property.executionPublishing 
policy '%s' not recognised", publishingPolicy));
+            };
+            case DISABLED -> new 
ExecutionPublishingFacetForPropertyAnnotation.Disabled(holder);
+            case ENABLED, ENABLED_FOR_UPDATES_ONLY -> new 
ExecutionPublishingFacetForPropertyAnnotation.Enabled(holder);
+            default -> throw new IllegalStateException(
+                    String.format("@Property#executionPublishing '%s' not 
recognised", publishing));
+            }))
             .orElseGet(() -> {
                 // there is no publishing facet from either @Action or 
@Property, so use the appropriate configuration to install a default
-                if (representsProperty(holder)) {
+                if (representsProperty(holder))
                     // we are dealing with a property
-                    switch (publishingPolicy) {
-                        case NONE:
-                            return new 
ExecutionPublishingFacetForPropertyFromConfiguration.None(holder);
-                        case ALL:
-                            return new 
ExecutionPublishingFacetForPropertyFromConfiguration.All(holder);
-                        default:
-                            throw new 
IllegalStateException(String.format("configured property.executionPublishing 
policy '%s' not recognised", publishingPolicy));
-                    }
-                } else {
+                    return switch (publishingPolicy) {
+                        case NONE -> new 
ExecutionPublishingFacetForPropertyFromConfiguration.None(holder);
+                        case ALL -> new 
ExecutionPublishingFacetForPropertyFromConfiguration.All(holder);
+                        default -> throw new IllegalStateException(
+                                String.format("configured 
property.executionPublishing policy '%s' not recognised", publishingPolicy));
+                    };
+                else {
                     // we are dealing with an action
                     var actionPublishingPolicy = 
ActionConfigOptions.actionExecutionPublishingPolicy(configuration);
-                    switch (actionPublishingPolicy) {
-                        case NONE:
-                            return new 
ExecutionPublishingFacetForActionFromConfiguration.None(holder);
-                        case IGNORE_QUERY_ONLY:
-                        case IGNORE_SAFE:
-                            return Facets.hasSafeSemantics(holder)
-                                    ? new 
ExecutionPublishingFacetForActionFromConfiguration.IgnoreSafe(holder)
-                                    : new 
ExecutionPublishingFacetForActionFromConfiguration.IgnoreSafeYetNot(holder);
-                        case ALL:
-                            return new 
ExecutionPublishingFacetForActionFromConfiguration.All(holder);
-                        default:
-                            throw new 
IllegalStateException(String.format("configured action.executionPublishing 
policy '%s' not recognised", actionPublishingPolicy));
-                    }
+                    return switch (actionPublishingPolicy) {
+                    case NONE -> new 
ExecutionPublishingFacetForActionFromConfiguration.None(holder);
+                    case IGNORE_QUERY_ONLY, IGNORE_SAFE -> 
Facets.hasSafeSemantics(holder)
+                                                        ? new 
ExecutionPublishingFacetForActionFromConfiguration.IgnoreSafe(holder)
+                                                        : new 
ExecutionPublishingFacetForActionFromConfiguration.IgnoreSafeYetNot(holder);
+                    case ALL -> new 
ExecutionPublishingFacetForActionFromConfiguration.All(holder);
+                    default -> throw new IllegalStateException(
+                            String.format("configured 
action.executionPublishing policy '%s' not recognised", 
actionPublishingPolicy));
+                    };
                 }
             });
     }
 
     private static boolean representsProperty(final FacetHolder holder) {
         // a property
-        if (holder instanceof TypedFacetHolder && 
((TypedFacetHolder)holder).featureType() == FeatureType.PROPERTY) {
+        if (holder instanceof TypedFacetHolder typedFacetHolder
+                && typedFacetHolder.featureType() == FeatureType.PROPERTY)
             return true;
-        }
         // or a mixin
-        return  holder.containsFacet(ContributingFacet.class) &&
-                holder.getFacet(ContributingFacet.class).contributed() == 
MixinFacet.Contributing.AS_PROPERTY;
+        return holder.lookupFacet(ContributingFacet.class)
+                .map(ContributingFacet::contributed)
+                .map(MixinFacet.Contributing.AS_PROPERTY::equals)
+                .orElse(false);
     }
 
     public ExecutionPublishingFacetForPropertyAnnotation(final FacetHolder 
holder) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotation.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotation.java
index 9956501d41c..56a686b2250 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotation.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotation.java
@@ -38,32 +38,27 @@ public static Optional<EntityChangePublishingFacet> create(
 
         var publish = 
entityChangePublishingIfAny.orElse(Publishing.AS_CONFIGURED);
 
-        switch (publish) {
-        case NOT_SPECIFIED:
-        case AS_CONFIGURED:
-
-            var publishingPolicy = 
DomainObjectConfigOptions.entityChangePublishingPolicy(configuration);
-            switch (publishingPolicy) {
-            case NONE:
-                return Optional.of(entityChangePublishingIfAny.isPresent()
+        return switch (publish) {
+            case NOT_SPECIFIED, AS_CONFIGURED -> {
+                var publishingPolicy = 
DomainObjectConfigOptions.entityChangePublishingPolicy(configuration);
+                yield switch (publishingPolicy) {
+                    case NONE -> 
Optional.of(entityChangePublishingIfAny.isPresent()
                         ? new 
EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured(holder, false)
                         : new 
EntityChangePublishingFacetFromConfiguration(holder, false));
-            default:
-                return Optional.of(entityChangePublishingIfAny.isPresent()
+                    default -> 
Optional.of(entityChangePublishingIfAny.isPresent()
                         ? new 
EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured(holder, true)
                         : new 
EntityChangePublishingFacetFromConfiguration(holder, true));
+                };
             }
-        case DISABLED:
-            return Optional.of(new 
EntityChangePublishingFacetForDomainObjectAnnotation(holder, false));
-        case ENABLED:
-            return Optional.of(new 
EntityChangePublishingFacetForDomainObjectAnnotation(holder, true));
-
-        default:
-            throw _Exceptions.unmatchedCase(publish);
-        }
+            case DISABLED -> Optional.of(new 
EntityChangePublishingFacetForDomainObjectAnnotation(holder, false, false, 
false, false));
+            case ENABLED -> Optional.of(new 
EntityChangePublishingFacetForDomainObjectAnnotation(holder, true, true, true, 
true));
+            case ENABLED_FOR_UPDATES_ONLY -> Optional.of(new 
EntityChangePublishingFacetForDomainObjectAnnotation(holder, true, false, true, 
false));
+            default -> throw _Exceptions.unmatchedCase(publish);
+        };
     }
 
-    protected EntityChangePublishingFacetForDomainObjectAnnotation(final 
FacetHolder holder, boolean enabled) {
-        super(holder, enabled);
+    protected EntityChangePublishingFacetForDomainObjectAnnotation(final 
FacetHolder holder, final boolean enabled,
+            final boolean enabledForCreate, final boolean enabledForUpdate, 
final boolean enabledForDelete) {
+        super(holder, enabled, enabledForCreate, enabledForUpdate, 
enabledForDelete);
     }
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured.java
index df5890d9df7..e724d79135d 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured.java
@@ -23,7 +23,7 @@
 public class EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured 
extends EntityChangePublishingFacetForDomainObjectAnnotation {
 
     public 
EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured(final 
FacetHolder facetHolder, final boolean enabled) {
-        super(facetHolder, enabled);
+        super(facetHolder, enabled, enabled, enabled, enabled);
     }
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetFromConfiguration.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetFromConfiguration.java
index cdeebda521c..0ef4aef74cf 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetFromConfiguration.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetFromConfiguration.java
@@ -30,7 +30,7 @@ public class EntityChangePublishingFacetFromConfiguration
 extends EntityChangePublishingFacetAbstract {
 
     public EntityChangePublishingFacetFromConfiguration(final FacetHolder 
facetHolder, final boolean enabled) {
-        super(facetHolder, enabled);
+        super(facetHolder, enabled, enabled, enabled, enabled);
     }
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacet.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacet.java
index 91513f3bee0..c8b869cdc12 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacet.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacet.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.publish.entitychange;
 
+import java.util.Optional;
+
 import org.apache.causeway.core.metamodel.facetapi.Facet;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
@@ -30,22 +32,41 @@
  */
 public interface EntityChangePublishingFacet extends Facet {
 
-    public static boolean isPublishingEnabled(final FacetHolder facetHolder) {
-        if(facetHolder==null) {
-            return false;
-        }
+    static boolean isPublishingEnabled(final FacetHolder facetHolder) {
+        return entityChangePublishingFacet(facetHolder)
+                .map(EntityChangePublishingFacet::isEnabled)
+                .orElse(false);
+    }
+
+    static boolean isPublishingEnabledForCreate(final FacetHolder facetHolder) 
{
+        return entityChangePublishingFacet(facetHolder)
+                .map(EntityChangePublishingFacet::isEnabledForCreate)
+                .orElse(false);
+    }
 
-        if(facetHolder instanceof ObjectSpecification) {
-            if(!((ObjectSpecification)facetHolder).isEntity()) {
-                return false;
-            }
-        }
+    static boolean isPublishingEnabledForUpdate(final FacetHolder facetHolder) 
{
+        return entityChangePublishingFacet(facetHolder)
+                .map(EntityChangePublishingFacet::isEnabledForUpdate)
+                .orElse(false);
+    }
 
-        var entityChangePublishingFacet = 
facetHolder.getFacet(EntityChangePublishingFacet.class);
-        return entityChangePublishingFacet != null
-                && entityChangePublishingFacet.isEnabled();
+    static boolean isPublishingEnabledForDelete(final FacetHolder facetHolder) 
{
+        return entityChangePublishingFacet(facetHolder)
+                .map(EntityChangePublishingFacet::isEnabledForDelete)
+                .orElse(false);
     }
 
     boolean isEnabled();
+    boolean isEnabledForCreate();
+    boolean isEnabledForUpdate();
+    boolean isEnabledForDelete();
 
+    private static Optional<EntityChangePublishingFacet> 
entityChangePublishingFacet(final FacetHolder facetHolder) {
+        if(facetHolder==null)
+            return Optional.empty();
+        if(facetHolder instanceof ObjectSpecification objSpepc
+                && !objSpepc.isEntity())
+            return Optional.empty(); // optimization
+        return facetHolder.lookupFacet(EntityChangePublishingFacet.class);
+    }
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacetAbstract.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacetAbstract.java
index af60b3f0544..20ce78bf62f 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacetAbstract.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacetAbstract.java
@@ -32,12 +32,18 @@ private static final Class<? extends Facet> type() {
         return EntityChangePublishingFacet.class;
     }
 
-    @Getter
-    private final boolean enabled;
+    @Getter private final boolean enabled;
+    @Getter private final boolean enabledForCreate;
+    @Getter private final boolean enabledForUpdate;
+    @Getter private final boolean enabledForDelete;
 
-    public EntityChangePublishingFacetAbstract(final FacetHolder facetHolder, 
boolean enabled) {
+    public EntityChangePublishingFacetAbstract(final FacetHolder facetHolder, 
final boolean enabled,
+            final boolean enabledForCreate, final boolean enabledForUpdate, 
final boolean enabledForDelete) {
         super(EntityChangePublishingFacetAbstract.type(), facetHolder);
         this.enabled = enabled;
+        this.enabledForCreate = enabledForCreate;
+        this.enabledForUpdate = enabledForUpdate;
+        this.enabledForDelete = enabledForDelete;
     }
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacet.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacet.java
index 6eca0d5a86a..4ea8c167ac0 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacet.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacet.java
@@ -18,14 +18,14 @@
  */
 package 
org.apache.causeway.core.metamodel.facets.properties.property.entitychangepublishing;
 
+import org.jspecify.annotations.NonNull;
+
 import org.apache.causeway.applib.annotation.Publishing;
 import org.apache.causeway.applib.value.Blob;
 import org.apache.causeway.applib.value.Clob;
 import org.apache.causeway.core.metamodel.facetapi.Facet;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
-import org.jspecify.annotations.NonNull;
-
 /**
  * Indicates whether a property should be excluded from entity change 
publishing (auditing).
  * @since 2.0
@@ -33,7 +33,7 @@
 public interface EntityPropertyChangePublishingPolicyFacet extends Facet {
 
     /**
-     * Must be one of Publishing.ENABLED or Publishing.DISABLED.
+     * Must be one of {@link Publishing#ENABLED}, {@link 
Publishing#ENABLED_FOR_UPDATES_ONLY} or {@link Publishing#DISABLED}.
      */
     @NonNull Publishing getEntityChangePublishing();
 
@@ -42,7 +42,8 @@ default boolean isPublishingVetoed() {
     }
 
     default boolean isPublishingAllowed() {
-        return getEntityChangePublishing() == Publishing.ENABLED;
+        return getEntityChangePublishing() == Publishing.ENABLED
+                || getEntityChangePublishing() == 
Publishing.ENABLED_FOR_UPDATES_ONLY;
     }
 
     static boolean isExcludedFromPublishing(final @NonNull OneToOneAssociation 
property) {
@@ -58,7 +59,7 @@ static boolean isExcludedFromPublishing(final @NonNull 
OneToOneAssociation prope
                     
.map(EntityPropertyChangePublishingPolicyFacet::isPublishingAllowed)
                     .orElse(false);
 
-            //XXX CAUSEWAY-1488, exclude Bob/Clob from property change 
publishing unless explicitly allowed
+            //XXX CAUSEWAY-1488, exclude Blob/Clob from property change 
publishing unless explicitly allowed
             return !isExplictlyAllowed;
         }
 
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacetForPropertyAnnotation.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacetForPropertyAnnotation.java
index dbf4cc19abc..bcba7479eb2 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacetForPropertyAnnotation.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacetForPropertyAnnotation.java
@@ -34,9 +34,9 @@ public static 
Optional<EntityPropertyChangePublishingPolicyFacet> create(
         return propertyIfAny
                 .map(Property::entityChangePublishing)
                 // only install facet if policy is explicit ('enabled' or 
'disabled')
-                .filter(entityChangePublishing ->
-                    entityChangePublishing == Publishing.ENABLED
-                        || entityChangePublishing == Publishing.DISABLED)
+                .filter(entityChangePublishing -> entityChangePublishing == 
Publishing.ENABLED
+                    || entityChangePublishing == 
Publishing.ENABLED_FOR_UPDATES_ONLY
+                    || entityChangePublishing == Publishing.DISABLED)
                 .map(entityChangePublishing ->
                     new 
EntityPropertyChangePublishingPolicyFacetForPropertyAnnotation(entityChangePublishing,
 holder));
     }
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/FacetFactoryTestAbstract.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/FacetFactoryTestAbstract.java
new file mode 100644
index 00000000000..a286ff984aa
--- /dev/null
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/FacetFactoryTestAbstract.java
@@ -0,0 +1,484 @@
+/*
+ *  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.causeway.core.metamodel.facets;
+
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.jspecify.annotations.NonNull;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.applib.annotation.Introspection.IntrospectionPolicy;
+import org.apache.causeway.applib.id.LogicalType;
+import org.apache.causeway.applib.services.i18n.TranslationService;
+import org.apache.causeway.applib.services.iactnlayer.InteractionService;
+import org.apache.causeway.commons.internal.assertions._Assert;
+import 
org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
+import org.apache.causeway.commons.internal.reflection._MethodFacades;
+import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
+import org.apache.causeway.core.metamodel.execution.MemberExecutorService;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.facetapi.FeatureType;
+import 
org.apache.causeway.core.metamodel.facets.FacetFactory.ProcessClassContext;
+import 
org.apache.causeway.core.metamodel.facets.FacetFactory.ProcessMethodContext;
+import 
org.apache.causeway.core.metamodel.facets.FacetFactory.ProcessParameterContext;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
+import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.causeway.core.metamodel.spec.impl._JUnitSupport;
+import org.apache.causeway.core.metamodel.valuesemantics.IntValueSemantics;
+import org.apache.causeway.core.mmtestsupport.MetaModelContext_forTesting;
+import org.apache.causeway.core.mmtestsupport.MethodRemover_forTesting;
+import 
org.apache.causeway.core.security.authentication.InteractionContextFactory;
+
+import lombok.Builder;
+import lombok.Getter;
+
+public abstract class FacetFactoryTestAbstract
+implements HasMetaModelContext {
+
+    // -- SCENARIO BUILDER
+
+    @Builder
+    public record ActionScenario(
+            Class<?> declaringClass,
+            String actionName,
+            Optional<Class<?>> mixinClass) {
+        public static ActionScenarioBuilder builder(final Class<?> 
declaringClass, final String actionName) {
+            return new ActionScenario.ActionScenarioBuilder()
+                    .mixinClass(Optional.empty())
+                    .declaringClass(declaringClass)
+                    .actionName(actionName);
+        }
+    }
+
+    @Builder
+    public record ParameterScenario(
+            Class<?> declaringClass,
+            String actionName,
+            int paramIndex,
+            Optional<Class<?>> mixinClass) {
+        public static ParameterScenarioBuilder builder(final Class<?> 
declaringClass, final String actionName, final int paramIndex) {
+            return new ParameterScenario.ParameterScenarioBuilder()
+                    .mixinClass(Optional.empty())
+                    .declaringClass(declaringClass)
+                    .actionName(actionName)
+                    .paramIndex(paramIndex);
+        }
+    }
+
+    @Builder
+    public record PropertyScenario(
+            Class<?> declaringClass,
+            String propertyName,
+            Optional<Class<?>> mixinClass) {
+        public static PropertyScenarioBuilder builder(final Class<?> 
declaringClass, final String propertyName) {
+            return new PropertyScenario.PropertyScenarioBuilder()
+                    .mixinClass(Optional.empty())
+                    .declaringClass(declaringClass)
+                    .propertyName(propertyName);
+        }
+    }
+
+    @Builder
+    public record CollectionScenario(
+            Class<?> declaringClass,
+            String collectionName,
+            Optional<Class<?>> mixinClass) {
+        public static CollectionScenarioBuilder builder(final Class<?> 
declaringClass, final String collectionName) {
+            return new CollectionScenario.CollectionScenarioBuilder()
+                    .mixinClass(Optional.empty())
+                    .declaringClass(declaringClass)
+                    .collectionName(collectionName);
+        }
+    }
+
+    // -- SETUP
+
+    @Getter(onMethod_ = {@Override}) protected MetaModelContext 
metaModelContext;
+
+    private MethodRemover_forTesting methodRemover;
+
+    @BeforeEach
+    protected void setup() {
+        setup(__->{});
+    }
+
+    protected final void setup(
+        final 
Consumer<MetaModelContext_forTesting.MetaModelContext_forTestingBuilder> 
customizer) {
+
+        var mockTranslationService = Mockito.mock(TranslationService.class);
+        var mockInteractionService = Mockito.mock(InteractionService.class);
+        var mockMemberExecutorService = 
Mockito.mock(MemberExecutorService.class);
+
+        var iaContext = InteractionContextFactory.testing();
+
+        this.methodRemover = new MethodRemover_forTesting();
+
+        var builder = MetaModelContext_forTesting.builder()
+            .translationService(mockTranslationService)
+            .interactionService(mockInteractionService)
+            .memberExecutor(mockMemberExecutorService)
+            .valueSemantic(new IntValueSemantics());
+
+        customizer.accept(builder);
+
+        this.metaModelContext = builder.build();
+
+        
Mockito.when(mockInteractionService.currentInteractionContext()).thenReturn(Optional.of(iaContext));
+    }
+
+    // -- TEAR DOWN
+
+    @AfterEach
+    protected void tearDownAll() {
+        methodRemover = null;
+    }
+
+    @FunctionalInterface
+    public static interface MemberScenarioConsumer {
+        void accept(
+                ProcessMethodContext processMethodContext,
+                FacetHolder facetHolder,
+                FacetedMethod facetedMethod);
+    }
+    @FunctionalInterface
+    protected static interface MixedInActionScenarioConsumer {
+        void accept(
+                ProcessMethodContext processMethodContext,
+                ObjectSpecification mixeeSpec,
+                FacetedMethod facetedMethod,
+                ObjectAction mixedInAct);
+    }
+    @FunctionalInterface
+    protected static interface MixedInPropertyScenarioConsumer {
+        void accept(
+                ProcessMethodContext processMethodContext,
+                ObjectSpecification mixeeSpec,
+                FacetedMethod facetedMethod,
+                OneToOneAssociation mixedInProp);
+    }
+    @FunctionalInterface
+    protected static interface MixedInCollectionScenarioConsumer {
+        void accept(
+                ProcessMethodContext processMethodContext,
+                ObjectSpecification mixeeSpec,
+                FacetedMethod facetedMethod,
+                OneToManyAssociation mixedInColl);
+    }
+    @FunctionalInterface
+    protected static interface ParameterScenarioConsumer {
+        void accept(
+                ProcessParameterContext processParameterContext,
+                FacetHolder facetHolder,
+                FacetedMethod facetedMethod,
+                FacetedMethodParameter facetedMethodParameter);
+    }
+
+    /**
+     * Action scenario.
+     */
+    protected void actionScenario(
+            final Class<?> declaringClass, final String actionName, final 
MemberScenarioConsumer consumer) {
+        actionScenario(ActionScenario.builder(declaringClass, 
actionName).build(), consumer);
+    }
+    /**
+     * Custom Action scenario.
+     */
+    protected void actionScenario(
+            final ActionScenario scenario, final MemberScenarioConsumer 
consumer) {
+
+        var declaringClass = scenario.declaringClass();
+        var memberId = scenario.actionName();
+        var actionMethod = _Utils.findMethodByNameOrFail(declaringClass, 
memberId);
+        var paramTypes = actionMethod.paramTypes();
+        var facetHolder = actionFacetHolder(declaringClass, memberId, 
paramTypes);
+        var facetedMethod = 
FacetedMethod.testing.createForAction(getMetaModelContext(), declaringClass, 
memberId, paramTypes);
+        var processMethodContext = ProcessMethodContext
+                .forTesting(declaringClass, FeatureType.ACTION, actionMethod, 
methodRemover, facetedMethod);
+
+        consumer.accept(processMethodContext, facetHolder, facetedMethod);
+    }
+    /**
+     * MixedIn Action scenario.
+     */
+    protected void actionScenarioMixedIn(
+            final Class<?> mixeeClass, final Class<?> mixinClass, final 
MixedInActionScenarioConsumer consumer) {
+        var scenario = ActionScenario.builder(mixeeClass, "unused")
+                .mixinClass(Optional.of(mixinClass))
+                .build();
+        actionScenarioMixedIn(scenario, consumer);
+    }
+    protected void actionScenarioMixedIn(
+            final ActionScenario scenario, final MixedInActionScenarioConsumer 
consumer) {
+
+        var declaringClass = scenario.declaringClass();
+
+        // get mixin main, assuming 'act'
+        var mixinClass = scenario.mixinClass().orElseThrow();
+        var mixedInMethod = _Utils.findMethodByNameOrFail(mixinClass, "act");
+
+        var annotatedMethod = mixedInMethod;
+        var facetedMethod = 
FacetedMethod.createForAction(getMetaModelContext(), mixinClass,
+                _MethodFacades.regular(annotatedMethod));
+
+        var id = facetedMethod.getFeatureIdentifier();
+        assertNotNull(id.className());
+
+        var processMethodContext = new ProcessMethodContext(
+                mixinClass, IntrospectionPolicy.ENCAPSULATION_ENABLED, 
FeatureType.ACTION,
+                _MethodFacades.regular(annotatedMethod),
+                methodRemover, facetedMethod, true);
+
+        final ObjectSpecification mixeeSpec = 
getSpecificationLoader().loadSpecification(declaringClass);
+        final ObjectSpecification mixinSpec = 
getSpecificationLoader().loadSpecification(mixinClass);
+        final ObjectAction mixedInAct =
+                _JUnitSupport.mixedInActionforMixinMain(mixeeSpec, mixinSpec, 
"act", facetedMethod);
+
+        consumer.accept(processMethodContext, mixeeSpec, facetedMethod, 
mixedInAct);
+    }
+
+    /**
+     * Parameter scenario.
+     */
+    protected void parameterScenario(
+            final Class<?> declaringClass, final String actionName, final int 
paramIndex, final ParameterScenarioConsumer consumer) {
+        parameterScenario(ParameterScenario.builder(declaringClass, 
actionName, paramIndex).build(), consumer);
+    }
+    /**
+     * Custom Parameter scenario.
+     */
+    protected void parameterScenario(
+            final ParameterScenario scenario, final ParameterScenarioConsumer 
consumer) {
+        _Assert.assertEquals(0, scenario.paramIndex(), ()->"not yet 
implemented otherwise");
+
+        var declaringClass = scenario.declaringClass();
+        var memberId = scenario.actionName();
+        var actionMethod = _Utils.findMethodByNameOrFail(declaringClass, 
memberId);
+        var paramTypes = actionMethod.paramTypes();
+        var facetHolder = actionFacetHolder(declaringClass, memberId, 
paramTypes);
+        var facetedMethod = 
FacetedMethod.testing.createForAction(getMetaModelContext(), declaringClass, 
memberId, paramTypes);
+        var facetedMethodParameter =
+                actionMethod.isNoArg()
+                ? (FacetedMethodParameter)null
+                : new FacetedMethodParameter(getMetaModelContext(),
+                    FeatureType.ACTION_PARAMETER_SINGULAR, 
facetedMethod.owningType(),
+                    facetedMethod.methodFacade(), 0);
+
+        var processParameterContext =
+                FacetFactory.ProcessParameterContext.forTesting(
+                        declaringClass, 
IntrospectionPolicy.ANNOTATION_OPTIONAL, actionMethod, null, 
facetedMethodParameter);
+
+        consumer.accept(processParameterContext, facetHolder, facetedMethod, 
facetedMethodParameter);
+    }
+
+    /**
+     * Property scenario.
+     */
+    protected void propertyScenario(
+            final Class<?> declaringClass, final String propertyName, final 
MemberScenarioConsumer consumer) {
+        propertyScenario(PropertyScenario.builder(declaringClass, 
propertyName).build(), consumer);
+    }
+    /**
+     * Custom Property scenario.
+     */
+    protected void propertyScenario(
+            final PropertyScenario scenario, final MemberScenarioConsumer 
consumer) {
+        var declaringClass = scenario.declaringClass();
+        var memberId = scenario.propertyName();
+
+        var facetHolder = propertyFacetHolder(declaringClass, memberId);
+        var annotatedMethod = _Utils.findGetterOrFail(declaringClass, 
memberId);
+        var facetedMethod = 
FacetedMethod.createForProperty(getMetaModelContext(), declaringClass, 
annotatedMethod);
+
+        var processMethodContext = ProcessMethodContext
+                .forTesting(declaringClass, FeatureType.PROPERTY, 
annotatedMethod, methodRemover, facetedMethod);
+
+        consumer.accept(processMethodContext, facetHolder, facetedMethod);
+    }
+    /**
+     * MixedIn Property scenario.
+     */
+    protected void propertyScenarioMixedIn(
+            final Class<?> mixeeClass, final Class<?> mixinClass, final 
MixedInPropertyScenarioConsumer consumer) {
+        var scenario = PropertyScenario.builder(mixeeClass, "unused")
+                .mixinClass(Optional.of(mixinClass))
+                .build();
+        propertyScenarioMixedIn(scenario, consumer);
+    }
+    protected void propertyScenarioMixedIn(
+            final PropertyScenario scenario, final 
MixedInPropertyScenarioConsumer consumer) {
+
+        var declaringClass = scenario.declaringClass();
+
+        // get mixin main, assuming 'prop'
+        var mixinClass = scenario.mixinClass().orElseThrow();
+        var annotatedMethod = _Utils.findMethodByNameOrFail(mixinClass, 
"prop");
+
+        var facetedMethod = 
FacetedMethod.createForProperty(getMetaModelContext(), mixinClass, 
annotatedMethod);
+
+        var id = facetedMethod.getFeatureIdentifier();
+        assertNotNull(id.className());
+
+        var processMethodContext = new ProcessMethodContext(
+                mixinClass, IntrospectionPolicy.ENCAPSULATION_ENABLED, 
FeatureType.PROPERTY,
+                _MethodFacades.regular(annotatedMethod),
+                methodRemover, facetedMethod, true);
+
+        final ObjectSpecification mixeeSpec = 
getSpecificationLoader().loadSpecification(declaringClass);
+        final ObjectSpecification mixinSpec = 
getSpecificationLoader().loadSpecification(mixinClass);
+        final OneToOneAssociation mixedInProp =
+                _JUnitSupport.mixedInProp(mixeeSpec, mixinSpec, "prop", 
facetedMethod);
+
+        consumer.accept(processMethodContext, mixeeSpec, facetedMethod, 
mixedInProp);
+    }
+
+    /**
+     * Collection scenario.
+     */
+    protected void collectionScenario(
+            final Class<?> declaringClass, final String collectionName, final 
MemberScenarioConsumer consumer) {
+        collectionScenario(CollectionScenario.builder(declaringClass, 
collectionName).build(), consumer);
+    }
+    /**
+     * Custom Collection scenario.
+     */
+    protected void collectionScenario(
+            final CollectionScenario scenario, final MemberScenarioConsumer 
consumer) {
+
+        var declaringClass = scenario.declaringClass();
+        var memberId = scenario.collectionName();
+        var facetHolder = collectionFacetHolder(declaringClass, memberId);
+        var annotatedMethod = _Utils.findGetterOrFail(declaringClass, 
memberId);
+        var facetedMethod = 
FacetedMethod.createForProperty(getMetaModelContext(), declaringClass, 
annotatedMethod);
+
+        var processMethodContext = ProcessMethodContext
+                .forTesting(declaringClass, FeatureType.COLLECTION, 
annotatedMethod, methodRemover, facetedMethod);
+
+        consumer.accept(processMethodContext, facetHolder, facetedMethod);
+    }
+    /**
+     * MixedIn Collection scenario.
+     */
+    protected void collectionScenarioMixedIn(
+            final Class<?> mixeeClass, final Class<?> mixinClass, final 
MixedInCollectionScenarioConsumer consumer) {
+        var scenario = CollectionScenario.builder(mixeeClass, "unused")
+                .mixinClass(Optional.of(mixinClass))
+                .build();
+        collectionScenarioMixedIn(scenario, consumer);
+    }
+    protected void collectionScenarioMixedIn(
+            final CollectionScenario scenario, final 
MixedInCollectionScenarioConsumer consumer) {
+
+        var declaringClass = scenario.declaringClass();
+
+        // get mixin main, assuming 'coll'
+        var mixinClass = scenario.mixinClass().orElseThrow();
+        var annotatedMethod = _Utils.findMethodByNameOrFail(mixinClass, 
"coll");
+
+        var facetedMethod = 
FacetedMethod.createForCollection(getMetaModelContext(), mixinClass, 
annotatedMethod);
+
+        var id = facetedMethod.getFeatureIdentifier();
+        assertNotNull(id.className());
+
+        var processMethodContext = new ProcessMethodContext(
+                mixinClass, IntrospectionPolicy.ENCAPSULATION_ENABLED, 
FeatureType.COLLECTION,
+                _MethodFacades.regular(annotatedMethod),
+                methodRemover, facetedMethod, true);
+
+        final ObjectSpecification mixeeSpec = 
getSpecificationLoader().loadSpecification(declaringClass);
+        final ObjectSpecification mixinSpec = 
getSpecificationLoader().loadSpecification(mixinClass);
+        final OneToManyAssociation mixedInColl =
+                _JUnitSupport.mixedInColl(mixeeSpec, mixinSpec, "coll", 
facetedMethod);
+
+        consumer.accept(processMethodContext, mixeeSpec, facetedMethod, 
mixedInColl);
+    }
+
+    /**
+     * DomainObject scenario.
+     */
+    protected void objectScenario(final Class<?> declaringClass, final 
BiConsumer<ProcessClassContext, FacetHolder> consumer) {
+        var facetHolder = FacetHolder.simple(getMetaModelContext(),
+                Identifier.classIdentifier(LogicalType.fqcn(declaringClass)));
+        var processClassContext = ProcessClassContext
+                .forTesting(declaringClass, methodRemover, facetHolder);
+        consumer.accept(processClassContext, facetHolder);
+    }
+
+    // -- UTILITY
+
+    protected static ResolvedMethod findMethodExactOrFail(final Class<?> type, 
final String methodName, final Class<?>[] paramTypes) {
+        return _Utils.findMethodExactOrFail(type, methodName, paramTypes);
+    }
+
+    protected static ResolvedMethod findMethodExactOrFail(final Class<?> type, 
final String methodName) {
+        return _Utils.findMethodExactOrFail(type, methodName);
+    }
+
+    protected static Optional<ResolvedMethod> findMethodExact(final Class<?> 
type, final String methodName) {
+        return _Utils.findMethodExact(type, methodName);
+    }
+
+    private FacetHolder actionFacetHolder(final Class<?> declaringClass, final 
String memberId, final Class<?>[] paramTypes) {
+        return FacetHolder.simple(getMetaModelContext(),
+                Identifier.actionIdentifier(LogicalType.fqcn(declaringClass), 
memberId, paramTypes));
+    }
+
+    private FacetHolder propertyFacetHolder(final Class<?> declaringClass, 
final String memberId) {
+        return FacetHolder.simple(getMetaModelContext(),
+                
Identifier.propertyIdentifier(LogicalType.fqcn(declaringClass), memberId));
+    }
+
+    private FacetHolder collectionFacetHolder(final Class<?> declaringClass, 
final String memberId) {
+        return FacetHolder.simple(getMetaModelContext(),
+                
Identifier.collectionIdentifier(LogicalType.fqcn(declaringClass), memberId));
+    }
+
+    // -- EXPECTATIONS
+
+    protected final void assertNoMethodsRemoved() {
+        assertTrue(methodRemover.removedMethodMethodCalls().isEmpty());
+        assertTrue(methodRemover.removeMethodArgsCalls().isEmpty());
+    }
+
+    protected final void assertMethodWasRemoved(final ResolvedMethod method) {
+        assertTrue(methodRemover.removedMethodMethodCalls().contains(method),
+                ()->String.format("method was not removed in test scenario: 
%s", method));
+    }
+
+    protected final void assertMethodWasRemoved(final Class<?> type, final 
String methodName) {
+        assertMethodWasRemoved(findMethodExactOrFail(type, methodName));
+    }
+
+    protected final void assertMethodEqualsFirstIn(
+            final @NonNull ResolvedMethod method,
+            final @NonNull ImperativeFacet imperativeFacet) {
+        _Utils.assertMethodEquals(method, 
imperativeFacet.getMethods().getFirstElseFail().asMethodElseFail());
+    }
+
+}
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactoryTest.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactoryTest.java
new file mode 100644
index 00000000000..deb67cba2de
--- /dev/null
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactoryTest.java
@@ -0,0 +1,768 @@
+/*
+ *  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.causeway.core.metamodel.facets.object.domainobject;
+
+import java.util.UUID;
+
+import jakarta.inject.Named;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.springframework.boot.test.util.TestPropertyValues;
+
+import org.apache.causeway.applib.annotation.Bounding;
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.DomainService;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.id.LogicalType;
+import org.apache.causeway.applib.mixins.system.HasInteractionId;
+import org.apache.causeway.commons.collections.Can;
+import 
org.apache.causeway.core.config.metamodel.facets.DomainObjectConfigOptions;
+import org.apache.causeway.core.metamodel.facetapi.Facet;
+import org.apache.causeway.core.metamodel.facets.FacetFactoryTestAbstract;
+import 
org.apache.causeway.core.metamodel.facets.object.autocomplete.AutoCompleteFacet;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobject.autocomplete.AutoCompleteFacetForDomainObjectAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobject.choices.ChoicesFacetForDomainObjectAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobject.editing.ImmutableFacetForDomainObjectAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobject.editing.ImmutableFacetForDomainObjectAnnotationAsConfigured;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobject.editing.ImmutableFacetFromConfiguration;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobject.entitychangepublishing.EntityChangePublishingFacetForDomainObjectAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobject.entitychangepublishing.EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobject.entitychangepublishing.EntityChangePublishingFacetFromConfiguration;
+import 
org.apache.causeway.core.metamodel.facets.object.immutable.ImmutableFacet;
+import 
org.apache.causeway.core.metamodel.facets.object.logicaltype.AliasedFacet;
+import 
org.apache.causeway.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet;
+import 
org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacet;
+import 
org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacetForDomainObjectAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.objectvalue.choices.ChoicesFacet;
+import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailures;
+import org.apache.causeway.core.mmtestsupport.MetaModelContext_forTesting;
+
+abstract class DomainObjectAnnotationFacetFactoryTest
+extends FacetFactoryTestAbstract {
+
+    DomainObjectAnnotationFacetFactory facetFactory;
+
+    @AfterEach
+    protected void tearDown() throws Exception {
+        facetFactory = null;
+    }
+
+    static class Customer {
+    }
+
+    class SomeHasInteractionId implements HasInteractionId {
+
+        @Override
+        public UUID getInteractionId() {
+            return null;
+        }
+
+    }
+
+    @Override
+    protected void setup() {
+        //overrides default setup method to do nothing
+    }
+
+    void allowingEntityChangePublishingToReturn(final 
DomainObjectConfigOptions.EntityChangePublishingPolicy value) {
+        var testPropertyValues = value!=null
+            ? 
TestPropertyValues.of("causeway.applib.annotation.domainObject.entityChangePublishing="
 + value.name())
+            : TestPropertyValues.empty();
+        super.setup(builder->builder.testPropertyValues(testPropertyValues));
+        facetFactory = new 
DomainObjectAnnotationFacetFactory(getMetaModelContext());
+    }
+    void allowingObjectsEditingToReturn(final 
DomainObjectConfigOptions.EditingObjectsConfiguration value) {
+        var testPropertyValues = value!=null
+            ? 
TestPropertyValues.of("causeway.applib.annotation.domainObject.editing=" + 
value.name())
+            : TestPropertyValues.empty();
+        super.setup(builder->builder.testPropertyValues(testPropertyValues));
+        facetFactory = new 
DomainObjectAnnotationFacetFactory(getMetaModelContext());
+    }
+    protected void ignoringConfiguration() {
+        super.setup();
+        facetFactory = new 
DomainObjectAnnotationFacetFactory(getMetaModelContext());
+    }
+
+    @Nested
+    public class EntityChangePublishing  {
+
+        @DomainObject(entityChangePublishing = 
org.apache.causeway.applib.annotation.Publishing.AS_CONFIGURED)
+        class CustomerWithDomainObjectAndAuditingSetToAsConfigured {
+        }
+
+        @DomainObject(entityChangePublishing = 
org.apache.causeway.applib.annotation.Publishing.DISABLED)
+        class CustomerWithDomainObjectAndAuditingSetToDisabled {
+        }
+
+        @DomainObject(entityChangePublishing = 
org.apache.causeway.applib.annotation.Publishing.ENABLED)
+        class CustomerWithDomainObjectAndAuditingSetToEnabled {
+        }
+
+        @DomainObject(entityChangePublishing = 
Publishing.ENABLED_FOR_UPDATES_ONLY)
+        class 
CustomerWithDomainObjectAndEntityChangePublishingSetToEnabledForUpdatesOnly {
+        }
+
+        @Nested
+        public class WhenNotAnnotatedAndDefaultsFromConfiguration {
+
+            @Test
+            void configured_value_set_to_all() {
+                
allowingEntityChangePublishingToReturn(DomainObjectConfigOptions.EntityChangePublishingPolicy.ALL);
+                
objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.processEntityChangePublishing(
+                            
processClassContext.synthesizeOnType(DomainObject.class), processClassContext);
+
+                    final EntityChangePublishingFacet facet = 
facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertThat(facet, is(notNullValue()));
+                    assertTrue(facet instanceof 
EntityChangePublishingFacetFromConfiguration);
+                    assertThat(facet.isEnabled(), is(true));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+            @Test
+            void configured_value_set_to_none() {
+                
allowingEntityChangePublishingToReturn(DomainObjectConfigOptions.EntityChangePublishingPolicy.NONE);
+                
objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final EntityChangePublishingFacet facet = 
facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertNotNull(facet);
+                    assertThat(facet.isEnabled(), is(false));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithAuditingSetToAsConfigured {
+
+            @Test
+            public void configured_value_set_to_all() {
+                
allowingEntityChangePublishingToReturn(DomainObjectConfigOptions.EntityChangePublishingPolicy.ALL);
+                
objectScenario(CustomerWithDomainObjectAndAuditingSetToAsConfigured.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final EntityChangePublishingFacet facet = 
facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof 
EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured);
+                    assertThat(facet.isEnabled(), is(true));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+            @Test
+            public void configured_value_set_to_none() {
+                
allowingEntityChangePublishingToReturn(DomainObjectConfigOptions.EntityChangePublishingPolicy.NONE);
+                
objectScenario(CustomerWithDomainObjectAndAuditingSetToAsConfigured.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final EntityChangePublishingFacet facet = 
facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertNotNull(facet);
+                    assertThat(facet.isEnabled(), is(false));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithAuditingSetToEnabled {
+
+            @Test
+            public void irrespective_of_configured_value() {
+                allowingEntityChangePublishingToReturn(null);
+                
objectScenario(CustomerWithDomainObjectAndAuditingSetToEnabled.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = 
facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof 
EntityChangePublishingFacetForDomainObjectAnnotation);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+        }
+
+        @Nested
+        public class 
WithDomainObjectAnnotationWithEntityChangePublishingSetToEnabledForUpdatesOnly {
+
+            @Test
+            public void irrespective_of_configured_value() {
+                allowingEntityChangePublishingToReturn(null);
+                
objectScenario(CustomerWithDomainObjectAndEntityChangePublishingSetToEnabledForUpdatesOnly.class,
 (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = 
facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof 
EntityChangePublishingFacetForDomainObjectAnnotation);
+
+                    
assertFalse(EntityChangePublishingFacet.isPublishingEnabledForCreate(facetHolder));
+                    
assertTrue(EntityChangePublishingFacet.isPublishingEnabledForUpdate(facetHolder));
+                    
assertFalse(EntityChangePublishingFacet.isPublishingEnabledForDelete(facetHolder));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithAuditingSetToDisabled {
+
+            @Test
+            public void irrespective_of_configured_value() {
+                
allowingEntityChangePublishingToReturn(DomainObjectConfigOptions.EntityChangePublishingPolicy.ALL);
+                
objectScenario(CustomerWithDomainObjectAndAuditingSetToDisabled.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    
assertFalse(EntityChangePublishingFacet.isPublishingEnabled(facetHolder));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+        }
+    }
+
+    @Nested
+    public class AutoComplete {
+
+        class CustomerRepository {
+            public String lookup(final String x) { return null; }
+        }
+
+        class CustomerRepositoryWithDefaultMethodName {
+            public String autoComplete(final String x) { return null; }
+        }
+
+        @DomainObject(autoCompleteRepository = CustomerRepository.class, 
autoCompleteMethod = "lookup")
+        class CustomerWithDomainObjectAndAutoCompleteRepositoryAndAction {
+        }
+
+        @DomainObject(autoCompleteRepository = 
CustomerRepositoryWithDefaultMethodName.class)
+        class CustomerWithDomainObjectAndAutoCompleteRepository {
+        }
+
+        @DomainObject
+        class CustomerWithDomainObjectButNoAutoCompleteRepository {
+        }
+
+        @BeforeEach
+        public void setUp() {
+            ignoringConfiguration();
+        }
+
+        @Test
+        public void whenDomainObjectAndAutoCompleteRepositoryAndAction() {
+
+            
objectScenario(CustomerWithDomainObjectAndAutoCompleteRepositoryAndAction.class,
 (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = 
facetHolder.getFacet(AutoCompleteFacet.class);
+                assertNotNull(facet);
+
+                assertTrue(facet instanceof 
AutoCompleteFacetForDomainObjectAnnotation);
+
+                final AutoCompleteFacetForDomainObjectAnnotation 
autoCompleteFacet = (AutoCompleteFacetForDomainObjectAnnotation) facet;
+
+                
assertThat(CustomerRepository.class.isAssignableFrom(autoCompleteFacet.getRepositoryClass()),
 is(true));
+                assertThat(autoCompleteFacet.getActionName(), is("lookup"));
+
+                assertNoMethodsRemoved();
+            });
+
+        }
+
+        @Test
+        public void whenDomainObjectAndAutoCompleteRepository() {
+
+            
objectScenario(CustomerWithDomainObjectAndAutoCompleteRepository.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = 
facetHolder.getFacet(AutoCompleteFacet.class);
+                assertNotNull(facet);
+
+                assertTrue(facet instanceof 
AutoCompleteFacetForDomainObjectAnnotation);
+
+                final AutoCompleteFacetForDomainObjectAnnotation 
autoCompleteFacet = (AutoCompleteFacetForDomainObjectAnnotation) facet;
+
+                
assertThat(CustomerRepositoryWithDefaultMethodName.class.isAssignableFrom(autoCompleteFacet.getRepositoryClass()),
 is(true));
+                assertThat(autoCompleteFacet.getActionName(), 
is("autoComplete"));
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenDomainObjectAnnotationButNoAutoComplete() {
+
+            
objectScenario(CustomerWithDomainObjectButNoAutoCompleteRepository.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = 
facetHolder.getFacet(AutoCompleteFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenNoDomainObjectAnnotation() {
+
+            
objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = 
facetHolder.getFacet(AutoCompleteFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+    }
+
+    @Nested
+    public class Bounded {
+
+        @DomainObject(bounding = Bounding.BOUNDED)
+        class CustomerWithDomainObjectAndBoundedSetToTrue {
+        }
+
+        @DomainObject(bounding = Bounding.UNBOUNDED)
+        class CustomerWithDomainObjectAndBoundedSetToFalse {
+        }
+
+        @DomainObject
+        class CustomerWithDomainObjectButNoBounded {
+        }
+
+        @BeforeEach
+        public void setUp() {
+            ignoringConfiguration();
+        }
+
+        @Test
+        public void whenDomainObjectAndBoundedSetToTrue() {
+
+            objectScenario(CustomerWithDomainObjectAndBoundedSetToTrue.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ChoicesFacet.class);
+                assertNotNull(facet);
+
+                assertTrue(facet instanceof 
ChoicesFacetForDomainObjectAnnotation);
+
+                assertNoMethodsRemoved();
+
+            });
+        }
+
+        @Test
+        public void whenDomainObjectAndAutoCompleteRepository() {
+
+            objectScenario(CustomerWithDomainObjectAndBoundedSetToFalse.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ChoicesFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenNoDomainObjectAnnotation() {
+
+            
objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ChoicesFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+    }
+
+
+    @DomainObject(editing = 
org.apache.causeway.applib.annotation.Editing.AS_CONFIGURED)
+    static class CustomerWithDomainObjectAndEditingSetToAsConfigured {
+    }
+
+    @DomainObject(editing = 
org.apache.causeway.applib.annotation.Editing.DISABLED)
+    static class CustomerWithDomainObjectAndEditingSetToDisabled {
+    }
+
+    @DomainObject(editing = 
org.apache.causeway.applib.annotation.Editing.ENABLED)
+    static class CustomerWithDomainObjectAndEditingSetToEnabled {
+    }
+
+    @Nested
+    public class Editing {
+
+        @Nested
+        public class WhenNotAnnotatedAndDefaultsFromConfiguration {
+
+            @Test
+            public void configured_value_set_to_true() {
+                
allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.TRUE);
+
+                
objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = 
facetHolder.getFacet(ImmutableFacet.class);
+                    assertNull(facet);
+
+                    assertNoMethodsRemoved();
+
+                });
+            }
+
+            @Test
+            public void configured_value_set_to_false() {
+                
allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.FALSE);
+
+                
objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = 
facetHolder.getFacet(ImmutableFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof 
ImmutableFacetFromConfiguration);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+            @Test
+            public void configured_value_set_to_defaults() {
+                ignoringConfiguration();
+                
objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = 
facetHolder.getFacet(ImmutableFacet.class);
+                    assertNotNull(facet); // default is now non-editable
+                    assertTrue(facet instanceof 
ImmutableFacetFromConfiguration);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithEditingSetToAsConfigured {
+
+            @Test
+            public void configured_value_set_to_true() {
+                
allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.TRUE);
+                
objectScenario(CustomerWithDomainObjectAndEditingSetToAsConfigured.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = 
facetHolder.getFacet(ImmutableFacet.class);
+                    assertNull(facet);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+            @Test
+            public void configured_value_set_to_false() {
+                
allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.FALSE);
+                
objectScenario(CustomerWithDomainObjectAndEditingSetToAsConfigured.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = 
facetHolder.getFacet(ImmutableFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof 
ImmutableFacetForDomainObjectAnnotation);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+            @Test
+            public void configured_value_set_to_defaults() {
+                ignoringConfiguration();
+                
objectScenario(CustomerWithDomainObjectAndEditingSetToAsConfigured.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = 
facetHolder.getFacet(ImmutableFacet.class);
+                    assertNotNull(facet); // default is now non-editable
+                    assertTrue(facet instanceof 
ImmutableFacetForDomainObjectAnnotationAsConfigured);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithEditingSetToEnabled {
+
+            @Test
+            public void irrespective_of_configured_value() {
+                
allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.FALSE);
+                
objectScenario(CustomerWithDomainObjectAndEditingSetToEnabled.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final ImmutableFacet facet = 
facetHolder.getFacet(ImmutableFacet.class);
+                    assertNull(facet);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithEditingSetToDisabled {
+
+            @Test
+            public void irrespective_of_configured_value() {
+                
allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.TRUE);
+                
objectScenario(CustomerWithDomainObjectAndEditingSetToDisabled.class, 
(processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = 
facetHolder.getFacet(ImmutableFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof 
ImmutableFacetForDomainObjectAnnotation);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+        }
+    }
+
+    @Named("CUS")
+    @DomainObject
+    static class LogicalTypeNameCustomerWithDomainObjectAndObjectTypeSet {
+    }
+
+    @DomainObject
+    static class LogicalTypeNameCustomerWithDomainObjectButNoObjectType {
+    }
+
+    @Nested
+    public class LogicalTypeName {
+
+        @BeforeEach
+        public void setUp() {
+            ignoringConfiguration();
+        }
+
+        @Test
+        public void whenDomainObjectAndObjectTypeSetToTrue() {
+            
assertThat(LogicalType.infer(LogicalTypeNameCustomerWithDomainObjectAndObjectTypeSet.class).logicalName(),
+                    is("CUS"));
+            assertNoMethodsRemoved();
+        }
+
+        @Test
+        public void whenDomainObjectAndObjectTypeNotSet() {
+
+            
objectScenario(LogicalTypeNameCustomerWithDomainObjectButNoObjectType.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(AliasedFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenNoDomainObjectAnnotation() {
+
+            
objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(AliasedFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+    }
+
+    @DomainObject(nature = org.apache.causeway.applib.annotation.Nature.ENTITY)
+    static class CustomerWithDomainObjectAndNatureSetToJdoEntity {
+    }
+
+    @DomainObject(nature = 
org.apache.causeway.applib.annotation.Nature.NOT_SPECIFIED)
+    static class CustomerWithDomainObjectAndNatureSetToNotSpecified {
+    }
+
+    @DomainObject(nature = 
org.apache.causeway.applib.annotation.Nature.VIEW_MODEL)
+    static class CustomerWithDomainObjectAndNatureSetToViewModel {
+    }
+
+    @DomainObject
+    static class CustomerWithDomainObjectButNoNature {
+    }
+
+    @Nested
+    public class Nature  {
+
+        @BeforeEach
+        public void setUp() {
+            ignoringConfiguration();
+        }
+
+        @Test
+        public void whenDomainObjectAndNatureSetToJdoEntity() {
+
+            
objectScenario(CustomerWithDomainObjectAndNatureSetToJdoEntity.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ViewModelFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenDomainObjectAndNatureSetToNotSpecified() {
+
+            
objectScenario(CustomerWithDomainObjectAndNatureSetToNotSpecified.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ViewModelFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenDomainObjectAndNatureSetToViewModel() {
+
+            
objectScenario(CustomerWithDomainObjectAndNatureSetToViewModel.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ViewModelFacet.class);
+                assertNotNull(facet);
+
+                assertTrue(facet instanceof 
ViewModelFacetForDomainObjectAnnotation);
+
+                assertNoMethodsRemoved();
+            });
+
+        }
+
+        @Test
+        public void whenNoDomainObjectAnnotation() {
+
+            
objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, 
(processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ViewModelFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+
+        }
+
+    }
+
+    @Named("object.name")
+    @DomainObject(aliased = {"object.name", "object.alias"})
+    static class AliasDomainObjectWithAliases {
+    }
+
+    @Named("service.name")
+    @DomainService(aliased = {"service.name", "service.alias"})
+    static class AliasDomainServiceWithAliases {
+    }
+
+    @Nested
+    public class Alias {
+        DomainObjectAnnotationFacetFactory facetFactory;
+
+        @Test
+        public void testValidationDomainObjectWithAliasesConfigured() {
+            metaModelContext = MetaModelContext_forTesting.builder()
+                
.testPropertyValues(TestPropertyValues.of("causeway.core.metaModel.validator.allowLogicalTypeNameAsAlias=true"))
+                .refiners(Can.of(__->facetFactory))
+                .build();
+            facetFactory = new 
DomainObjectAnnotationFacetFactory(getMetaModelContext());
+            ((MetaModelContext_forTesting) 
getMetaModelContext()).getProgrammingModel();//kicks off the programming model 
factory
+
+            
getMetaModelContext().getSpecificationLoader().specForTypeElseFail(AliasDomainObjectWithAliases.class);
+            ValidationFailures validationFailures = 
getMetaModelContext().getSpecificationLoader().getOrAssessValidationResult();
+            assertFalse(validationFailures.hasFailures());
+        }
+
+        @Test
+        public void testValidationDomainServiceWithAliasesConfigured() {
+            metaModelContext = MetaModelContext_forTesting.builder()
+                
.testPropertyValues(TestPropertyValues.of("causeway.core.metaModel.validator.allowLogicalTypeNameAsAlias=true"))
+                .refiners(Can.of(__->facetFactory))
+                .build();
+            facetFactory = new 
DomainObjectAnnotationFacetFactory(getMetaModelContext());
+            ((MetaModelContext_forTesting) 
getMetaModelContext()).getProgrammingModel();//kicks off the programming model 
factory
+
+            
getMetaModelContext().getSpecificationLoader().specForTypeElseFail(AliasDomainServiceWithAliases.class);
+            ValidationFailures validationFailures = 
getMetaModelContext().getSpecificationLoader().getOrAssessValidationResult();
+            assertFalse(validationFailures.hasFailures());
+        }
+        @Test
+        public void testValidationDomainObjectWithAliasesDefault() {
+            metaModelContext = MetaModelContext_forTesting.builder()
+                .refiners(Can.of(__->facetFactory))
+                .build();
+            facetFactory = new 
DomainObjectAnnotationFacetFactory(getMetaModelContext());
+            ((MetaModelContext_forTesting) 
getMetaModelContext()).getProgrammingModel();//kicks off the programming model 
factory
+
+            
getMetaModelContext().getSpecificationLoader().specForTypeElseFail(AliasDomainObjectWithAliases.class);
+            ValidationFailures validationFailures = 
getMetaModelContext().getSpecificationLoader().getOrAssessValidationResult();
+            assertTrue(validationFailures.hasFailures());
+        }
+
+        @Test
+        public void testValidationDomainServiceWithAliasesDefault() {
+            metaModelContext = MetaModelContext_forTesting.builder()
+                .refiners(Can.of(__->facetFactory))
+                .build();
+            facetFactory = new 
DomainObjectAnnotationFacetFactory(getMetaModelContext());
+            ((MetaModelContext_forTesting) 
getMetaModelContext()).getProgrammingModel();//kicks off the programming model 
factory
+
+            
getMetaModelContext().getSpecificationLoader().specForTypeElseFail(AliasDomainServiceWithAliases.class);
+            ValidationFailures validationFailures = 
getMetaModelContext().getSpecificationLoader().getOrAssessValidationResult();
+            assertTrue(validationFailures.hasFailures());
+        }
+    }
+}
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/properties/property/PropertyAnnotationFacetFactoryTest.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/properties/property/PropertyAnnotationFacetFactoryTest.java
new file mode 100644
index 00000000000..17bbf6dab49
--- /dev/null
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/properties/property/PropertyAnnotationFacetFactoryTest.java
@@ -0,0 +1,885 @@
+/*
+ *  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.causeway.core.metamodel.facets.properties.property;
+
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.mockito.Mockito;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.MemberSupport;
+import org.apache.causeway.applib.annotation.Nature;
+import org.apache.causeway.applib.annotation.Optionality;
+import org.apache.causeway.applib.annotation.Property;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.Snapshot;
+import org.apache.causeway.applib.annotation.Where;
+import org.apache.causeway.applib.events.domain.PropertyDomainEvent;
+import org.apache.causeway.applib.spec.Specification;
+import org.apache.causeway.core.metamodel.commons.matchers.CausewayMatchers;
+import org.apache.causeway.core.metamodel.consent.Consent.VetoReason;
+import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.facetapi.FacetUtil;
+import 
org.apache.causeway.core.metamodel.facets.DomainEventFacetAbstract.EventTypeOrigin;
+import org.apache.causeway.core.metamodel.facets.FacetFactory;
+import org.apache.causeway.core.metamodel.facets.FacetFactoryTestAbstract;
+import org.apache.causeway.core.metamodel.facets.FacetedMethod;
+import 
org.apache.causeway.core.metamodel.facets.members.disabled.DisabledFacet;
+import 
org.apache.causeway.core.metamodel.facets.objectvalue.mandatory.MandatoryFacet;
+import 
org.apache.causeway.core.metamodel.facets.objectvalue.maxlen.MaxLengthFacet;
+import 
org.apache.causeway.core.metamodel.facets.objectvalue.mustsatisfyspec.MustSatisfySpecificationFacet;
+import org.apache.causeway.core.metamodel.facets.objectvalue.regex.RegExFacet;
+import 
org.apache.causeway.core.metamodel.facets.propcoll.accessor.PropertyOrCollectionAccessorFacetAbstract;
+import 
org.apache.causeway.core.metamodel.facets.propcoll.memserexcl.SnapshotExcludeFacet;
+import 
org.apache.causeway.core.metamodel.facets.properties.property.disabled.DisabledFacetForPropertyAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.properties.property.entitychangepublishing.EntityPropertyChangePublishingPolicyFacet;
+import 
org.apache.causeway.core.metamodel.facets.properties.property.mandatory.MandatoryFacetForPropertyAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.properties.property.maxlength.MaxLengthFacetForPropertyAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.properties.property.modify.PropertyDomainEventFacet;
+import 
org.apache.causeway.core.metamodel.facets.properties.property.modify.PropertyModifyFacetAbstract;
+import 
org.apache.causeway.core.metamodel.facets.properties.property.mustsatisfy.MustSatisfySpecificationFacetForPropertyAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.properties.property.regex.RegExFacetForPropertyAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.properties.property.snapshot.SnapshotExcludeFacetForPropertyAnnotation;
+import 
org.apache.causeway.core.metamodel.facets.properties.update.clear.PropertyClearFacet;
+import 
org.apache.causeway.core.metamodel.facets.properties.update.clear.PropertyClearFacetAbstract;
+import 
org.apache.causeway.core.metamodel.facets.properties.update.modify.PropertySetterFacet;
+import 
org.apache.causeway.core.metamodel.facets.properties.update.modify.PropertySetterFacetAbstract;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import 
org.apache.causeway.core.metamodel.postprocessors.members.SynthesizeDomainEventsForMixinPostProcessor;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+class PropertyAnnotationFacetFactoryTest extends FacetFactoryTestAbstract {
+
+    PropertyAnnotationFacetFactory facetFactory;
+
+    private static void processDomainEvent(
+            final PropertyAnnotationFacetFactory facetFactory, final 
FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processDomainEvent(processMethodContext, propertyIfAny);
+    }
+
+    private static void processOptional(
+            final PropertyAnnotationFacetFactory facetFactory, final 
FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processOptional(processMethodContext, propertyIfAny);
+    }
+
+    private static void processRegEx(
+            final PropertyAnnotationFacetFactory facetFactory, final 
FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processRegEx(processMethodContext, propertyIfAny);
+    }
+
+    private static void processEditing(
+            final PropertyAnnotationFacetFactory facetFactory, final 
FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processEditing(processMethodContext, propertyIfAny);
+    }
+
+    private static void processMaxLength(
+            final PropertyAnnotationFacetFactory facetFactory, final 
FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processMaxLength(processMethodContext, propertyIfAny);
+    }
+
+    private static void processMustSatisfy(
+            final PropertyAnnotationFacetFactory facetFactory, final 
FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processMustSatisfy(processMethodContext, propertyIfAny);
+    }
+
+    private static void processSnapshot(
+            final PropertyAnnotationFacetFactory facetFactory, final 
FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processSnapshot(processMethodContext, propertyIfAny);
+    }
+
+    private static void processEntityPropertyChangePublishing(
+            final PropertyAnnotationFacetFactory facetFactory, final 
FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        
facetFactory.processEntityPropertyChangePublishing(processMethodContext, 
propertyIfAny);
+    }
+
+    @BeforeEach
+    final void setUp() throws Exception {
+        facetFactory = new 
PropertyAnnotationFacetFactory(getMetaModelContext());
+    }
+
+    @AfterEach
+    final void tearDown() throws Exception {
+        facetFactory = null;
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class Modify extends PropertyAnnotationFacetFactoryTest {
+
+        private void addGetterFacet(final FacetHolder holder) {
+            var mockOnType = Mockito.mock(ObjectSpecification.class);
+            FacetUtil.addFacet(new 
PropertyOrCollectionAccessorFacetAbstract(mockOnType, holder) {
+                @Override
+                public Object getAssociationValueAsPojo(
+                        final ManagedObject inObject,
+                        final InteractionInitiatedBy interactionInitiatedBy) {
+                    return null;
+                }
+            });
+        }
+
+        private void addSetterFacet(final FacetHolder holder) {
+            FacetUtil.addFacet(new PropertySetterFacetAbstract(holder) {
+                @Override
+                public ManagedObject setProperty(
+                        final OneToOneAssociation owningAssociation,
+                        final ManagedObject inObject,
+                        final ManagedObject value,
+                        final InteractionInitiatedBy interactionInitiatedBy) {
+                    return inObject;
+                }
+            });
+        }
+
+        private void addClearFacet(final FacetHolder holder) {
+            FacetUtil.addFacet(new PropertyClearFacetAbstract(holder) {
+                @Override
+                public ManagedObject clearProperty(
+                        final OneToOneAssociation owningProperty,
+                        final ManagedObject targetAdapter,
+                        final InteractionInitiatedBy interactionInitiatedBy) {
+                    return targetAdapter;
+                }
+            });
+        }
+
+        private void assertHasPropertyDomainEventFacet(
+                final FacetedMethod facetedMethod,
+                final EventTypeOrigin eventTypeOrigin,
+                final Class<? extends PropertyDomainEvent<?,?>> eventType) {
+            var domainEventFacet = 
facetedMethod.lookupFacet(PropertyDomainEventFacet.class).orElseThrow();
+            assertEquals(eventTypeOrigin, 
domainEventFacet.getEventTypeOrigin());
+            assertThat(domainEventFacet.getEventType(), 
CausewayMatchers.classEqualTo(eventType));
+
+            if(facetedMethod.methodFacade().getName().equals("prop"))
+                return; // skip further checks, when in a mixed-in scenario
+
+            // then
+            var setterFacet = 
facetedMethod.getFacet(PropertySetterFacet.class);
+            assertNotNull(setterFacet);
+            assertTrue(setterFacet instanceof PropertyModifyFacetAbstract, 
"unexpected facet: " + setterFacet);
+            final PropertyModifyFacetAbstract setterFacetImpl = 
(PropertyModifyFacetAbstract) setterFacet;
+            assertEquals(eventTypeOrigin, 
setterFacetImpl.getEventTypeOrigin());
+            assertThat(setterFacetImpl.getEventType(), 
CausewayMatchers.classEqualTo(eventType));
+
+            // then
+            var clearFacet = facetedMethod.getFacet(PropertyClearFacet.class);
+            assertNotNull(clearFacet);
+            assertTrue(clearFacet instanceof PropertyModifyFacetAbstract);
+            final PropertyModifyFacetAbstract clearFacetImpl = 
(PropertyModifyFacetAbstract) clearFacet;
+            assertEquals(eventTypeOrigin, 
setterFacetImpl.getEventTypeOrigin());
+            assertThat(clearFacetImpl.getEventType(), 
CausewayMatchers.classEqualTo(eventType));
+        }
+
+        @Test
+        void withPropertyDomainEvent_fallingBackToDefault() {
+
+            class Customer {
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                addGetterFacet(facetedMethod);
+                addSetterFacet(facetedMethod);
+                addClearFacet(facetedMethod);
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.DEFAULT, 
PropertyDomainEvent.Default.class);
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_annotatedOnMethod() {
+
+            class Customer {
+                class NamedChangedDomainEvent extends 
PropertyDomainEvent<Customer, String> {}
+                @Property(domainEvent = NamedChangedDomainEvent.class)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                addGetterFacet(facetedMethod);
+                addSetterFacet(facetedMethod);
+                addClearFacet(facetedMethod);
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, 
Customer.NamedChangedDomainEvent.class);
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_annotatedOnType() {
+
+            @DomainObject(propertyDomainEvent = 
Customer.NamedChangedDomainEvent.class)
+            class Customer {
+                class NamedChangedDomainEvent extends 
PropertyDomainEvent<Customer, String> {}
+                @Property
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                addGetterFacet(facetedMethod);
+                addSetterFacet(facetedMethod);
+                addClearFacet(facetedMethod);
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_OBJECT, 
Customer.NamedChangedDomainEvent.class);
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_annotatedOnTypeAndMethod() {
+
+            @DomainObject(propertyDomainEvent = 
Customer.NamedChangedDomainEvent1.class)
+            class Customer {
+                class NamedChangedDomainEvent1 extends 
PropertyDomainEvent<Customer, String> {}
+                class NamedChangedDomainEvent2 extends 
PropertyDomainEvent<Customer, String> {}
+                @Property(domainEvent = NamedChangedDomainEvent2.class)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                addGetterFacet(facetedMethod);
+                addSetterFacet(facetedMethod);
+                addClearFacet(facetedMethod);
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+
+                // then - the property annotation should win
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, 
Customer.NamedChangedDomainEvent2.class);
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_mixedIn_annotatedOnMethod() {
+            var postProcessor = new 
SynthesizeDomainEventsForMixinPostProcessor(getMetaModelContext());
+
+            // given
+            class Customer {
+                class NamedChangedDomainEvent extends 
PropertyDomainEvent<Customer, String> {}
+            }
+            @DomainObject(nature=Nature.MIXIN, mixinMethod = "prop")
+            @RequiredArgsConstructor
+            @SuppressWarnings("unused")
+            class Customer_name {
+                final Customer mixee;
+                @Property(domainEvent = Customer.NamedChangedDomainEvent.class)
+                public String prop() { return "mixed-in name"; }
+            }
+
+            propertyScenarioMixedIn(Customer.class, Customer_name.class,
+                    (processMethodContext, mixeeSpec, facetedMethod, 
mixedInProp)->{
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+                postProcessor.postProcessProperty(mixeeSpec, mixedInProp);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, 
Customer.NamedChangedDomainEvent.class);
+
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_mixedIn_annotatedOnMixedInType() {
+            var postProcessor = new 
SynthesizeDomainEventsForMixinPostProcessor(getMetaModelContext());
+
+            // given
+            class Customer {
+                class NamedChangedDomainEvent extends 
PropertyDomainEvent<Customer, String> {}
+            }
+            @Property(domainEvent = Customer.NamedChangedDomainEvent.class)
+            @RequiredArgsConstructor
+            @SuppressWarnings("unused")
+            class Customer_name {
+                final Customer mixee;
+                @MemberSupport
+                public String prop() { return "mixed-in name"; }
+            }
+
+            propertyScenarioMixedIn(Customer.class, Customer_name.class,
+                    (processMethodContext, mixeeSpec, facetedMethod, 
mixedInProp)->{
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+                postProcessor.postProcessProperty(mixeeSpec, mixedInProp);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, 
Customer.NamedChangedDomainEvent.class);
+
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_mixedIn_annotatedOnMixeeType() {
+            var postProcessor = new 
SynthesizeDomainEventsForMixinPostProcessor(getMetaModelContext());
+
+            // given
+            @DomainObject(propertyDomainEvent = 
Customer.NamedChangedDomainEvent.class)
+            class Customer {
+                class NamedChangedDomainEvent extends 
PropertyDomainEvent<Customer, String> {}
+            }
+            @Property
+            @RequiredArgsConstructor
+            @SuppressWarnings("unused")
+            class Customer_name {
+                final Customer mixee;
+                @MemberSupport
+                public String prop() { return "mixed-in name"; }
+            }
+
+            propertyScenarioMixedIn(Customer.class, Customer_name.class,
+                    (processMethodContext, mixeeSpec, facetedMethod, 
mixedInProp)->{
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+                postProcessor.postProcessProperty(mixeeSpec, mixedInProp);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_OBJECT, 
Customer.NamedChangedDomainEvent.class);
+
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_mixedIn_annotatedOnMixeeAndMixedInType() {
+            var postProcessor = new 
SynthesizeDomainEventsForMixinPostProcessor(getMetaModelContext());
+
+            // given
+            @DomainObject(propertyDomainEvent = 
Customer.NamedChangedDomainEvent1.class)
+            class Customer {
+                class NamedChangedDomainEvent1 extends 
PropertyDomainEvent<Customer, String> {}
+                class NamedChangedDomainEvent2 extends 
PropertyDomainEvent<Customer, String> {}
+            }
+            @Property(domainEvent = Customer.NamedChangedDomainEvent2.class)
+            @RequiredArgsConstructor
+            @SuppressWarnings("unused")
+            class Customer_name {
+                final Customer mixee;
+                @MemberSupport
+                public String prop() { return "mixed-in name"; }
+            }
+
+            propertyScenarioMixedIn(Customer.class, Customer_name.class,
+                    (processMethodContext, mixeeSpec, facetedMethod, 
mixedInProp)->{
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+                postProcessor.postProcessProperty(mixeeSpec, mixedInProp);
+
+                // then - the mixed-in annotation should win
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, 
Customer.NamedChangedDomainEvent2.class);
+
+            });
+        }
+
+        @Test
+        void 
withPropertyDomainEvent_mixedIn_annotatedOnMixeeTypeAndMixedInMethod() {
+            var postProcessor = new 
SynthesizeDomainEventsForMixinPostProcessor(getMetaModelContext());
+
+            // given
+            @DomainObject(propertyDomainEvent = 
Customer.NamedChangedDomainEvent1.class)
+            class Customer {
+                class NamedChangedDomainEvent1 extends 
PropertyDomainEvent<Customer, String> {}
+                class NamedChangedDomainEvent2 extends 
PropertyDomainEvent<Customer, String> {}
+            }
+            @DomainObject(nature=Nature.MIXIN, mixinMethod = "prop")
+            @RequiredArgsConstructor
+            @SuppressWarnings("unused")
+            class Customer_name {
+                final Customer mixee;
+                @Property(domainEvent = 
Customer.NamedChangedDomainEvent2.class)
+                public String prop() { return "mixed-in name"; }
+            }
+
+            propertyScenarioMixedIn(Customer.class, Customer_name.class,
+                    (processMethodContext, mixeeSpec, facetedMethod, 
mixedInProp)->{
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+                postProcessor.postProcessProperty(mixeeSpec, mixedInProp);
+
+                // then - the mixed-in annotation should win
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, 
Customer.NamedChangedDomainEvent2.class);
+
+            });
+        }
+
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class Editing extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void withAnnotationOnGetter() {
+            @SuppressWarnings("unused")
+            class Customer {
+                @Property(
+                        editing = 
org.apache.causeway.applib.annotation.Editing.DISABLED,
+                        editingDisabledReason = "you cannot edit the name 
property")
+                public String getName() { return null; }
+                public void setName(final String name) {}
+            }
+
+            assertDisabledFacetOn(Customer.class, "name",
+                    "you cannot edit the name property");
+        }
+
+        @Test
+        void withAnnotationOnField() {
+
+            class Customer {
+                @Property(
+                        editing = 
org.apache.causeway.applib.annotation.Editing.DISABLED,
+                        editingDisabledReason = "you cannot edit the name 
property")
+                @Getter @Setter
+                private String name;
+            }
+
+            assertDisabledFacetOn(Customer.class, "name",
+                    "you cannot edit the name property");
+        }
+
+        @Test
+        void withAnnotationOnBooleanGetter() {
+            @SuppressWarnings("unused")
+            class Customer {
+                @Property(
+                        editing = 
org.apache.causeway.applib.annotation.Editing.DISABLED,
+                        editingDisabledReason = "you cannot edit the 
subscribed property"
+                        )
+                public boolean isSubscribed() { return true; }
+                public void setSubscribed(final boolean b) {}
+            }
+
+            assertDisabledFacetOn(Customer.class, "subscribed",
+                    "you cannot edit the subscribed property");
+        }
+
+        @Test
+        void withAnnotationOnBooleanField() {
+
+            class Customer {
+                @Property(
+                        editing = 
org.apache.causeway.applib.annotation.Editing.DISABLED,
+                        editingDisabledReason = "you cannot edit the 
subscribed property"
+                        )
+                @Getter @Setter
+                private boolean subscribed;
+            }
+
+            assertDisabledFacetOn(Customer.class, "subscribed",
+                    "you cannot edit the subscribed property");
+        }
+
+        // -- SPECIAL SCENARIO CAUSEWAY-2963
+
+        static interface PrimitiveBooleanHolder {
+            @Property(
+                    editing = 
org.apache.causeway.applib.annotation.Editing.DISABLED,
+                    editingDisabledReason = "a")
+            boolean isReadWriteProperty();
+            void setReadWriteProperty(boolean c);
+        }
+
+        static class PrimitiveBooleanEntity implements PrimitiveBooleanHolder {
+            @Property(
+                    editing = 
org.apache.causeway.applib.annotation.Editing.DISABLED,
+                    editingDisabledReason = "b")
+            @Getter @Setter
+            private boolean readWriteProperty;
+        }
+
+        @Test
+        void recognizeAnnotationOnPrimitiveBoolean() {
+            assertDisabledFacetOn(PrimitiveBooleanEntity.class, 
"readWriteProperty", "b");
+        }
+
+        // -- HELPER
+
+        private void assertDisabledFacetOn(final Class<?> declaringClass, 
final String propertyName, final String expectedDisabledReason) {
+
+            // given
+            propertyScenario(declaringClass, propertyName, 
(processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processEditing(facetFactory, processMethodContext);
+                // then
+                var disabledFacet = 
facetedMethod.getFacet(DisabledFacet.class);
+                assertNotNull(disabledFacet);
+                assertTrue(disabledFacet instanceof 
DisabledFacetForPropertyAnnotation);
+                var disabledFacet2 = (DisabledFacetForPropertyAnnotation) 
disabledFacet;
+                assertThat(disabledFacet.where(), is(Where.EVERYWHERE));
+                
assertThat(disabledFacet2.disabledReason(null).map(VetoReason::string).orElse(null),
 is(expectedDisabledReason));
+            });
+        }
+
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class MaxLength extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void withAnnotation() {
+
+            class Customer {
+                @Property(maxLength = 30)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processMaxLength(facetFactory, processMethodContext);
+
+                // then
+                final MaxLengthFacet maxLengthFacet = 
facetedMethod.getFacet(MaxLengthFacet.class);
+                assertNotNull(maxLengthFacet);
+                assertTrue(maxLengthFacet instanceof 
MaxLengthFacetForPropertyAnnotation);
+                assertThat(maxLengthFacet.value(), is(30));
+            });
+        }
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class MustSatisfy extends PropertyAnnotationFacetFactoryTest {
+
+        public static class NotTooHot implements Specification {
+            @Override
+            public String satisfies(final Object obj) {
+                return null;
+            }
+        }
+
+        public static class NotTooCold implements Specification {
+            @Override
+            public String satisfies(final Object obj) {
+                return null;
+            }
+        }
+
+        @Test
+        void withAnnotation() {
+
+            class Customer {
+                @Property(
+                        mustSatisfy = {NotTooHot.class, NotTooCold.class}
+                        )
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processMustSatisfy(facetFactory, processMethodContext);
+
+                // then
+                final MustSatisfySpecificationFacet 
mustSatisfySpecificationFacet = 
facetedMethod.getFacet(MustSatisfySpecificationFacet.class);
+                assertNotNull(mustSatisfySpecificationFacet);
+                assertTrue(mustSatisfySpecificationFacet instanceof 
MustSatisfySpecificationFacetForPropertyAnnotation);
+                final MustSatisfySpecificationFacetForPropertyAnnotation 
mustSatisfySpecificationFacetImpl = 
(MustSatisfySpecificationFacetForPropertyAnnotation) 
mustSatisfySpecificationFacet;
+                var specifications = 
mustSatisfySpecificationFacetImpl.getSpecifications();
+                assertThat(specifications.size(), is(2));
+
+                assertTrue(specifications.getElseFail(0) instanceof NotTooHot);
+                assertTrue(specifications.getElseFail(1) instanceof 
NotTooCold);
+            });
+        }
+
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class EntityPropertyChangePublishingPolicy extends 
PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void exclusion() {
+
+            class Customer {
+                @Property(entityChangePublishing = Publishing.DISABLED)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processEntityPropertyChangePublishing(facetFactory, 
processMethodContext);
+                // then
+                var changePolicyFacet = 
facetedMethod.lookupFacet(EntityPropertyChangePublishingPolicyFacet.class).orElse(null);
+                assertNotNull(changePolicyFacet);
+                assertTrue(changePolicyFacet.isPublishingVetoed());
+                assertFalse(changePolicyFacet.isPublishingAllowed());
+            });
+        }
+
+        @Test
+        void whenDefault() {
+
+            class Customer {
+                @Property
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processEntityPropertyChangePublishing(facetFactory, 
processMethodContext);
+                // then
+                var changePolicyFacet = 
facetedMethod.getFacet(EntityPropertyChangePublishingPolicyFacet.class);
+                assertNull(changePolicyFacet);
+            });
+        }
+
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class SnapshotExcluded extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void withAnnotation() {
+
+            class Customer {
+                @Property(snapshot = Snapshot.EXCLUDED)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processSnapshot(facetFactory, processMethodContext);
+                // then
+                final SnapshotExcludeFacet snapshotExcludeFacet = 
facetedMethod.getFacet(SnapshotExcludeFacet.class);
+                assertNotNull(snapshotExcludeFacet);
+                assertTrue(snapshotExcludeFacet instanceof 
SnapshotExcludeFacetForPropertyAnnotation);
+            });
+        }
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class Mandatory extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void whenOptionalityIsTrue() {
+
+            class Customer {
+                @Property(optionality = Optionality.OPTIONAL)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processOptional(facetFactory, processMethodContext);
+                // then
+                final MandatoryFacet mandatoryFacet = 
facetedMethod.getFacet(MandatoryFacet.class);
+                assertNotNull(mandatoryFacet);
+                assertTrue(mandatoryFacet instanceof 
MandatoryFacetForPropertyAnnotation.Optional);
+            });
+        }
+
+        @Test
+        void whenOptionalityIsFalse() {
+
+            class Customer {
+                @Property(optionality = Optionality.MANDATORY)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processOptional(facetFactory, processMethodContext);
+                // then
+                final MandatoryFacet mandatoryFacet = 
facetedMethod.getFacet(MandatoryFacet.class);
+                assertNotNull(mandatoryFacet);
+                assertTrue(mandatoryFacet instanceof 
MandatoryFacetForPropertyAnnotation.Required);
+            });
+        }
+
+        @Test
+        void whenOptionalityIsDefault() {
+
+            class Customer {
+                @Property(optionality = Optionality.DEFAULT)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processOptional(facetFactory, processMethodContext);
+                // then
+                final MandatoryFacet mandatoryFacet = 
facetedMethod.getFacet(MandatoryFacet.class);
+                assertNull(mandatoryFacet);
+            });
+        }
+
+        @Test
+        void whenNone() {
+
+            class Customer {
+                @Property()
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processOptional(facetFactory, processMethodContext);
+                // then
+                final MandatoryFacet mandatoryFacet = 
facetedMethod.getFacet(MandatoryFacet.class);
+                assertNull(mandatoryFacet);
+            });
+        }
+
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class RegEx extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void whenHasAnnotation() {
+
+            class Customer {
+                @Property(
+                        regexPattern = "[123].*",
+                        regexPatternFlags = Pattern.CASE_INSENSITIVE | 
Pattern.MULTILINE)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processRegEx(facetFactory, processMethodContext);
+                // then
+                final RegExFacet regExFacet = 
facetedMethod.getFacet(RegExFacet.class);
+                assertNotNull(regExFacet);
+                assertTrue(regExFacet instanceof 
RegExFacetForPropertyAnnotation);
+                assertThat(regExFacet.patternFlags(), is(10));
+                assertThat(regExFacet.regexp(), is("[123].*"));
+            });
+        }
+
+        @Test
+        void whenNone() {
+
+            class Customer {
+                @Property()
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processRegEx(facetFactory, processMethodContext);
+                // then
+                final RegExFacet regExFacet = 
facetedMethod.getFacet(RegExFacet.class);
+                assertNull(regExFacet);
+            });
+        }
+
+        @Test
+        void whenEmptyString() {
+
+            class Customer {
+                @Property(regexPattern = "")
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processRegEx(facetFactory, processMethodContext);
+
+                // then
+                final RegExFacet regExFacet = 
facetedMethod.getFacet(RegExFacet.class);
+                assertNull(regExFacet);
+            });
+        }
+
+        @Test
+        void whenNotAnnotatedOnStringProperty() {
+
+            class Customer {
+                @Property(regexPattern = "[abc].*")
+                public int getName() {return 0; }
+                @SuppressWarnings("unused") public void setName(final int 
name) { }
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, 
facetHolder, facetedMethod)->{
+                // when
+                processRegEx(facetFactory, processMethodContext);
+
+                // then
+                final RegExFacet regExFacet = 
facetedMethod.getFacet(RegExFacet.class);
+                assertNull(regExFacet);
+            });
+        }
+
+    }
+
+}
diff --git 
a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
 
b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
index e2baafec544..fce90ce2432 100644
--- 
a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
+++ 
b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
@@ -39,18 +39,12 @@
 import jakarta.inject.Named;
 import jakarta.inject.Provider;
 
-import org.apache.causeway.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
-import org.apache.causeway.core.metamodel.execution.InteractionInternal;
-
-import org.apache.causeway.core.metamodel.services.deadlock.DeadlockRecognizer;
-import org.apache.causeway.schema.chg.v2.ChangesDto;
-import org.apache.causeway.schema.chg.v2.ObjectsDto;
-import org.apache.causeway.schema.common.v2.OidsDto;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import org.springframework.beans.factory.annotation.Qualifier;
 import 
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.core.Ordered;
-import org.jspecify.annotations.Nullable;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.support.TransactionSynchronization;
@@ -61,6 +55,7 @@
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.annotation.Programmatic;
 import org.apache.causeway.applib.annotation.TransactionScope;
+import org.apache.causeway.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.iactn.Interaction;
 import org.apache.causeway.applib.services.iactn.InteractionProvider;
@@ -74,10 +69,12 @@
 import org.apache.causeway.commons.internal.collections._Sets;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.config.CausewayConfiguration;
+import org.apache.causeway.core.metamodel.execution.InteractionInternal;
 import 
org.apache.causeway.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.object.MmEntityUtils;
+import org.apache.causeway.core.metamodel.services.deadlock.DeadlockRecognizer;
 import 
org.apache.causeway.core.metamodel.services.objectlifecycle.HasEnlistedEntityPropertyChanges;
 import 
org.apache.causeway.core.metamodel.services.objectlifecycle.PreAndPostValue;
 import 
org.apache.causeway.core.metamodel.services.objectlifecycle.PropertyChangeRecord;
@@ -88,9 +85,11 @@
 import 
org.apache.causeway.core.transaction.changetracking.EntityPropertyChangePublisher;
 import 
org.apache.causeway.core.transaction.changetracking.HasEnlistedEntityChanges;
 import 
org.apache.causeway.persistence.commons.CausewayModulePersistenceCommons;
+import org.apache.causeway.schema.chg.v2.ChangesDto;
+import org.apache.causeway.schema.chg.v2.ObjectsDto;
+import org.apache.causeway.schema.common.v2.OidsDto;
 
 import lombok.Getter;
-import org.jspecify.annotations.NonNull;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
 
@@ -305,21 +304,18 @@ private boolean shouldPublish(final PreAndPostValue 
preAndPostValue) {
 
     private boolean isEntityExcludedForChangePublishing(final ManagedObject 
entity) {
 
-        if (!configuration.isEnabled()) {
+        if (!configuration.isEnabled())
             return true;
-        }
 
-        if(!EntityChangePublishingFacet.isPublishingEnabled(entity.objSpec())) 
{
+        if(!EntityChangePublishingFacet.isPublishingEnabled(entity.objSpec()))
             return true; // ignore entities that are not enabled for entity 
change publishing
-        }
 
         // guard against transient
         if(ManagedObjects.bookmark(entity).isEmpty()) return true;
 
-        if(changes.isMemoized()) {
+        if(changes.isMemoized())
             throw _Exceptions.illegalState("Cannot enlist additional changes 
for auditing, "
                     + "since changedObjectPropertiesRef was already prepared 
(memoized) for auditing.");
-        }
 
         return false;
     }
@@ -373,9 +369,8 @@ public Optional<EntityChanges> getEntityChanges(
 
         // a defensive copy of
         var changeKindByEnlistedAdapter = new 
HashMap<>(this.changeKindByEnlistedAdapter);
-        if(changeKindByEnlistedAdapter.isEmpty()) {
+        if(changeKindByEnlistedAdapter.isEmpty())
             return Optional.empty();
-        }
 
         final Interaction interaction = currentInteraction();
         final int numberEntitiesLoaded1 = numberEntitiesLoaded();
@@ -418,9 +413,8 @@ private static ChangesDto newDto(
 
         changeKindByEnlistedEntity.forEach((bookmark, kind)->{
             var oidDto = bookmark.toOidDto();
-            if(oidDto==null) {
+            if(oidDto==null)
                 return;
-            }
             switch(kind) {
                 case CREATE:
                     objectsDto.getCreated().getOid().add(oidDto);
@@ -530,9 +524,9 @@ public void enlistCreated(final ManagedObject entity) {
 
         _Xray.enlistCreated(entity, interactionProviderProvider);
 
-        if (isEntityExcludedForChangePublishing(entity)) {
+        if (isEntityExcludedForChangePublishing(entity)
+                || 
!EntityChangePublishingFacet.isPublishingEnabledForCreate(entity.objSpec()))
             return;
-        }
 
         log.debug("enlist entity's property changes for publishing {}", 
entity);
 
@@ -551,9 +545,9 @@ public void enlistUpdating(
 
         _Xray.enlistUpdating(entity, interactionProviderProvider);
 
-        if (isEntityExcludedForChangePublishing(entity)) {
+        if (isEntityExcludedForChangePublishing(entity)
+                || 
!EntityChangePublishingFacet.isPublishingEnabledForUpdate(entity.objSpec()))
             return;
-        }
 
         if(log.isDebugEnabled()) {
             log.debug("enlist entity's property changes for publishing {}", 
entity);
@@ -588,7 +582,9 @@ public void enlistDeleting(final ManagedObject entity) {
 
         _Xray.enlistDeleting(entity, interactionProviderProvider);
 
-        if (isEntityExcludedForChangePublishing(entity)) return;
+        if (isEntityExcludedForChangePublishing(entity)
+                || 
!EntityChangePublishingFacet.isPublishingEnabledForDelete(entity.objSpec()))
+            return;
 
         suppressAutoFlushIfRequired(() -> {
             final boolean enlisted = enlistForChangeKindPublishing(entity, 
EntityChangeKind.DELETE);


Reply via email to