You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2022/08/03 12:15:42 UTC

[isis] 02/02: ISIS-3110: introduces EntityChangeTrackerJpa, mirroring JDO impl, but leveraging the PropertyChangeRecords already provided to us

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

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

commit f427fbea15a081018034cbfbc6a54e4d2e94e783
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Wed Aug 3 13:15:20 2022 +0100

    ISIS-3110: introduces EntityChangeTrackerJpa, mirroring JDO impl, but leveraging the PropertyChangeRecords already provided to us
---
 .../objectlifecycle/ObjectLifecyclePublisher.java  | 118 +++++++----------
 .../publish/ObjectLifecyclePublisherDefault.java   |  27 ++--
 .../changetracking/EntityChangeTracker.java        |  11 +-
 .../integtests/AuditTrail_IntegTestAbstract.java   |   4 +-
 .../jpa/integtests/AuditTrail_IntegTest.java       |   5 -
 .../audittrail/jpa/integtests/model/Counter.java   |   6 +-
 .../changetracking/EntityChangeTrackerJdo.java     |  12 ++
 .../IsisModulePersistenceJpaIntegration.java       |   4 +-
 .../changetracking/EntityChangeTrackerJpa.java}    | 145 ++++++++++++++++-----
 .../PersistenceMetricsServiceJpa.java              |  54 --------
 .../changetracking/_ChangingEntitiesFactory.java   | 143 ++++++++++++++++++++
 .../changetracking/_SimpleChangingEntities.java    | 121 +++++++++++++++++
 .../jpa/integration/changetracking/_Xray.java      | 145 +++++++++++++++++++++
 .../bootstrap/css/bootstrap-overrides-all-v2.css   |   5 +
 14 files changed, 618 insertions(+), 182 deletions(-)

diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
index 2ef38b932f..8d7adce84f 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
@@ -69,100 +69,74 @@ public interface ObjectLifecyclePublisher {
     static HasEnlistedEntityPropertyChanges publishingPayloadForCreation(
             final @NonNull ManagedObject entity) {
 
-        return new HasEnlistedEntityPropertyChanges() {
+        return (timestamp, user, txId) -> entityPropertyChangesForCreation(timestamp, user, txId, entity);
+    }
 
-            @Override
-            public Can<EntityPropertyChange> getPropertyChanges(
-                    final Timestamp timestamp,
-                    final String user,
-                    final TransactionId txId) {
+    private static Can<EntityPropertyChange> entityPropertyChangesForCreation(Timestamp timestamp, String user, TransactionId txId, ManagedObject entity) {
+        return propertyChangeRecordsForCreation(entity).stream()
+                .map(pcr -> pcr.toEntityPropertyChange(timestamp, user, txId))
+                .collect(Can.toCan());
+    }
 
-                return entity
+    static Can<PropertyChangeRecord> propertyChangeRecordsForCreation(ManagedObject entity) {
+        return entity
                 .getSpecification()
                 .streamProperties(MixedIn.EXCLUDED)
-                .filter(property->EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification()))
-                .filter(property->!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
-                .map(property->
+                .filter(property -> EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification()))
+                .filter(property -> !EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
+                .map(property ->
                         PropertyChangeRecord
-                        .of(
-                                entity,
-                                property,
-                                PreAndPostValue
-                                    .pre(PropertyValuePlaceholder.NEW)
-                                    .withPost(ManagedObjects.UnwrapUtil.single(property.get(entity, InteractionInitiatedBy.FRAMEWORK))))
-                        .toEntityPropertyChange(
-                                timestamp,
-                                user,
-                                txId)
-                )
+                                .of(
+                                        entity,
+                                        property,
+                                        PreAndPostValue
+                                                .pre(PropertyValuePlaceholder.NEW)
+                                                .withPost(ManagedObjects.UnwrapUtil.single(property.get(entity, InteractionInitiatedBy.FRAMEWORK)))))
                 .collect(Can.toCan());
-
-            }
-
-        };
-
     }
 
     static HasEnlistedEntityPropertyChanges publishingPayloadForDeletion(
             final @NonNull ManagedObject entity) {
 
-        return new HasEnlistedEntityPropertyChanges() {
+        return (timestamp, user, txId) -> entityPropertyChangesForDeletion(timestamp, user, txId, entity);
 
-            @Override
-            public Can<EntityPropertyChange> getPropertyChanges(
-                    final Timestamp timestamp,
-                    final String user,
-                    final TransactionId txId) {
+    }
 
-                return entity
+    private static Can<EntityPropertyChange> entityPropertyChangesForDeletion(Timestamp timestamp, String user, TransactionId txId, ManagedObject entity) {
+        return propertyChangeRecordsForDeletion(entity).stream()
+                .map(pcr -> pcr.toEntityPropertyChange(timestamp, user, txId))
+                .collect(Can.toCan());
+    }
+
+    static Can<PropertyChangeRecord> propertyChangeRecordsForDeletion(ManagedObject entity) {
+        return entity
                 .getSpecification()
                 .streamProperties(MixedIn.EXCLUDED)
-                .filter(property->EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification()))
-                .filter(property->!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
-                .map(property->
+                .filter(property -> EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification()))
+                .filter(property -> !EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
+                .map(property ->
                         PropertyChangeRecord
-                        .of(
-                                entity,
-                                property,
-                                PreAndPostValue
-                                    .pre(ManagedObjects.UnwrapUtil.single(property.get(entity, InteractionInitiatedBy.FRAMEWORK)))
-                                    .withPost(PropertyValuePlaceholder.DELETED))
-                        .toEntityPropertyChange(
-                                timestamp,
-                                user,
-                                txId)
+                                .of(
+                                        entity,
+                                        property,
+                                        PreAndPostValue
+                                                .pre(ManagedObjects.UnwrapUtil.single(property.get(entity, InteractionInitiatedBy.FRAMEWORK)))
+                                                .withPost(PropertyValuePlaceholder.DELETED))
                 )
                 .collect(Can.toCan());
-
-            }
-
-        };
-
     }
 
-    static HasEnlistedEntityPropertyChanges publishingPayloadForUpdate(
-            final ManagedObject entity,
-            final Can<PropertyChangeRecord> changeRecords) {
-
-        return new HasEnlistedEntityPropertyChanges() {
-
-            @Override
-            public Can<EntityPropertyChange> getPropertyChanges(
-                    final Timestamp timestamp,
-                    final String user,
-                    final TransactionId txId) {
-
-                return changeRecords
-                .map(changeRecord->
-                    changeRecord
-                    .toEntityPropertyChange(
-                            timestamp,
-                            user,
-                            txId));
-            }
+    static HasEnlistedEntityPropertyChanges publishingPayloadForUpdate(final Can<PropertyChangeRecord> changeRecords) {
+        return (timestamp, user, txId) -> entityPropertyChangesForUpdate(timestamp, user, txId, changeRecords);
+    }
 
-        };
+    private static Can<EntityPropertyChange> entityPropertyChangesForUpdate(Timestamp timestamp, String user, TransactionId txId, Can<PropertyChangeRecord> changeRecords) {
+        return propertyChangeRecordsForUpdate(changeRecords)
+                .map(pcr -> pcr.toEntityPropertyChange(timestamp, user, txId));
+    }
 
+    static Can<PropertyChangeRecord> propertyChangeRecordsForUpdate(Can<PropertyChangeRecord> changeRecords) {
+        return changeRecords;
     }
 
 
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
index f1ff4e6a07..cf3aa6f0f6 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
@@ -21,6 +21,7 @@ package org.apache.isis.core.runtimeservices.publish;
 import javax.annotation.Priority;
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.inject.Provider;
 
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
@@ -48,7 +49,7 @@ import org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePu
 import org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecord;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 import org.apache.isis.core.runtimeservices.IsisModuleCoreRuntimeServices;
-import org.apache.isis.core.transaction.changetracking.EntityPropertyChangePublisher;
+import org.apache.isis.core.transaction.changetracking.EntityChangeTracker;
 import org.apache.isis.core.transaction.changetracking.PersistenceCallbackHandlerAbstract;
 
 /**
@@ -65,14 +66,18 @@ extends PersistenceCallbackHandlerAbstract
 implements
     ObjectLifecyclePublisher {
 
-    private final EntityPropertyChangePublisher entityPropertyChangePublisher;
+    private final Provider<EntityChangeTracker> entityChangeTrackerProvider;
 
     @Inject
     public ObjectLifecyclePublisherDefault(
             final EventBusService eventBusService,
-            final EntityPropertyChangePublisher entityPropertyChangePublisher) {
+            final Provider<EntityChangeTracker> entityChangeTrackerProvider) {
         super(eventBusService);
-        this.entityPropertyChangePublisher = entityPropertyChangePublisher;
+        this.entityChangeTrackerProvider = entityChangeTrackerProvider;
+    }
+
+    EntityChangeTracker entityChangeTracker() {
+        return entityChangeTrackerProvider.get();
     }
 
     @Override
@@ -100,9 +105,7 @@ implements
         postLifecycleEventIfRequired(entity, UpdatingLifecycleEventFacet.class);
 
         if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) {
-            entityPropertyChangePublisher.publishChangedProperties(
-                    ObjectLifecyclePublisher
-                    .publishingPayloadForUpdate(entity, changeRecords));
+            entityChangeTracker().enlistUpdating(entity, changeRecords);
         }
 
     }
@@ -113,9 +116,8 @@ implements
         postLifecycleEventIfRequired(entity, RemovingLifecycleEventFacet.class);
 
         if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) {
-            entityPropertyChangePublisher.publishChangedProperties(
-                    ObjectLifecyclePublisher
-                    .publishingPayloadForDeletion(entity));
+            entityChangeTracker().enlistDeleting(entity, ObjectLifecyclePublisher
+                    .propertyChangeRecordsForDeletion(entity));
         }
     }
 
@@ -125,9 +127,8 @@ implements
         postLifecycleEventIfRequired(entity, PersistedLifecycleEventFacet.class);
 
         if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) {
-            entityPropertyChangePublisher.publishChangedProperties(
-                    ObjectLifecyclePublisher
-                    .publishingPayloadForCreation(entity));
+            entityChangeTracker().enlistCreated(entity, ObjectLifecyclePublisher
+                    .propertyChangeRecordsForCreation(entity));
         }
     }
 
diff --git a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTracker.java b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTracker.java
index ffe1bf8da5..45acfa6121 100644
--- a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTracker.java
+++ b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTracker.java
@@ -18,6 +18,8 @@
  */
 package org.apache.isis.core.transaction.changetracking;
 
+import org.apache.isis.commons.collections.Can;
+import org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecord;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 
 /**
@@ -38,6 +40,8 @@ public interface EntityChangeTracker {
      */
     void enlistCreated(ManagedObject entity);
 
+    void enlistCreated(ManagedObject entity, Can<PropertyChangeRecord> propertyChangeRecords);
+
     /**
      * Publishing support: for object stores to enlist an object that is about to be deleted,
      * capturing the pre-deletion value of the properties of the {@link ManagedObject}.
@@ -47,7 +51,9 @@ public interface EntityChangeTracker {
      * The post-modification values are captured  when the transaction commits.  In the case of deleted objects, a
      * dummy value <tt>'[DELETED]'</tt> is used as the post-modification value.
      */
-    void enlistDeleting(ManagedObject entity);
+    void enlistDeleting(ManagedObject entity) ;
+
+    void enlistDeleting(ManagedObject entity, Can<PropertyChangeRecord> propertyChangeRecords);
 
     /**
      * Publishing support: for object stores to enlist an object that is about to be updated,
@@ -59,6 +65,8 @@ public interface EntityChangeTracker {
      */
     void enlistUpdating(ManagedObject entity);
 
+    void enlistUpdating(ManagedObject entity, Can<PropertyChangeRecord> propertyChangeRecords);
+
     /**
      * Fires the appropriate event and lifecycle callback: {@literal LOADED}
      */
@@ -74,5 +82,6 @@ public interface EntityChangeTracker {
      */
     void recognizeUpdating(ManagedObject entity);
 
+
 }
 
diff --git a/extensions/security/audittrail/applib/src/test/java/org/apache/isis/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java b/extensions/security/audittrail/applib/src/test/java/org/apache/isis/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java
index 3673c987a7..e4c4910bec 100644
--- a/extensions/security/audittrail/applib/src/test/java/org/apache/isis/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java
+++ b/extensions/security/audittrail/applib/src/test/java/org/apache/isis/extensions/audittrail/applib/integtests/AuditTrail_IntegTestAbstract.java
@@ -74,7 +74,7 @@ public abstract class AuditTrail_IntegTestAbstract extends IsisIntegrationTestAb
         // then
         var entries = auditTrailEntryRepository.findAll();
         val propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList());
-        assertThat(propertyIds).containsExactlyInAnyOrder("name", "num", "num2");
+        assertThat(propertyIds).contains("name", "num", "num2");
 
         val entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x));
         assertThat(entriesById.get("name"))
@@ -175,7 +175,7 @@ public abstract class AuditTrail_IntegTestAbstract extends IsisIntegrationTestAb
         // then
         var entries = auditTrailEntryRepository.findAll();
         val propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList());
-        assertThat(propertyIds).containsExactlyInAnyOrder("name", "num", "num2");
+        assertThat(propertyIds).contains("name", "num", "num2");
 
         val entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x));
         assertThat(entriesById.get("name"))
diff --git a/extensions/security/audittrail/persistence-jpa/src/test/java/org/apache/isis/extensions/audittrail/jpa/integtests/AuditTrail_IntegTest.java b/extensions/security/audittrail/persistence-jpa/src/test/java/org/apache/isis/extensions/audittrail/jpa/integtests/AuditTrail_IntegTest.java
index 2c768d2f72..837457fcb5 100644
--- a/extensions/security/audittrail/persistence-jpa/src/test/java/org/apache/isis/extensions/audittrail/jpa/integtests/AuditTrail_IntegTest.java
+++ b/extensions/security/audittrail/persistence-jpa/src/test/java/org/apache/isis/extensions/audittrail/jpa/integtests/AuditTrail_IntegTest.java
@@ -73,11 +73,6 @@ public class AuditTrail_IntegTest extends AuditTrail_IntegTestAbstract {
         return Counter.builder().name(name).build();
     }
 
-    @BeforeEach()
-    void checkPersistenceStack() {
-        // currently disabled for JPA, since EntityPropertyChangePublisher still to be implemented.
-        Assumptions.assumeThat(isisBeanTypeRegistry.determineCurrentPersistenceStack().isJpa()).isFalse();
-    }
 
     @Inject IsisBeanTypeRegistry isisBeanTypeRegistry;
 }
diff --git a/extensions/security/audittrail/persistence-jpa/src/test/java/org/apache/isis/extensions/audittrail/jpa/integtests/model/Counter.java b/extensions/security/audittrail/persistence-jpa/src/test/java/org/apache/isis/extensions/audittrail/jpa/integtests/model/Counter.java
index f477634eb2..351fec8052 100644
--- a/extensions/security/audittrail/persistence-jpa/src/test/java/org/apache/isis/extensions/audittrail/jpa/integtests/model/Counter.java
+++ b/extensions/security/audittrail/persistence-jpa/src/test/java/org/apache/isis/extensions/audittrail/jpa/integtests/model/Counter.java
@@ -23,12 +23,15 @@ package org.apache.isis.extensions.audittrail.jpa.integtests.model;
 import javax.inject.Named;
 import javax.persistence.Column;
 import javax.persistence.Entity;
+import javax.persistence.EntityListeners;
 import javax.persistence.GeneratedValue;
 import javax.persistence.Id;
 import javax.persistence.Table;
 
 import org.apache.isis.applib.annotation.DomainObject;
 import org.apache.isis.applib.annotation.Nature;
+import org.apache.isis.applib.annotation.Publishing;
+import org.apache.isis.persistence.jpa.applib.integration.IsisEntityListener;
 
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
@@ -43,7 +46,8 @@ import lombok.Setter;
         name = "Counter"
 )
 @Named("audittrail.test.Counter")
-@DomainObject(nature = Nature.ENTITY)
+@EntityListeners(IsisEntityListener.class)
+@DomainObject(nature = Nature.ENTITY, entityChangePublishing = Publishing.ENABLED)
 @NoArgsConstructor
 @Builder
 @AllArgsConstructor(access = AccessLevel.PRIVATE)
diff --git a/persistence/jdo/integration/src/main/java/org/apache/isis/persistence/jdo/integration/changetracking/EntityChangeTrackerJdo.java b/persistence/jdo/integration/src/main/java/org/apache/isis/persistence/jdo/integration/changetracking/EntityChangeTrackerJdo.java
index cb2427003b..fd39f99375 100644
--- a/persistence/jdo/integration/src/main/java/org/apache/isis/persistence/jdo/integration/changetracking/EntityChangeTrackerJdo.java
+++ b/persistence/jdo/integration/src/main/java/org/apache/isis/persistence/jdo/integration/changetracking/EntityChangeTrackerJdo.java
@@ -366,6 +366,10 @@ implements
         }
     }
 
+    @Override
+    public void enlistCreated(ManagedObject entity, Can<PropertyChangeRecord> propertyChangeRecords) {
+    }
+
     @Override
     public void enlistDeleting(final ManagedObject entity) {
         _Xray.enlistDeleting(entity, interactionProviderProvider);
@@ -374,6 +378,10 @@ implements
         postLifecycleEventIfRequired(entity, RemovingLifecycleEventFacet.class);
     }
 
+    @Override
+    public void enlistDeleting(ManagedObject entity, Can<PropertyChangeRecord> propertyChangeRecords) {
+    }
+
     @Override
     public void enlistUpdating(final ManagedObject entity) {
         _Xray.enlistUpdating(entity, interactionProviderProvider);
@@ -389,6 +397,10 @@ implements
         }
     }
 
+    @Override
+    public void enlistUpdating(ManagedObject entity, Can<PropertyChangeRecord> propertyChangeRecords) {
+    }
+
     @Override
     public void recognizeLoaded(final ManagedObject entity) {
         _Xray.recognizeLoaded(entity, interactionProviderProvider);
diff --git a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/IsisModulePersistenceJpaIntegration.java b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/IsisModulePersistenceJpaIntegration.java
index 682b625059..6989843f23 100644
--- a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/IsisModulePersistenceJpaIntegration.java
+++ b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/IsisModulePersistenceJpaIntegration.java
@@ -23,7 +23,7 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
 import org.apache.isis.core.runtime.IsisModuleCoreRuntime;
-import org.apache.isis.persistence.jpa.integration.changetracking.PersistenceMetricsServiceJpa;
+import org.apache.isis.persistence.jpa.integration.changetracking.EntityChangeTrackerJpa;
 import org.apache.isis.persistence.jpa.integration.entity.JpaEntityIntegration;
 import org.apache.isis.persistence.jpa.integration.services.JpaSupportServiceUsingSpring;
 import org.apache.isis.persistence.jpa.integration.typeconverters.applib.IsisBookmarkConverter;
@@ -52,7 +52,7 @@ import org.apache.isis.persistence.jpa.metamodel.IsisModulePersistenceJpaMetamod
 
         // @Service's
         JpaSupportServiceUsingSpring.class,
-        PersistenceMetricsServiceJpa.class,
+        EntityChangeTrackerJpa.class,
 
 })
 @EntityScan(basePackageClasses = {
diff --git a/persistence/jdo/integration/src/main/java/org/apache/isis/persistence/jdo/integration/changetracking/EntityChangeTrackerJdo.java b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerJpa.java
similarity index 75%
copy from persistence/jdo/integration/src/main/java/org/apache/isis/persistence/jdo/integration/changetracking/EntityChangeTrackerJdo.java
copy to persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerJpa.java
index cb2427003b..2cee271ff1 100644
--- a/persistence/jdo/integration/src/main/java/org/apache/isis/persistence/jdo/integration/changetracking/EntityChangeTrackerJdo.java
+++ b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerJpa.java
@@ -16,8 +16,9 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.persistence.jdo.integration.changetracking;
+package org.apache.isis.persistence.jpa.integration.changetracking;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -33,6 +34,7 @@ import javax.inject.Provider;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.context.event.EventListener;
 import org.springframework.core.annotation.Order;
+import org.springframework.lang.Nullable;
 import org.springframework.stereotype.Service;
 
 import org.apache.isis.applib.annotation.EntityChangeKind;
@@ -48,6 +50,7 @@ import org.apache.isis.applib.services.publishing.spi.EntityPropertyChange;
 import org.apache.isis.applib.services.xactn.TransactionId;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Lazy;
+import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.collections._Maps;
 import org.apache.isis.commons.internal.collections._Sets;
 import org.apache.isis.commons.internal.exceptions._Exceptions;
@@ -71,7 +74,6 @@ import org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRec
 import org.apache.isis.core.metamodel.services.objectlifecycle.PropertyValuePlaceholder;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 import org.apache.isis.core.metamodel.spec.ManagedObjects;
-import org.apache.isis.core.metamodel.spec.ManagedObjects.EntityUtil;
 import org.apache.isis.core.metamodel.spec.feature.MixedIn;
 import org.apache.isis.core.transaction.changetracking.EntityChangeTracker;
 import org.apache.isis.core.transaction.changetracking.EntityChangesPublisher;
@@ -90,12 +92,12 @@ import lombok.extern.log4j.Log4j2;
  * @since 2.0 {@index}
  */
 @Service
-@Named("isis.transaction.EntityChangeTrackerJdo")
+@Named("isis.transaction.EntityChangeTrackerJpa")
 @Priority(PriorityPrecedence.EARLY)
 @Qualifier("jdo")
 @InteractionScope
 @Log4j2
-public class EntityChangeTrackerJdo
+public class EntityChangeTrackerJpa
 extends PersistenceCallbackHandlerAbstract
 implements
     MetricsService,
@@ -103,27 +105,50 @@ implements
     HasEnlistedEntityPropertyChanges,
     HasEnlistedEntityChanges {
 
+    /**
+     * If provided by the ORM.
+     */
+    private final List<PropertyChangeRecord> enlistedPropertyChangesOfCreated = _Lists.newArrayList();
+    /**
+     * If provided by the ORM.
+     */
+    private final List<PropertyChangeRecord> enlistedPropertyChangesOfUpdated = _Lists.newArrayList();
+    /**
+     * If provided by the ORM.
+     */
+    private final List<PropertyChangeRecord> enlistedPropertyChangesOfDeleted = _Lists.newArrayList();
+
     /**
      * Contains initial change records having set the pre-values of every property of every object that was enlisted.
+     *
+     * <p>
+     *     ONLY USED IF THE ENLISTED PROPERTY CHANGES ({@link #enlistedPropertyChangesOfCreated}, {@link #enlistedPropertyChangesOfUpdated}, {@link #enlistedPropertyChangesOfDeleted}) were not provided already.
+     * </p>
      */
     private final Map<String, PropertyChangeRecord> propertyChangeRecordsById = _Maps.newLinkedHashMap();
 
     /**
      * Contains pre- and post- values of every property of every object that actually changed. A lazy snapshot,
      * triggered by internal call to {@link #snapshotPropertyChangeRecords()}.
+     *
+     * <p>
+     *     ONLY USED IF THE ENLISTED PROPERTY CHANGES ({@link #enlistedPropertyChangesOfCreated}, {@link #enlistedPropertyChangesOfUpdated}, {@link #enlistedPropertyChangesOfDeleted}) were not provided already.
+     * </p>
      */
     private final _Lazy<Set<PropertyChangeRecord>> entityPropertyChangeRecordsForPublishing
         = _Lazy.threadSafe(this::capturePostValuesAndDrain);
 
+
     @Getter(AccessLevel.PACKAGE)
     private final Map<Bookmark, EntityChangeKind> changeKindByEnlistedAdapter = _Maps.newLinkedHashMap();
 
+
     private final EntityPropertyChangePublisher entityPropertyChangePublisher;
     private final EntityChangesPublisher entityChangesPublisher;
     private final Provider<InteractionProvider> interactionProviderProvider;
 
     @Inject
-    public EntityChangeTrackerJdo(
+    public EntityChangeTrackerJpa(
             final EntityPropertyChangePublisher entityPropertyChangePublisher,
             final EntityChangesPublisher entityChangesPublisher,
             final EventBusService eventBusService,
@@ -140,30 +165,47 @@ implements
         .orElse(false);
     }
 
-    private void enlistCreatedInternal(final @NonNull ManagedObject adapter) {
+    private void enlistCreatedInternal(final @NonNull ManagedObject adapter, @Nullable Can<PropertyChangeRecord> propertyChangeRecords) {
         if(!isEntityEnabledForChangePublishing(adapter)) {
             return;
         }
         enlistForChangeKindPublishing(adapter, EntityChangeKind.CREATE);
-        enlistForPreAndPostValuePublishing(adapter, record->record.setPreValue(PropertyValuePlaceholder.NEW));
+        if (propertyChangeRecords != null) {
+            // provided by ORM
+            propertyChangeRecords.forEach(this.enlistedPropertyChangesOfCreated::add);
+        } else {
+            // home-grown approach
+            enlistForPreAndPostValuePublishing(adapter, record->record.setPreValue(PropertyValuePlaceholder.NEW));
+        }
     }
 
-    private void enlistUpdatingInternal(
-            final @NonNull ManagedObject entity) {
+    private void enlistUpdatingInternal(final @NonNull ManagedObject entity, Can<PropertyChangeRecord> propertyChangeRecords) {
         if(!isEntityEnabledForChangePublishing(entity)) {
             return;
         }
         enlistForChangeKindPublishing(entity, EntityChangeKind.UPDATE);
-        enlistForPreAndPostValuePublishing(entity, PropertyChangeRecord::updatePreValue);
+        if(propertyChangeRecords != null) {
+            // provided by ORM
+            propertyChangeRecords.forEach(this.enlistedPropertyChangesOfUpdated::add);
+        } else {
+            // home-grown approach
+            enlistForPreAndPostValuePublishing(entity, PropertyChangeRecord::updatePreValue);
+        }
     }
 
-    private void enlistDeletingInternal(final @NonNull ManagedObject adapter) {
+    private void enlistDeletingInternal(final @NonNull ManagedObject adapter, Can<PropertyChangeRecord> propertyChangeRecords) {
         if(!isEntityEnabledForChangePublishing(adapter)) {
             return;
         }
         final boolean enlisted = enlistForChangeKindPublishing(adapter, EntityChangeKind.DELETE);
         if(enlisted) {
-            enlistForPreAndPostValuePublishing(adapter, PropertyChangeRecord::updatePreValue);
+            if (propertyChangeRecords != null) {
+                // provided by ORM
+                propertyChangeRecords.forEach(this.enlistedPropertyChangesOfDeleted::add);
+            } else {
+                // home-grown approach
+                enlistForPreAndPostValuePublishing(adapter, PropertyChangeRecord::updatePreValue);
+            }
         }
     }
 
@@ -173,12 +215,17 @@ implements
         return entityPropertyChangeRecordsForPublishing.get();
     }
 
+    private boolean isOrmSuppliedChangeRecords() {
+        return !(enlistedPropertyChangesOfCreated.isEmpty() && enlistedPropertyChangesOfUpdated.isEmpty() && enlistedPropertyChangesOfDeleted.isEmpty());
+    }
+
     private boolean isEntityEnabledForChangePublishing(final @NonNull ManagedObject adapter) {
 
         if(!EntityChangePublishingFacet.isPublishingEnabled(adapter.getSpecification())) {
             return false; // ignore entities that are not enabled for entity change publishing
         }
 
+        // if home-grown
         if(entityPropertyChangeRecordsForPublishing.isMemoized()) {
             throw _Exceptions.illegalState("Cannot enlist additional changes for auditing, "
                     + "since changedObjectPropertiesRef was already prepared (memoized) for auditing.");
@@ -213,9 +260,14 @@ implements
 
     private void postPublishing() {
         log.debug("purging entity change records");
-        propertyChangeRecordsById.clear();
+        this.enlistedPropertyChangesOfCreated.clear();
+        this.enlistedPropertyChangesOfUpdated.clear();
+        this.enlistedPropertyChangesOfDeleted.clear();
+
+//        propertyChangeRecordsById.clear();
         changeKindByEnlistedAdapter.clear();
-        entityPropertyChangeRecordsForPublishing.clear();
+//        entityPropertyChangeRecordsForPublishing.clear();
+
         entityChangeEventCount.reset();
         numberEntitiesLoaded.reset();
     }
@@ -240,7 +292,6 @@ implements
             final java.sql.Timestamp timestamp,
             final String userName,
             final TransactionId txId) {
-
         return snapshotPropertyChangeRecords().stream()
                 .map(propertyChangeRecord -> propertyChangeRecord.toEntityPropertyChange(timestamp, userName, txId))
                 .collect(Can.toCan());
@@ -316,20 +367,34 @@ implements
      */
     private Set<PropertyChangeRecord> capturePostValuesAndDrain() {
 
-        val records = propertyChangeRecordsById.values().stream()
-                // set post values, which have been left empty up to now
-                .peek(rec->{
-                    // assuming this check correctly detects deleted entities (JDO)
-                    if(EntityUtil.isDetachedOrRemoved(rec.getEntity())) {
-                        rec.updatePostValueAsDeleted();
-                    } else {
-                        rec.updatePostValueAsNonDeleted();
-                    }
-                })
-                .filter(managedProperty->managedProperty.getPreAndPostValue().shouldPublish())
-                .collect(_Sets.toUnmodifiable());
-
-        propertyChangeRecordsById.clear();
+        Set<PropertyChangeRecord> records;
+
+        if (isOrmSuppliedChangeRecords()) {
+            records = _Sets.newLinkedHashSet();
+            // TODO: might need to make this more sophisticated ?
+            records.addAll(enlistedPropertyChangesOfCreated);
+            records.addAll(enlistedPropertyChangesOfUpdated);
+            records.addAll(enlistedPropertyChangesOfDeleted);
+
+            enlistedPropertyChangesOfCreated.clear();
+            enlistedPropertyChangesOfUpdated.clear();
+            enlistedPropertyChangesOfDeleted.clear();
+        } else {
+            records = propertyChangeRecordsById.values().stream()
+                    // set post values, which have been left empty up to now
+                    .peek(rec->{
+                        // assuming this check correctly detects deleted entities (JDO)
+                        if(ManagedObjects.EntityUtil.isDetachedOrRemoved(rec.getEntity())) {
+                            rec.updatePostValueAsDeleted();
+                        } else {
+                            rec.updatePostValueAsNonDeleted();
+                        }
+                    })
+                    .filter(managedProperty->managedProperty.getPreAndPostValue().shouldPublish())
+                    .collect(_Sets.toUnmodifiable());
+
+            propertyChangeRecordsById.clear();
+        }
 
         return records;
 
@@ -356,9 +421,15 @@ implements
 
     @Override
     public void enlistCreated(final ManagedObject entity) {
+        enlistCreated(entity, null);
+    }
+
+
+    @Override
+    public void enlistCreated(ManagedObject entity,  @Nullable final Can<PropertyChangeRecord> propertyChangeRecords) {
         _Xray.enlistCreated(entity, interactionProviderProvider);
         val hasAlreadyBeenEnlisted = isEnlisted(entity);
-        enlistCreatedInternal(entity);
+        enlistCreatedInternal(entity, propertyChangeRecords);
 
         if(!hasAlreadyBeenEnlisted) {
             CallbackFacet.callCallback(entity, PersistedCallbackFacet.class);
@@ -368,19 +439,29 @@ implements
 
     @Override
     public void enlistDeleting(final ManagedObject entity) {
+        enlistDeleting(entity, null);
+    }
+
+    @Override
+    public void enlistDeleting(ManagedObject entity, final Can<PropertyChangeRecord> propertyChangeRecords) {
         _Xray.enlistDeleting(entity, interactionProviderProvider);
-        enlistDeletingInternal(entity);
+        enlistDeletingInternal(entity, propertyChangeRecords);
         CallbackFacet.callCallback(entity, RemovingCallbackFacet.class);
         postLifecycleEventIfRequired(entity, RemovingLifecycleEventFacet.class);
     }
 
     @Override
     public void enlistUpdating(final ManagedObject entity) {
+        enlistUpdating(entity, null);
+    }
+
+    @Override
+    public void enlistUpdating(ManagedObject entity, final Can<PropertyChangeRecord> propertyChangeRecords) {
         _Xray.enlistUpdating(entity, interactionProviderProvider);
         val hasAlreadyBeenEnlisted = isEnlisted(entity);
         // we call this come what may;
         // additional properties may now have been changed, and the changeKind for publishing might also be modified
-        enlistUpdatingInternal(entity);
+        enlistUpdatingInternal(entity, propertyChangeRecords);
 
         if(!hasAlreadyBeenEnlisted) {
             // prevent an infinite loop... don't call the 'updating()' callback on this object if we have already done so
diff --git a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/PersistenceMetricsServiceJpa.java b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/PersistenceMetricsServiceJpa.java
deleted file mode 100644
index 407ce89282..0000000000
--- a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/PersistenceMetricsServiceJpa.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.persistence.jpa.integration.changetracking;
-
-import javax.annotation.Priority;
-import javax.inject.Named;
-
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Service;
-
-import org.apache.isis.applib.annotation.PriorityPrecedence;
-import org.apache.isis.applib.services.metrics.MetricsService;
-
-/**
- * @since 2.0 {@index}
- */
-@Service
-@Named("isis.transaction.PersistenceMetricsServiceJpa")
-@Priority(PriorityPrecedence.EARLY)
-@Qualifier("jpa")
-//@Log4j2
-public class PersistenceMetricsServiceJpa
-implements
-    MetricsService {
-
-    // -- METRICS
-
-    @Override
-    public int numberEntitiesLoaded() {
-        return -1; // n/a
-    }
-
-    @Override
-    public int numberEntitiesDirtied() {
-        return -1; // n/a
-    }
-
-}
diff --git a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_ChangingEntitiesFactory.java b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_ChangingEntitiesFactory.java
new file mode 100644
index 0000000000..c3e142433f
--- /dev/null
+++ b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_ChangingEntitiesFactory.java
@@ -0,0 +1,143 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.persistence.jpa.integration.changetracking;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.apache.isis.applib.annotation.EntityChangeKind;
+import org.apache.isis.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.iactn.Interaction;
+import org.apache.isis.applib.services.publishing.spi.EntityChanges;
+import org.apache.isis.core.metamodel.execution.InteractionInternal;
+import org.apache.isis.schema.chg.v2.ChangesDto;
+import org.apache.isis.schema.chg.v2.ObjectsDto;
+import org.apache.isis.schema.common.v2.OidsDto;
+
+import lombok.val;
+
+final class _ChangingEntitiesFactory {
+
+    public static Optional<EntityChanges> createChangingEntities(
+            final java.sql.Timestamp completedAt,
+            final String userName,
+            final EntityChangeTrackerJpa entityChangeTracker) {
+
+        if(entityChangeTracker.getChangeKindByEnlistedAdapter().isEmpty()) {
+            return Optional.empty();
+        }
+
+        // take a copy of enlisted adapters ... the JDO implementation of the PublishingService
+        // creates further entities which would be enlisted;
+        // taking copy of the map avoids ConcurrentModificationException
+        val changeKindByEnlistedAdapter = new HashMap<>(
+                entityChangeTracker.getChangeKindByEnlistedAdapter());
+
+        val changingEntities = newChangingEntities(
+                completedAt,
+                userName,
+                entityChangeTracker.currentInteraction(),
+                entityChangeTracker.numberEntitiesLoaded(),
+                // side-effect: it locks the result for this transaction,
+                // such that cannot enlist on top of it
+                entityChangeTracker.snapshotPropertyChangeRecords().size(),
+                changeKindByEnlistedAdapter);
+
+        return Optional.of(changingEntities);
+    }
+
+    // -- HELPER
+
+    private static EntityChanges newChangingEntities(
+            final java.sql.Timestamp completedAt,
+            final String userName,
+            final Interaction interaction,
+            final int numberEntitiesLoaded,
+            final int numberEntityPropertiesModified,
+            final Map<Bookmark, EntityChangeKind> changeKindByEnlistedAdapter) {
+
+        val interactionId = interaction.getInteractionId();
+        final int nextEventSequence = ((InteractionInternal) interaction).getThenIncrementTransactionSequence();
+
+        return new _SimpleChangingEntities(
+                    interactionId, nextEventSequence,
+                    userName, completedAt,
+                    numberEntitiesLoaded,
+                    numberEntityPropertiesModified,
+                    ()->newDto(
+                            interactionId, nextEventSequence,
+                            userName, completedAt,
+                            numberEntitiesLoaded,
+                            numberEntityPropertiesModified,
+                            changeKindByEnlistedAdapter));
+    }
+
+    private static ChangesDto newDto(
+            final UUID interactionId, final int transactionSequenceNum,
+            final String userName, final java.sql.Timestamp completedAt,
+            final int numberEntitiesLoaded,
+            final int numberEntityPropertiesModified,
+            final Map<Bookmark, EntityChangeKind> changeKindByEnlistedEntity) {
+
+        val objectsDto = new ObjectsDto();
+        objectsDto.setCreated(new OidsDto());
+        objectsDto.setUpdated(new OidsDto());
+        objectsDto.setDeleted(new OidsDto());
+
+        changeKindByEnlistedEntity.forEach((bookmark, kind)->{
+            val oidDto = bookmark.toOidDto();
+            if(oidDto==null) {
+                return;
+            }
+            switch(kind) {
+            case CREATE:
+                objectsDto.getCreated().getOid().add(oidDto);
+                return;
+            case UPDATE:
+                objectsDto.getUpdated().getOid().add(oidDto);
+                return;
+            case DELETE:
+                objectsDto.getDeleted().getOid().add(oidDto);
+                return;
+            }
+        });
+
+        objectsDto.setLoaded(numberEntitiesLoaded);
+        objectsDto.setPropertiesModified(numberEntityPropertiesModified);
+
+        val changesDto = new ChangesDto();
+
+        changesDto.setMajorVersion("2");
+        changesDto.setMinorVersion("0");
+
+        changesDto.setInteractionId(interactionId.toString());
+        changesDto.setSequence(transactionSequenceNum);
+
+        changesDto.setUsername(userName);
+        changesDto.setCompletedAt(JavaSqlXMLGregorianCalendarMarshalling.toXMLGregorianCalendar(completedAt));
+
+        changesDto.setObjects(objectsDto);
+        return changesDto;
+    }
+
+
+}
diff --git a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_SimpleChangingEntities.java b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_SimpleChangingEntities.java
new file mode 100644
index 0000000000..addff06fb0
--- /dev/null
+++ b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_SimpleChangingEntities.java
@@ -0,0 +1,121 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.persistence.jpa.integration.changetracking;
+
+import java.sql.Timestamp;
+import java.util.UUID;
+import java.util.function.Supplier;
+
+import org.apache.isis.applib.services.publishing.spi.EntityChanges;
+import org.apache.isis.schema.chg.v2.ChangesDto;
+
+import lombok.NonNull;
+import lombok.ToString;
+
+/**
+ * Captures which objects were created, updated or deleted in the course of a transaction.
+ */
+@ToString
+final class _SimpleChangingEntities implements EntityChanges {
+
+    private UUID transactionUuid;
+    private final int sequence;
+    private final String userName;
+    private final Timestamp completedAt;
+    private final int numberEntitiesLoaded;
+    private final int numberEntityPropertiesModified;
+
+    @ToString.Exclude
+    private final Supplier<ChangesDto> changesDtoSupplier;
+
+    public _SimpleChangingEntities(
+            final @NonNull UUID transactionUuid,
+            final int sequence,
+            final @NonNull String userName,
+            final @NonNull Timestamp completedAt,
+            final int numberEntitiesLoaded,
+            final int numberEntityPropertiesModified,
+            final @NonNull Supplier<ChangesDto> changesDtoSupplier) {
+
+        this.transactionUuid = transactionUuid;
+        this.sequence = sequence;
+        this.userName = userName;
+        this.completedAt = completedAt;
+        this.numberEntitiesLoaded = numberEntitiesLoaded;
+        this.numberEntityPropertiesModified = numberEntityPropertiesModified;
+        this.changesDtoSupplier = changesDtoSupplier;
+    }
+
+    @Override
+    public UUID getInteractionId() {
+        return transactionUuid;
+    }
+
+    @Override
+    public int getSequence() {
+        return sequence;
+    }
+
+    /**
+     * The date/time at which this set of enlisted objects was created
+     * (approx the completion time of the transaction).
+     */
+    @Override
+    public Timestamp getCompletedAt() {
+        return completedAt;
+    }
+
+    @Override
+    public String getUsername() {
+        return userName;
+    }
+
+    private ChangesDto dto;
+
+    @Override
+    public ChangesDto getDto() {
+        return dto != null ? dto : (dto = changesDtoSupplier.get());
+    }
+
+    @Override
+    public int getNumberLoaded() {
+        return numberEntitiesLoaded;
+    }
+
+    @Override
+    public int getNumberCreated() {
+        return getDto().getObjects().getCreated().getOid().size();
+    }
+
+    @Override
+    public int getNumberUpdated() {
+        return getDto().getObjects().getUpdated().getOid().size();
+    }
+
+    @Override
+    public int getNumberDeleted() {
+        return getDto().getObjects().getDeleted().getOid().size();
+    }
+
+    @Override
+    public int getNumberPropertiesModified() {
+        return numberEntityPropertiesModified;
+    }
+
+}
diff --git a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java
new file mode 100644
index 0000000000..50955f25fb
--- /dev/null
+++ b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java
@@ -0,0 +1,145 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.persistence.jpa.integration.changetracking;
+
+import java.awt.Color;
+
+import javax.inject.Provider;
+
+import org.apache.isis.applib.services.iactn.InteractionProvider;
+import org.apache.isis.commons.internal.debug._XrayEvent;
+import org.apache.isis.commons.internal.debug.xray.XrayUi;
+import org.apache.isis.core.metamodel.spec.ManagedObject;
+import org.apache.isis.core.metamodel.spec.ManagedObjects;
+import org.apache.isis.core.security.util.XrayUtil;
+
+import lombok.val;
+
+final class _Xray {
+
+    public static void publish(
+            final EntityChangeTrackerJpa entityChangeTrackerDefault,
+            final Provider<InteractionProvider> interactionProviderProvider) {
+
+        if(!XrayUi.isXrayEnabled()) {
+            return;
+        }
+
+        final long propertyChangeRecordCount = entityChangeTrackerDefault.countPotentialPropertyChangeRecords();
+
+        val enteringLabel = String.format("consider %d entity change records for publishing",
+                propertyChangeRecordCount);
+
+        XrayUtil.createSequenceHandle(interactionProviderProvider.get(), "ec-tracker")
+        .ifPresent(handle->{
+
+            handle.submit(sequenceData->{
+
+                sequenceData.alias("ec-tracker", "EntityChange-\nTracker-\n(Default)");
+
+                if(propertyChangeRecordCount==0) {
+                    sequenceData.setConnectionArrowColor(Color.GRAY);
+                    sequenceData.setConnectionLabelColor(Color.GRAY);
+                }
+
+                val callee = handle.getCallees().getFirstOrFail();
+                sequenceData.enter(handle.getCaller(), callee, enteringLabel);
+                //sequenceData.activate(callee);
+            });
+
+        });
+    }
+
+    public static void enlistCreated(
+            final ManagedObject entity,
+            final Provider<InteractionProvider> interactionProviderProvider) {
+        addSequence("enlistCreated", entity, interactionProviderProvider);
+    }
+
+    public static void enlistDeleting(
+            final ManagedObject entity,
+            final Provider<InteractionProvider> interactionProviderProvider) {
+        addSequence("enlistDeleting", entity, interactionProviderProvider);
+    }
+
+    public static void enlistUpdating(
+            final ManagedObject entity,
+            final Provider<InteractionProvider> interactionProviderProvider) {
+        addSequence("enlistUpdating", entity, interactionProviderProvider);
+    }
+
+    public static void recognizeLoaded(
+            final ManagedObject entity,
+            final Provider<InteractionProvider> interactionProviderProvider) {
+        addSequence("recognizeLoaded", entity, interactionProviderProvider);
+    }
+
+    public static void recognizePersisting(
+            final ManagedObject entity,
+            final Provider<InteractionProvider> interactionProviderProvider) {
+        addSequence("recognizePersisting", entity, interactionProviderProvider);
+    }
+
+    public static void recognizeUpdating(
+            final ManagedObject entity,
+            final Provider<InteractionProvider> interactionProviderProvider) {
+        addSequence("recognizeUpdating", entity, interactionProviderProvider);
+    }
+
+    // -- HELPER
+
+    private static void addSequence(
+            final String what,
+            final ManagedObject entity,
+            final Provider<InteractionProvider> interactionProviderProvider) {
+
+        if(!XrayUi.isXrayEnabled()) {
+            return;
+        }
+
+        val enteringLabel = String.format("%s %s",
+                what,
+                ManagedObjects.isNullOrUnspecifiedOrEmpty(entity)
+                    ? "<empty>"
+                    : String.format("%s:\n%s",
+                            entity.getSpecification().getLogicalTypeName(),
+                            "" + entity.getPojo()));
+
+        _XrayEvent.event(enteringLabel);
+
+        XrayUtil.createSequenceHandle(interactionProviderProvider.get(), "ec-tracker")
+        .ifPresent(handle->{
+
+            handle.submit(sequenceData->{
+
+                sequenceData.alias("ec-tracker", "EntityChange-\nTracker-\n(Default)");
+
+                val callee = handle.getCallees().getFirstOrFail();
+                sequenceData.enter(handle.getCaller(), callee, enteringLabel);
+                //sequenceData.activate(callee);
+            });
+
+        });
+
+    }
+
+
+
+
+}
diff --git a/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/pages/common/bootstrap/css/bootstrap-overrides-all-v2.css b/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/pages/common/bootstrap/css/bootstrap-overrides-all-v2.css
index de3fb3caa0..28c87ea119 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/pages/common/bootstrap/css/bootstrap-overrides-all-v2.css
+++ b/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/pages/common/bootstrap/css/bootstrap-overrides-all-v2.css
@@ -1011,6 +1011,11 @@ footer .footer-image {
     /*padding-bottom: 7px;*/
 }
 
+.actionParametersForm .alert {
+    margin-top: 10px;
+}
+
+
 .propertyEditForm .buttons .wicket-ajax-indicator,
 .actionParametersForm .buttons .wicket-ajax-indicator,
 .additionalLinkList .wicket-ajax-indicator {