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 9d51d073c8d5c009d9e90bb1c8c7d47830e3ff8e Author: Dan Haywood <[email protected]> AuthorDate: Wed Aug 3 19:30:47 2022 +0100 ISIS-3110: reworks JPA and JDO auditing (EntityPropertyChange) --- .../metamodel/objectmanager/ObjectManager.java | 15 +- .../objectlifecycle/ObjectLifecyclePublisher.java | 152 +++++----- .../objectlifecycle/PropertyChangeRecord.java | 54 ++-- .../objectlifecycle/PropertyChangeRecordId.java | 70 +++++ .../objectlifecycle/PropertyValuePlaceholder.java | 1 + .../IsisModuleCoreRuntimeServices.java | 2 + .../EntityPropertyChangePublisherDefault.java | 60 ++-- .../publish/LifecycleCallbackNotifier.java | 158 ++++++++++ .../publish/ObjectLifecyclePublisherDefault.java | 89 +++--- .../changetracking/EntityChangeTracker.java | 56 ++-- .../EntityPropertyChangePublisher.java | 5 +- .../changetracking/EntityChangeTrackerDefault.java | 317 ++++++++------------- .../jpa/integration/changetracking/_Xray.java | 11 - .../IsisModulePersistenceJdoDatanucleus.java | 10 +- .../changetracking/JdoLifecycleListener.java | 39 +-- .../metamodel/facets/entity/JdoEntityFacet.java | 11 +- .../jpa/applib/integration/IsisEntityListener.java | 65 +++-- 17 files changed, 607 insertions(+), 508 deletions(-) diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java index 2907699e8d..03204bb5a6 100644 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java @@ -145,7 +145,7 @@ public interface ObjectManager { public default ManagedObject adapt( final @Nullable Object pojo, final @NonNull Supplier<ObjectSpecification> fallbackElementType, - final EntityAdaptingMode bookmarking) { + final EntityAdaptingMode entityAdaptingMode) { if(pojo==null) { return ManagedObject.unspecified(); } @@ -159,11 +159,11 @@ public interface ObjectManager { return ManagedObject.unspecified(); } return spec.isScalar() - ? autoBookmarked(spec, pojo, bookmarking) + ? managedObjectFor(spec, pojo, entityAdaptingMode) : PackedManagedObject.pack( spec.getElementSpecification().orElseGet(fallbackElementType), _NullSafe.streamAutodetect(pojo) - .map(element->adapt(element, bookmarking)) + .map(element->adapt(element, entityAdaptingMode)) .collect(Can.toCan())); } @@ -190,7 +190,7 @@ public interface ObjectManager { || pojo.getClass().equals(proposedSpec.getCorrespondingClass())) // if actual type matches spec's, we assume, that we don't need to reload, // so this is a shortcut for performance reasons - ? autoBookmarked(proposedSpec, pojo, EntityAdaptingMode.MEMOIZE_BOOKMARK) + ? managedObjectFor(proposedSpec, pojo, EntityAdaptingMode.MEMOIZE_BOOKMARK) // fallback, ignoring proposedSpec : adapt(pojo); return adapter; @@ -198,13 +198,12 @@ public interface ObjectManager { // -- HELPER - private static ManagedObject autoBookmarked( + private static ManagedObject managedObjectFor( final ObjectSpecification spec, final Object pojo, - final EntityAdaptingMode bookmarking) { + final EntityAdaptingMode entityAdaptingMode) { - if(bookmarking.isMemoize() - && spec.isEntity()) { + if(entityAdaptingMode.isMemoize() && spec.isEntity()) { val entityFacet = spec.getFacet(EntityFacet.class); val state = entityFacet.getEntityState(pojo); if(state.isAttached()) { 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 8d7adce84f..006f1a0b16 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 @@ -20,6 +20,8 @@ package org.apache.isis.core.metamodel.services.objectlifecycle; import java.sql.Timestamp; +import org.springframework.lang.Nullable; + import org.apache.isis.applib.services.factory.FactoryService; import org.apache.isis.applib.services.publishing.spi.EntityPropertyChange; import org.apache.isis.applib.services.xactn.TransactionId; @@ -35,109 +37,99 @@ import org.apache.isis.core.metamodel.spec.feature.MixedIn; import lombok.NonNull; /** - * Responsible for collecting, then immediately publishing changes to domain objects, - * that is, - * notify publishing subscribers and call the various persistence call-back facets. + * Responsible for collecting and then passing along changes (to the EntityChangeTracker, in persistence commons) so + * that they can be published; and is responsible for calling the various persistence call-back facets. * * @since 2.0 {index} */ public interface ObjectLifecyclePublisher { /** - * Independent of the persistence stack, only triggered by {@link FactoryService} - * and internal {@link ObjectManager}. + * Independent of the persistence stack, called when an object has been created in-memory, for example by + * {@link FactoryService} and internal {@link ObjectManager}. + * + * <p> + * Default implementation fires off callback/lifecycle events. + * </p> + * * @param domainObject - an entity or view-model */ void onPostCreate(ManagedObject domainObject); + /** + * Called by both JPA and JDO, just after an object is retrieved from the database. + * + * <p> + * Default implementation calls <code>EntityChangeTracker#recognizeLoaded(ManagedObject)</code> and + * fires off callback/lifecycle events. + * </p> + * + * @param entity + */ void onPostLoad(ManagedObject entity); + /** + * Called by both JPA and JDO, just before an entity is inserted into the database. + * + * <p> + * Default implementation fires callbacks (including emitting the <code>PreStoreEvent</code>, eg as subscribed) + * by the <code>TimestampService</code>. + * </p> + * + * @param entity + */ void onPrePersist(ManagedObject entity); + /** + * Called by both JPA and JDO, just after an entity has been inserted into the database. + * + * <p> + * Default implementation fires callbacks and enlists the entity within <code>EntityChangeTracker</code> + * for create/persist. + * </p> + * + * @param entity + */ void onPostPersist(ManagedObject entity); - void onPreUpdate(ManagedObject entity, Can<PropertyChangeRecord> changeRecords); + /** + * Called by both JPA and JDO (though JDO does <i>not</i> provide any changeRecords). + * + * <p> + * Default implementation fires callbacks and enlists the entity within <code>EntityChangeTracker</code> + * for update. + * </p> + * + * @param entity + * @param changeRecords - optional parameter to provide the pre-computed {@link PropertyChangeRecord}s from the ORM. JPA does this, JDO does not. + */ + void onPreUpdate(ManagedObject entity, @Nullable Can<PropertyChangeRecord> changeRecords); + /** + * Called by both JPA and JDO, after an existing entity has been updated. + * + * <p> + * Default implementation fires callbacks. + * </p> + * + * @param entity + */ void onPostUpdate(ManagedObject entity); + /** + * Called by both JPA and JDO, just beforean entity is deleted from the database. + * + * <p> + * Default implementation fires callbacks and enlists the entity within <code>EntityChangeTracker</code> + * for delete/remove. + * </p> + * + * @param entity + */ void onPreRemove(ManagedObject entity); //void onPostRemove(ManagedObject entity); - // -- PUBLISHING PAYLOAD FACTORIES - - static HasEnlistedEntityPropertyChanges publishingPayloadForCreation( - final @NonNull ManagedObject entity) { - - return (timestamp, user, txId) -> entityPropertyChangesForCreation(timestamp, user, txId, entity); - } - - 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()); - } - - 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 -> - PropertyChangeRecord - .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 (timestamp, user, txId) -> entityPropertyChangesForDeletion(timestamp, user, txId, 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 -> - PropertyChangeRecord - .of( - entity, - property, - PreAndPostValue - .pre(ManagedObjects.UnwrapUtil.single(property.get(entity, InteractionInitiatedBy.FRAMEWORK))) - .withPost(PropertyValuePlaceholder.DELETED)) - ) - .collect(Can.toCan()); - } - - 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/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecord.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecord.java index 59104cb018..5afa259bb5 100644 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecord.java +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecord.java @@ -26,7 +26,7 @@ import org.apache.isis.applib.services.xactn.TransactionId; import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy; import org.apache.isis.core.metamodel.spec.ManagedObject; import org.apache.isis.core.metamodel.spec.ManagedObjects; -import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation; +import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -34,42 +34,37 @@ import lombok.NonNull; import lombok.ToString; import lombok.val; -@EqualsAndHashCode(of = {"bookmarkStr", "propertyId"}) -@ToString(of = {"bookmarkStr", "propertyId"}) +@EqualsAndHashCode(of = {"id"}) +@ToString(of = {"id"}) public final class PropertyChangeRecord { - @Getter private final ManagedObject entity; - @Getter private final ObjectAssociation property; - @Getter private final Bookmark bookmark; - @Getter private final String propertyId; + @Getter + private final PropertyChangeRecordId id; + + public ManagedObject getEntity() {return id.getEntity();} + public OneToOneAssociation getProperty() {return id.getProperty();} + public Bookmark getBookmark() {return id.getBookmark();} + public String getPropertyId() {return id.getPropertyId();} + @Getter private PreAndPostValue preAndPostValue; - private final String bookmarkStr; - public static PropertyChangeRecord of( - final @NonNull ManagedObject entity, - final @NonNull ObjectAssociation property) { - return new PropertyChangeRecord(entity, property, null); + public static @NonNull PropertyChangeRecord of( + final @NonNull PropertyChangeRecordId id) { + return new PropertyChangeRecord(id, PreAndPostValue.pre(PropertyValuePlaceholder.NEW)); } public static PropertyChangeRecord of( - final @NonNull ManagedObject entity, - final @NonNull ObjectAssociation property, + final @NonNull PropertyChangeRecordId id, final @NonNull PreAndPostValue preAndPostValue) { - return new PropertyChangeRecord(entity, property, preAndPostValue); + return new PropertyChangeRecord(id, preAndPostValue); } private PropertyChangeRecord( - final ManagedObject entity, - final ObjectAssociation property, + final @NonNull PropertyChangeRecordId id, final PreAndPostValue preAndPostValue) { - this.entity = entity; - this.property = property; - this.propertyId = property.getId(); - - this.bookmark = ManagedObjects.bookmarkElseFail(entity); - this.bookmarkStr = bookmark.toString(); + this.id = id; this.preAndPostValue = preAndPostValue; } @@ -79,15 +74,15 @@ public final class PropertyChangeRecord { return target.getLogicalTypeName() + "#" + propertyId; } - public void setPreValue(final Object pre) { - preAndPostValue = PreAndPostValue.pre(pre); + public void updatePreValueAsNew() { + preAndPostValue = PreAndPostValue.pre(PropertyValuePlaceholder.NEW); } - public void updatePreValue() { - setPreValue(getPropertyValue()); + public void updatePreValueWithCurrent() { + preAndPostValue = PreAndPostValue.pre(getPropertyValue()); } - public void updatePostValueAsNonDeleted() { + public void updatePostValueWithCurrent() { preAndPostValue = preAndPostValue.withPost(getPropertyValue()); } @@ -95,6 +90,7 @@ public final class PropertyChangeRecord { preAndPostValue = preAndPostValue.withPost(PropertyValuePlaceholder.DELETED); } + // -- UTILITY public EntityPropertyChange toEntityPropertyChange( @@ -120,7 +116,7 @@ public final class PropertyChangeRecord { // -- HELPER private Object getPropertyValue() { - val referencedAdapter = property.get(entity, InteractionInitiatedBy.FRAMEWORK); + val referencedAdapter = getProperty().get(getEntity(), InteractionInitiatedBy.FRAMEWORK); return ManagedObjects.UnwrapUtil.single(referencedAdapter); } diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecordId.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecordId.java new file mode 100644 index 0000000000..57674953be --- /dev/null +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecordId.java @@ -0,0 +1,70 @@ +/* + * 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.core.metamodel.services.objectlifecycle; + +import java.sql.Timestamp; + +import org.apache.isis.applib.services.bookmark.Bookmark; +import org.apache.isis.applib.services.publishing.spi.EntityPropertyChange; +import org.apache.isis.applib.services.xactn.TransactionId; +import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy; +import org.apache.isis.core.metamodel.spec.ManagedObject; +import org.apache.isis.core.metamodel.spec.ManagedObjects; +import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation; +import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +import lombok.val; + +@EqualsAndHashCode(of = {"bookmarkStr", "propertyId"}) +@ToString(of = {"bookmarkStr", "propertyId"}) +public final class PropertyChangeRecordId { + + @Getter private final String bookmarkStr; + @Getter private final String propertyId; + + @Getter private final ManagedObject entity; + @Getter private final Bookmark bookmark; + @Getter private OneToOneAssociation property; + + public static PropertyChangeRecordId of( + final @NonNull ManagedObject entity, + final @NonNull OneToOneAssociation property) { + return new PropertyChangeRecordId(entity, property); + } + private PropertyChangeRecordId( + final ManagedObject entity, + final OneToOneAssociation property) { + + // these exposed as a convenience + this.entity = entity; + this.property = property; + this.bookmark = ManagedObjects.bookmarkElseFail(entity); + + // these are the key + this.bookmarkStr = bookmark.toString(); + this.propertyId = property.getId(); + + } + +} + diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyValuePlaceholder.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyValuePlaceholder.java index 7aebc666d2..7f03f3ecf9 100644 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyValuePlaceholder.java +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyValuePlaceholder.java @@ -25,6 +25,7 @@ package org.apache.isis.core.metamodel.services.objectlifecycle; */ public enum PropertyValuePlaceholder { + UNKNOWN, NEW, DELETED ; diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java index d974c527a4..2552cec424 100644 --- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java +++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java @@ -52,6 +52,7 @@ import org.apache.isis.core.runtimeservices.publish.CommandPublisherDefault; import org.apache.isis.core.runtimeservices.publish.EntityChangesPublisherDefault; import org.apache.isis.core.runtimeservices.publish.EntityPropertyChangePublisherDefault; import org.apache.isis.core.runtimeservices.publish.ExecutionPublisherDefault; +import org.apache.isis.core.runtimeservices.publish.LifecycleCallbackNotifier; import org.apache.isis.core.runtimeservices.publish.ObjectLifecyclePublisherDefault; import org.apache.isis.core.runtimeservices.recognizer.ExceptionRecognizerServiceDefault; import org.apache.isis.core.runtimeservices.recognizer.dae.ExceptionRecognizerForDataAccessException; @@ -108,6 +109,7 @@ import org.apache.isis.core.runtimeservices.xmlsnapshot.XmlSnapshotServiceDefaul ObjectIconServiceDefault.class, ObjectLifecyclePublisherDefault.class, ObjectMementoServiceDefault.class, + LifecycleCallbackNotifier.class, SchemaValueMarshallerDefault.class, ScratchpadDefault.class, SerializingAdapterDefault.class, diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java index 076ff42315..83e4cda4e2 100644 --- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java +++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java @@ -34,6 +34,7 @@ import org.apache.isis.commons.collections.Can; import org.apache.isis.commons.having.HasEnabling; import org.apache.isis.core.metamodel.services.objectlifecycle.HasEnlistedEntityPropertyChanges; import org.apache.isis.core.runtimeservices.IsisModuleCoreRuntimeServices; +import org.apache.isis.core.security.util.XrayUtil; import org.apache.isis.core.transaction.changetracking.EntityPropertyChangePublisher; import org.springframework.beans.factory.annotation.Qualifier; @@ -44,6 +45,8 @@ import javax.annotation.PostConstruct; import javax.annotation.Priority; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Provider; + import java.util.List; @Service @@ -59,6 +62,7 @@ public class EntityPropertyChangePublisherDefault implements EntityPropertyChang private final ClockService clockService; private final TransactionService transactionService; private final InteractionLayerTracker iaTracker; + private final Provider<HasEnlistedEntityPropertyChanges> hasEnlistedEntityPropertyChangesProvider; private Can<EntityPropertyChangeSubscriber> enabledSubscribers = Can.empty(); @@ -68,48 +72,50 @@ public class EntityPropertyChangePublisherDefault implements EntityPropertyChang .filter(HasEnabling::isEnabled); } - @Override - public void publishChangedProperties( - final HasEnlistedEntityPropertyChanges hasEnlistedEntityPropertyChanges) { - - transactionService.flushTransaction(); - val payload = getPayload(hasEnlistedEntityPropertyChanges); - val xrayHandle = _Xray.enterEntityPropertyChangePublishing( - iaTracker, - payload, - enabledSubscribers, - ()->getCannotPublishReason(payload) - ); - - payload.forEach(propertyChange->{ - for (val subscriber : enabledSubscribers) { - subscriber.onChanging(propertyChange); - } - }); - - _Xray.exitPublishing(xrayHandle); + private HasEnlistedEntityPropertyChanges getHasEnlistedEntityPropertyChanges() { + return hasEnlistedEntityPropertyChangesProvider.get(); } - // -- HELPER + @Override + public void publishChangedProperties() { - private Can<EntityPropertyChange> getPayload( - HasEnlistedEntityPropertyChanges hasEnlistedEntityPropertyChanges) { + transactionService.flushTransaction(); if(enabledSubscribers.isEmpty()) { - return Can.empty(); + return; } val currentTime = clockService.getClock().nowAsJavaSqlTimestamp(); val currentUser = userService.currentUserNameElseNobody(); - val currentTransactionId = transactionService.currentTransactionId() - .orElse(TransactionId.empty()); + val currentTransactionId = transactionService.currentTransactionId().orElse(TransactionId.empty()); - return hasEnlistedEntityPropertyChanges.getPropertyChanges( + val propertyChanges = getHasEnlistedEntityPropertyChanges().getPropertyChanges( currentTime, currentUser, currentTransactionId); + + XrayUtil.SequenceHandle xrayHandle = null; + try { + xrayHandle = _Xray.enterEntityPropertyChangePublishing( + iaTracker, + propertyChanges, + enabledSubscribers, + () -> getCannotPublishReason(propertyChanges) + ); + + propertyChanges.forEach(propertyChange->{ + for (val subscriber : enabledSubscribers) { + subscriber.onChanging(propertyChange); + } + }); + } finally { + _Xray.exitPublishing(xrayHandle); + } } + + // -- HELPER + // x-ray support private @Nullable String getCannotPublishReason(final @NonNull Can<EntityPropertyChange> payload) { return enabledSubscribers.isEmpty() diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/LifecycleCallbackNotifier.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/LifecycleCallbackNotifier.java new file mode 100644 index 0000000000..d6a941c67e --- /dev/null +++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/LifecycleCallbackNotifier.java @@ -0,0 +1,158 @@ +/* + * 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.core.runtimeservices.publish; + +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.inject.Named; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import org.apache.isis.applib.annotation.InteractionScope; +import org.apache.isis.applib.annotation.PriorityPrecedence; +import org.apache.isis.applib.services.bookmark.Bookmark; +import org.apache.isis.applib.services.eventbus.EventBusService; +import org.apache.isis.core.metamodel.facets.object.callbacks.CallbackFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.CreatedCallbackFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.CreatedLifecycleEventFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.LoadedCallbackFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.LoadedLifecycleEventFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.PersistedCallbackFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.PersistedLifecycleEventFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.PersistingCallbackFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.PersistingLifecycleEventFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.RemovingCallbackFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.RemovingLifecycleEventFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedCallbackFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedLifecycleEventFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingCallbackFacet; +import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingLifecycleEventFacet; +import org.apache.isis.core.metamodel.spec.ManagedObject; +import org.apache.isis.core.runtimeservices.IsisModuleCoreRuntimeServices; +import org.apache.isis.core.transaction.changetracking.PersistenceCallbackHandlerAbstract; +import org.apache.isis.core.transaction.changetracking.events.PostStoreEvent; +import org.apache.isis.core.transaction.changetracking.events.PreStoreEvent; + +/** + * Calls lifecycle callbacks for entities, ensuring that any given entity is only ever called once. + * @since 2.0 {@index} + */ +@Component +@Named(IsisModuleCoreRuntimeServices.NAMESPACE + ".LifecycleCallbackNotifier") +@Priority(PriorityPrecedence.EARLY) +@Qualifier("Default") +@InteractionScope +//@Log4j2 +public class LifecycleCallbackNotifier extends PersistenceCallbackHandlerAbstract { + + private final Set<ManagedObject> postCreated = new LinkedHashSet<>(); + private final Set<ManagedObject> postLoaded = new LinkedHashSet<>(); + private final Set<ManagedObject> prePersisted = new LinkedHashSet<>(); + private final Set<ManagedObject> postPersisted = new LinkedHashSet<>(); + private final Set<ManagedObject> preUpdated = new LinkedHashSet<>(); + private final Set<ManagedObject> postUpdated = new LinkedHashSet<>(); + private final Set<ManagedObject> preRemoved = new LinkedHashSet<>(); + + @Inject + public LifecycleCallbackNotifier(EventBusService eventBusService) { + super(eventBusService); + } + + public void postCreate(ManagedObject entity) { + notify(entity, + postCreated, + e -> { + CallbackFacet.callCallback(entity, CreatedCallbackFacet.class); + postLifecycleEventIfRequired(entity, CreatedLifecycleEventFacet.class); + }); + } + + public void postLoad(ManagedObject entity) { + notify(entity, + postLoaded, + e -> { + CallbackFacet.callCallback(entity, LoadedCallbackFacet.class); + postLifecycleEventIfRequired(entity, LoadedLifecycleEventFacet.class); + }); + } + + public void prePersist(ManagedObject entity) { + notify(entity, + prePersisted, + e -> { + eventBusService.post(PreStoreEvent.of(entity.getPojo())); + CallbackFacet.callCallback(entity, PersistingCallbackFacet.class); + postLifecycleEventIfRequired(entity, PersistingLifecycleEventFacet.class); + }); + } + + public void postPersist(ManagedObject entity) { + notify(entity, + postPersisted, + e -> { + eventBusService.post(PostStoreEvent.of(entity.getPojo())); + CallbackFacet.callCallback(entity, PersistedCallbackFacet.class); + postLifecycleEventIfRequired(entity, PersistedLifecycleEventFacet.class); + }); + } + + public void preUpdate(ManagedObject entity) { + notify(entity, + preUpdated, + e -> { + eventBusService.post(PreStoreEvent.of(entity.getPojo())); + CallbackFacet.callCallback(entity, UpdatingCallbackFacet.class); + postLifecycleEventIfRequired(entity, UpdatingLifecycleEventFacet.class); + }); + } + + public void postUpdate(ManagedObject entity) { + notify(entity, + postUpdated, + e -> { + CallbackFacet.callCallback(entity, UpdatedCallbackFacet.class); + postLifecycleEventIfRequired(entity, UpdatedLifecycleEventFacet.class); + }); + } + + public void preRemove(ManagedObject entity) { + notify(entity, + preRemoved, + e -> { + CallbackFacet.callCallback(entity, RemovingCallbackFacet.class); + postLifecycleEventIfRequired(entity, RemovingLifecycleEventFacet.class); + }); + } + + private static void notify(ManagedObject entity, Set<ManagedObject> notified, Consumer<ManagedObject> notify) { + Optional.of(entity) + .filter(x -> !notified.contains(x)) + .ifPresent(x -> { + notify.accept(entity); + notified.add(x); + }); + } + +} 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 cf3aa6f0f6..26feea1bfc 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 @@ -24,33 +24,27 @@ import javax.inject.Named; import javax.inject.Provider; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import org.apache.isis.applib.annotation.PriorityPrecedence; -import org.apache.isis.applib.services.eventbus.EventBusService; import org.apache.isis.commons.collections.Can; import org.apache.isis.core.metamodel.facets.object.callbacks.CallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.CreatedCallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.CreatedLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.LoadedCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.LoadedLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.PersistedCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.PersistedLifecycleEventFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.PersistingCallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.PersistingLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.RemovingCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.RemovingLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedLifecycleEventFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingCallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet; import org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher; 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.EntityChangeTracker; -import org.apache.isis.core.transaction.changetracking.PersistenceCallbackHandlerAbstract; +import org.apache.isis.core.transaction.changetracking.events.PostStoreEvent; /** * @see ObjectLifecyclePublisher @@ -61,87 +55,68 @@ import org.apache.isis.core.transaction.changetracking.PersistenceCallbackHandle @Priority(PriorityPrecedence.EARLY) @Qualifier("Default") //@Log4j2 -public class ObjectLifecyclePublisherDefault -extends PersistenceCallbackHandlerAbstract -implements - ObjectLifecyclePublisher { +public class ObjectLifecyclePublisherDefault implements ObjectLifecyclePublisher { private final Provider<EntityChangeTracker> entityChangeTrackerProvider; + private final Provider<LifecycleCallbackNotifier> lifecycleCallbackNotifierProvider; @Inject public ObjectLifecyclePublisherDefault( - final EventBusService eventBusService, - final Provider<EntityChangeTracker> entityChangeTrackerProvider) { - super(eventBusService); + final Provider<EntityChangeTracker> entityChangeTrackerProvider, + final Provider<LifecycleCallbackNotifier> lifecycleCallbackNotifierProvider) { this.entityChangeTrackerProvider = entityChangeTrackerProvider; + this.lifecycleCallbackNotifierProvider = lifecycleCallbackNotifierProvider; } EntityChangeTracker entityChangeTracker() { return entityChangeTrackerProvider.get(); } - - @Override - public void onPostCreate(final ManagedObject domainObject) { - CallbackFacet.callCallback(domainObject, CreatedCallbackFacet.class); - postLifecycleEventIfRequired(domainObject, CreatedLifecycleEventFacet.class); + LifecycleCallbackNotifier lifecycleCallbackNotifier() { + return lifecycleCallbackNotifierProvider.get(); } @Override - public void onPrePersist(final ManagedObject entity) { - CallbackFacet.callCallback(entity, PersistingCallbackFacet.class); - postLifecycleEventIfRequired(entity, PersistingLifecycleEventFacet.class); + public void onPostCreate(final ManagedObject entity) { + lifecycleCallbackNotifier().postCreate(entity); } @Override - public void onPreUpdate( - final ManagedObject entity, - final Can<PropertyChangeRecord> changeRecords) { - - if(changeRecords.isEmpty()) { - return; - } - - CallbackFacet.callCallback(entity, UpdatingCallbackFacet.class); - postLifecycleEventIfRequired(entity, UpdatingLifecycleEventFacet.class); - - if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) { - entityChangeTracker().enlistUpdating(entity, changeRecords); - } - + public void onPostLoad(final ManagedObject entity) { + entityChangeTracker().incrementLoaded(entity); + lifecycleCallbackNotifier().postLoad(entity); } @Override - public void onPreRemove(final ManagedObject entity) { - CallbackFacet.callCallback(entity, RemovingCallbackFacet.class); - postLifecycleEventIfRequired(entity, RemovingLifecycleEventFacet.class); - - if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) { - entityChangeTracker().enlistDeleting(entity, ObjectLifecyclePublisher - .propertyChangeRecordsForDeletion(entity)); - } + public void onPrePersist(final ManagedObject entity) { + lifecycleCallbackNotifier().prePersist(entity); } @Override public void onPostPersist(final ManagedObject entity) { - CallbackFacet.callCallback(entity, PersistedCallbackFacet.class); - postLifecycleEventIfRequired(entity, PersistedLifecycleEventFacet.class); + entityChangeTracker().enlistCreated(entity); + lifecycleCallbackNotifier().postPersist(entity); + } - if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) { - entityChangeTracker().enlistCreated(entity, ObjectLifecyclePublisher - .propertyChangeRecordsForCreation(entity)); - } + @Override + public void onPreUpdate( + final ManagedObject entity, + @Nullable final Can<PropertyChangeRecord> changeRecords) { + entityChangeTracker().enlistUpdating(entity, changeRecords); + lifecycleCallbackNotifier().preUpdate(entity); } + @Override public void onPostUpdate(final ManagedObject entity) { - CallbackFacet.callCallback(entity, UpdatedCallbackFacet.class); - postLifecycleEventIfRequired(entity, UpdatedLifecycleEventFacet.class); + lifecycleCallbackNotifier().postUpdate(entity); } + @Override - public void onPostLoad(final ManagedObject entity) { - CallbackFacet.callCallback(entity, LoadedCallbackFacet.class); - postLifecycleEventIfRequired(entity, LoadedLifecycleEventFacet.class); + public void onPreRemove(final ManagedObject entity) { + entityChangeTracker().enlistDeleting(entity); + lifecycleCallbackNotifier().preRemove(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 45acfa6121..eaf09281b4 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.springframework.lang.Nullable; + import org.apache.isis.commons.collections.Can; import org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecord; import org.apache.isis.core.metamodel.spec.ManagedObject; @@ -33,54 +35,50 @@ public interface EntityChangeTracker { /** * Publishing support: for object stores to enlist an object that has just been created, * capturing a dummy value <tt>'[NEW]'</tt> for the pre-modification value. - * <p> - * Fires the appropriate event and lifecycle callback: {@literal PERSISTED} + * * <p> * The post-modification values are captured when the transaction commits. + * </p> */ 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}. - * <p> - * Fires the appropriate event and lifecycle callback: {@literal REMOVING} - * <p> - * 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, Can<PropertyChangeRecord> propertyChangeRecords); - /** * Publishing support: for object stores to enlist an object that is about to be updated, * capturing the pre-modification values of the properties of the {@link ManagedObject}. - * <p> - * Fires the appropriate event and lifecycle callback: {@literal UPDATING} + * * <p> * The post-modification values are captured when the transaction commits. + * + * <p> + * Overload as an optimization for ORMs (specifically, JPA) where already have access to the changed records by + * accessing the ORM-specific data structures (<code>EntityManager</code>'s unit-of-work). + * + * </p> + * + * @param entity + * @param propertyChangeRecords - optional parameter (as a performance optimization) to provide the pre-computed {@link PropertyChangeRecord}s from the ORM. JPA does this, JDO does not. */ - void enlistUpdating(ManagedObject entity); + void enlistUpdating(ManagedObject entity, @Nullable Can<PropertyChangeRecord> propertyChangeRecords); - void enlistUpdating(ManagedObject entity, Can<PropertyChangeRecord> propertyChangeRecords); /** - * Fires the appropriate event and lifecycle callback: {@literal LOADED} + * 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}. + * + * <p> + * 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. + * </p> */ - void recognizeLoaded(ManagedObject entity); + void enlistDeleting(ManagedObject entity) ; /** - * Fires the appropriate event and lifecycle callback: {@literal PERSISTING} + * Not strictly part of the concern of entity tracking, but allows the default implementation to also implement + * the {@link org.apache.isis.applib.services.metrics.MetricsService}. */ - void recognizePersisting(ManagedObject entity); + void incrementLoaded(ManagedObject entity); + - /** - * Fires the appropriate event and lifecycle callback: {@literal UPDATING} - */ - void recognizeUpdating(ManagedObject entity); } diff --git a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityPropertyChangePublisher.java b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityPropertyChangePublisher.java index 58ff0cff91..7627d74b55 100644 --- a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityPropertyChangePublisher.java +++ b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityPropertyChangePublisher.java @@ -19,7 +19,6 @@ package org.apache.isis.core.transaction.changetracking; import org.apache.isis.applib.services.publishing.spi.EntityPropertyChange; -import org.apache.isis.core.metamodel.services.objectlifecycle.HasEnlistedEntityPropertyChanges; /** * Notifies {@link org.apache.isis.applib.services.publishing.spi.EntityPropertyChangeSubscriber}s. @@ -35,9 +34,7 @@ public interface EntityPropertyChangePublisher { * a property of an entity has changed using the * {@link org.apache.isis.applib.services.publishing.spi.EntityPropertyChangeSubscriber#onChanging(EntityPropertyChange)} * callback. - * - * @param hasEnlistedEntityPropertyChanges */ - void publishChangedProperties(HasEnlistedEntityPropertyChanges hasEnlistedEntityPropertyChanges); + void publishChangedProperties(); } diff --git a/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerDefault.java b/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerDefault.java index 91cbbfeaa9..5a429a9f96 100644 --- a/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerDefault.java +++ b/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerDefault.java @@ -19,8 +19,8 @@ */ package org.apache.isis.persistence.jpa.integration.changetracking; -import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -51,28 +51,14 @@ 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; -import org.apache.isis.core.metamodel.facets.object.callbacks.CallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.LoadedCallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.LoadedLifecycleEventFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.PersistedCallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.PersistedLifecycleEventFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.PersistingCallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.PersistingLifecycleEventFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.RemovingCallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.RemovingLifecycleEventFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedCallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedLifecycleEventFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingCallbackFacet; -import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet; import org.apache.isis.core.metamodel.facets.properties.property.entitychangepublishing.EntityPropertyChangePublishingPolicyFacet; import org.apache.isis.core.metamodel.services.objectlifecycle.HasEnlistedEntityPropertyChanges; import org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecord; -import org.apache.isis.core.metamodel.services.objectlifecycle.PropertyValuePlaceholder; +import org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecordId; import org.apache.isis.core.metamodel.spec.ManagedObject; import org.apache.isis.core.metamodel.spec.ManagedObjects; import org.apache.isis.core.metamodel.spec.feature.MixedIn; @@ -106,35 +92,15 @@ 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> + * Contains a record for every objectId/propertyId that was changed. */ - private final Map<String, PropertyChangeRecord> propertyChangeRecordsById = _Maps.newLinkedHashMap(); + private final Map<PropertyChangeRecordId, PropertyChangeRecord> enlistedPropertyChangeRecordsById = _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); @@ -148,6 +114,10 @@ implements private final EntityChangesPublisher entityChangesPublisher; private final Provider<InteractionProvider> interactionProviderProvider; + private final LongAdder numberEntitiesLoaded = new LongAdder(); + private final LongAdder entityChangeEventCount = new LongAdder(); + private final AtomicBoolean persistentChangesEncountered = new AtomicBoolean(); + @Inject public EntityChangeTrackerDefault( final EntityPropertyChangePublisher entityPropertyChangePublisher, @@ -160,70 +130,22 @@ implements this.interactionProviderProvider = interactionProviderProvider; } - private boolean isEnlisted(final @NonNull ManagedObject adapter) { + private boolean isEnlistedWrtChangeKind(final @NonNull ManagedObject adapter) { return ManagedObjects.bookmark(adapter) .map(changeKindByEnlistedAdapter::containsKey) .orElse(false); } - private void enlistCreatedInternal(final @NonNull ManagedObject adapter, @Nullable Can<PropertyChangeRecord> propertyChangeRecords) { - if(!isEntityEnabledForChangePublishing(adapter)) { - return; - } - enlistForChangeKindPublishing(adapter, EntityChangeKind.CREATE); - 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, Can<PropertyChangeRecord> propertyChangeRecords) { - if(!isEntityEnabledForChangePublishing(entity)) { - return; - } - enlistForChangeKindPublishing(entity, EntityChangeKind.UPDATE); - 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, Can<PropertyChangeRecord> propertyChangeRecords) { - if(!isEntityEnabledForChangePublishing(adapter)) { - return; - } - final boolean enlisted = enlistForChangeKindPublishing(adapter, EntityChangeKind.DELETE); - if(enlisted) { - if (propertyChangeRecords != null) { - // provided by ORM - propertyChangeRecords.forEach(this.enlistedPropertyChangesOfDeleted::add); - } else { - // home-grown approach - enlistForPreAndPostValuePublishing(adapter, PropertyChangeRecord::updatePreValue); - } - } - } - Set<PropertyChangeRecord> snapshotPropertyChangeRecords() { // this code path has side-effects, it locks the result for this transaction, // such that cannot enlist on top of it return entityPropertyChangeRecordsForPublishing.get(); } - private boolean isOrmSuppliedChangeRecords() { - return !(enlistedPropertyChangesOfCreated.isEmpty() && enlistedPropertyChangesOfUpdated.isEmpty() && enlistedPropertyChangesOfDeleted.isEmpty()); - } - - private boolean isEntityEnabledForChangePublishing(final @NonNull ManagedObject adapter) { + private boolean isEntityExcludedForChangePublishing(ManagedObject entity) { - if(!EntityChangePublishingFacet.isPublishingEnabled(adapter.getSpecification())) { - return false; // ignore entities that are not enabled for entity change publishing + if(!EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) { + return true; // ignore entities that are not enabled for entity change publishing } if(entityPropertyChangeRecordsForPublishing.isMemoized()) { @@ -231,10 +153,7 @@ implements + "since changedObjectPropertiesRef was already prepared (memoized) for auditing."); } - entityChangeEventCount.increment(); - enableCommandPublishing(); - - return true; + return false; } /** @@ -254,20 +173,14 @@ implements _Xray.publish(this, interactionProviderProvider); log.debug("about to publish entity changes"); - entityPropertyChangePublisher.publishChangedProperties(this); + entityPropertyChangePublisher.publishChangedProperties(); entityChangesPublisher.publishChangingEntities(this); } private void postPublishing() { log.debug("purging entity change records"); - // if ORM provided property change records ... as in JPA - this.enlistedPropertyChangesOfCreated.clear(); - this.enlistedPropertyChangesOfUpdated.clear(); - this.enlistedPropertyChangesOfDeleted.clear(); - - // if instead we had to infer ourselves (home-grown)... as in JDO - propertyChangeRecordsById.clear(); + enlistedPropertyChangeRecordsById.clear(); entityPropertyChangeRecordsForPublishing.clear(); changeKindByEnlistedAdapter.clear(); @@ -309,12 +222,15 @@ implements // -- HELPER /** - * @return <code>true</code> if successfully enlisted, <code>false</code> if was already enlisted + * @return <code>true</code> if successfully enlisted, <code>false</code> if not (no longer) enlisted ... eg delete of an entity that was created earlier in the transaction */ private boolean enlistForChangeKindPublishing( final @NonNull ManagedObject entity, final @NonNull EntityChangeKind changeKind) { + entityChangeEventCount.increment(); + enableCommandPublishing(); + val bookmark = ManagedObjects.bookmarkElseFail(entity); val previousChangeKind = changeKindByEnlistedAdapter.get(bookmark); @@ -345,23 +261,7 @@ implements case DELETE: return false; } - return previousChangeKind == null; - } - - private void enlistForPreAndPostValuePublishing( - final ManagedObject entity, - final Consumer<PropertyChangeRecord> onNewChangeRecord) { - - log.debug("enlist entity's property changes for publishing {}", entity); - - entity.getSpecification().streamProperties(MixedIn.EXCLUDED) - .filter(property->!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property)) - .map(property->PropertyChangeRecord.of(entity, property)) - .filter(record->!propertyChangeRecordsById.containsKey(record.getPropertyId())) // already enlisted, so ignore - .forEach(record->{ - onNewChangeRecord.accept(record); - propertyChangeRecordsById.put(record.getPropertyId(), record); - }); + return false; } /** @@ -370,34 +270,20 @@ implements */ private Set<PropertyChangeRecord> capturePostValuesAndDrain() { - 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(); - } + val records = enlistedPropertyChangeRecordsById.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.updatePostValueWithCurrent(); + } + }) + .filter(managedProperty->managedProperty.getPreAndPostValue().shouldPublish()) + .collect(_Sets.toUnmodifiable()); + + enlistedPropertyChangeRecordsById.clear(); return records; @@ -405,98 +291,119 @@ implements // side-effect free, used by XRay long countPotentialPropertyChangeRecords() { - return propertyChangeRecordsById.size(); + return enlistedPropertyChangeRecordsById.size(); } - // -- METRICS SERVICE + // -- ENTITY CHANGE TRACKING @Override - public int numberEntitiesLoaded() { - return Math.toIntExact(numberEntitiesLoaded.longValue()); + public void enlistCreated(final ManagedObject entity) { + + _Xray.enlistCreated(entity, interactionProviderProvider); + + if (isEntityExcludedForChangePublishing(entity)) { + return; + } + + log.debug("enlist entity's property changes for publishing {}", entity); + enlistForChangeKindPublishing(entity, EntityChangeKind.CREATE); + + enlistForCreateOrUpdate(entity, PropertyChangeRecord::updatePreValueAsNew); } @Override - public int numberEntitiesDirtied() { - return changeKindByEnlistedAdapter.size(); - } + public void enlistUpdating( + final ManagedObject entity, + @Nullable final Can<PropertyChangeRecord> ormPropertyChangeRecords) { - // -- ENTITY CHANGE TRACKING + _Xray.enlistUpdating(entity, interactionProviderProvider); - @Override - public void enlistCreated(final ManagedObject entity) { - enlistCreated(entity, null); - } + if (isEntityExcludedForChangePublishing(entity)) { + return; + } + // we call this come what may; + // additional properties may now have been changed, and the changeKind for publishing might also be modified + enlistForChangeKindPublishing(entity, EntityChangeKind.UPDATE); - @Override - public void enlistCreated(ManagedObject entity, @Nullable final Can<PropertyChangeRecord> propertyChangeRecords) { - _Xray.enlistCreated(entity, interactionProviderProvider); - val hasAlreadyBeenEnlisted = isEnlisted(entity); - enlistCreatedInternal(entity, propertyChangeRecords); + if(ormPropertyChangeRecords != null) { + // provided by ORM + ormPropertyChangeRecords + .stream() + .filter(pcr -> !EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(pcr.getProperty())) + .forEach(pcr -> this.enlistedPropertyChangeRecordsById.put(pcr.getId(), pcr)); // if already known, then we don't replace (keep first pre-value we know about) + } else { + // home-grown approach + log.debug("enlist entity's property changes for publishing {}", entity); - if(!hasAlreadyBeenEnlisted) { - CallbackFacet.callCallback(entity, PersistedCallbackFacet.class); - postLifecycleEventIfRequired(entity, PersistedLifecycleEventFacet.class); + enlistForCreateOrUpdate(entity, PropertyChangeRecord::updatePreValueWithCurrent); } } - @Override - public void enlistDeleting(final ManagedObject entity) { - enlistDeleting(entity, null); + private void enlistForCreateOrUpdate(ManagedObject entity, Consumer<PropertyChangeRecord> propertyChangeRecordConsumer) { + entity.getSpecification().streamProperties(MixedIn.EXCLUDED) + .filter(property->!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property)) + .map(property -> PropertyChangeRecordId.of(entity, property)) + .filter(pcrId -> ! enlistedPropertyChangeRecordsById.containsKey(pcrId)) // only if not previously seen + .map(pcrId -> enlistedPropertyChangeRecordsById.put(pcrId, PropertyChangeRecord.of(pcrId))) + .filter(Objects::nonNull) // shouldn't happen, just keeping compiler happy + .forEach(propertyChangeRecordConsumer); } + @Override - public void enlistDeleting(ManagedObject entity, final Can<PropertyChangeRecord> propertyChangeRecords) { + public void enlistDeleting(final ManagedObject entity) { + _Xray.enlistDeleting(entity, interactionProviderProvider); - enlistDeletingInternal(entity, propertyChangeRecords); - CallbackFacet.callCallback(entity, RemovingCallbackFacet.class); - postLifecycleEventIfRequired(entity, RemovingLifecycleEventFacet.class); - } - @Override - public void enlistUpdating(final ManagedObject entity) { - enlistUpdating(entity, null); - } + if (isEntityExcludedForChangePublishing(entity)) { + return; + } - @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, propertyChangeRecords); + final boolean enlisted = enlistForChangeKindPublishing(entity, EntityChangeKind.DELETE); + if(enlisted) { - if(!hasAlreadyBeenEnlisted) { - // prevent an infinite loop... don't call the 'updating()' callback on this object if we have already done so - CallbackFacet.callCallback(entity, UpdatingCallbackFacet.class); - postLifecycleEventIfRequired(entity, UpdatingLifecycleEventFacet.class); + log.debug("enlist entity's property changes for publishing {}", entity); + + entity.getSpecification() + .streamProperties(MixedIn.EXCLUDED) + .filter(property -> EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) + .filter(property -> !EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property)) + .map(property -> PropertyChangeRecordId.of(entity, property)) + .map(pcrId -> enlistedPropertyChangeRecordsById.computeIfAbsent(pcrId, PropertyChangeRecord::of)) + .forEach(pcr -> { + pcr.updatePreValueWithCurrent(); + pcr.updatePostValueAsDeleted(); + }); } } + + + /** + * Used only for the implementation of {@link MetricsService}. + * @param entity + */ @Override - public void recognizeLoaded(final ManagedObject entity) { + public void incrementLoaded(final ManagedObject entity) { _Xray.recognizeLoaded(entity, interactionProviderProvider); - CallbackFacet.callCallback(entity, LoadedCallbackFacet.class); - postLifecycleEventIfRequired(entity, LoadedLifecycleEventFacet.class); numberEntitiesLoaded.increment(); } + + // -- METRICS SERVICE + @Override - public void recognizePersisting(final ManagedObject entity) { - _Xray.recognizePersisting(entity, interactionProviderProvider); - CallbackFacet.callCallback(entity, PersistingCallbackFacet.class); - postLifecycleEventIfRequired(entity, PersistingLifecycleEventFacet.class); + public int numberEntitiesLoaded() { + return Math.toIntExact(numberEntitiesLoaded.longValue()); } @Override - public void recognizeUpdating(final ManagedObject entity) { - _Xray.recognizeUpdating(entity, interactionProviderProvider); - CallbackFacet.callCallback(entity, UpdatedCallbackFacet.class); - postLifecycleEventIfRequired(entity, UpdatedLifecycleEventFacet.class); + public int numberEntitiesDirtied() { + return changeKindByEnlistedAdapter.size(); } - private final LongAdder numberEntitiesLoaded = new LongAdder(); - private final LongAdder entityChangeEventCount = new LongAdder(); - private final AtomicBoolean persistentChangesEncountered = new AtomicBoolean(); + + } diff --git a/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java b/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java index 7ee2ef86da..216a435458 100644 --- a/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java +++ b/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java @@ -91,17 +91,6 @@ final class _Xray { 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 diff --git a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/IsisModulePersistenceJdoDatanucleus.java b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/IsisModulePersistenceJdoDatanucleus.java index f3048871c2..e127e956a1 100644 --- a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/IsisModulePersistenceJdoDatanucleus.java +++ b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/IsisModulePersistenceJdoDatanucleus.java @@ -48,6 +48,7 @@ import org.apache.isis.core.config.IsisConfiguration; import org.apache.isis.core.config.beans.IsisBeanTypeRegistry; import org.apache.isis.core.config.beans.aoppatch.TransactionInterceptorFactory; import org.apache.isis.core.metamodel.context.MetaModelContext; +import org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher; import org.apache.isis.core.transaction.changetracking.EntityChangeTracker; import org.apache.isis.persistence.jdo.datanucleus.changetracking.JdoLifecycleListener; import org.apache.isis.persistence.jdo.datanucleus.config.DatanucleusSettings; @@ -157,6 +158,7 @@ public class IsisModulePersistenceJdoDatanucleus { final DataSource dataSource, final MetaModelContext metaModelContext, final EventBusService eventBusService, + final ObjectLifecyclePublisher objectLifecyclePublisher, final Provider<EntityChangeTracker> entityChangeTrackerProvider, final IsisBeanTypeRegistry beanTypeRegistry, final DatanucleusSettings dnSettings) { @@ -173,14 +175,14 @@ public class IsisModulePersistenceJdoDatanucleus { val pu = createDefaultPersistenceUnit(beanTypeRegistry); val pmf = new JDOPersistenceManagerFactory(pu, props); pmf.setConnectionFactory(dataSource); - integrateWithApplicationLayer(metaModelContext, eventBusService, entityChangeTrackerProvider, pmf); + integrateWithApplicationLayer(metaModelContext, entityChangeTrackerProvider, objectLifecyclePublisher, pmf); return pmf; } @Override protected PersistenceManagerFactory newPersistenceManagerFactory(final String name) { val pmf = super.newPersistenceManagerFactory(name); pmf.setConnectionFactory(dataSource); //might be too late, anyway, not sure if this is ever called - integrateWithApplicationLayer(metaModelContext, eventBusService, entityChangeTrackerProvider, pmf); + integrateWithApplicationLayer(metaModelContext, entityChangeTrackerProvider, objectLifecyclePublisher, pmf); return pmf; } }; @@ -330,14 +332,14 @@ public class IsisModulePersistenceJdoDatanucleus { private static void integrateWithApplicationLayer( final MetaModelContext metaModelContext, - final EventBusService eventBusService, final Provider<EntityChangeTracker> entityChangeTrackerProvider, + final ObjectLifecyclePublisher objectLifecyclePublisher, final PersistenceManagerFactory pmf) { // install JDO specific entity change listeners ... val jdoLifecycleListener = - new JdoLifecycleListener(metaModelContext, eventBusService, entityChangeTrackerProvider); + new JdoLifecycleListener(metaModelContext, entityChangeTrackerProvider, objectLifecyclePublisher); pmf.addInstanceLifecycleListener(jdoLifecycleListener, (Class[]) null); } diff --git a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/changetracking/JdoLifecycleListener.java b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/changetracking/JdoLifecycleListener.java index 647aa388e3..79f182388d 100644 --- a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/changetracking/JdoLifecycleListener.java +++ b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/changetracking/JdoLifecycleListener.java @@ -34,7 +34,9 @@ import org.datanucleus.enhancement.Persistable; import org.apache.isis.applib.services.eventbus.EventBusService; import org.apache.isis.core.metamodel.context.MetaModelContext; +import org.apache.isis.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet; import org.apache.isis.core.metamodel.objectmanager.ObjectManager.EntityAdaptingMode; +import org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher; import org.apache.isis.core.metamodel.spec.ManagedObject; import org.apache.isis.core.transaction.changetracking.EntityChangeTracker; import org.apache.isis.core.transaction.changetracking.events.PostStoreEvent; @@ -65,8 +67,8 @@ implements AttachLifecycleListener, ClearLifecycleListener, CreateLifecycleListe DetachLifecycleListener, DirtyLifecycleListener, LoadLifecycleListener, StoreLifecycleListener { private final @NonNull MetaModelContext metaModelContext; - private final @NonNull EventBusService eventBusService; private final @NonNull Provider<EntityChangeTracker> entityChangeTrackerProvider; + private final @NonNull ObjectLifecyclePublisher objectLifecyclePublisher; // -- CALLBACKS @@ -92,7 +94,9 @@ DetachLifecycleListener, DirtyLifecycleListener, LoadLifecycleListener, StoreLif log.debug("postLoad {}", ()->_Utils.debug(event)); final Persistable pojo = _Utils.persistableFor(event); val entity = adaptEntityAndInjectServices(pojo, EntityAdaptingMode.MEMOIZE_BOOKMARK); - getEntityChangeTracker().recognizeLoaded(entity); + + objectLifecyclePublisher.onPostLoad(entity); + } @Override @@ -101,13 +105,11 @@ DetachLifecycleListener, DirtyLifecycleListener, LoadLifecycleListener, StoreLif final Persistable pojo = _Utils.persistableFor(event); - eventBusService.post(PreStoreEvent.of(pojo)); - /* Called either when an entity is initially persisted, or when an entity is updated; fires the appropriate * lifecycle callback. So filter for those events when initially persisting. */ if(pojo.dnGetStateManager().isNew(pojo)) { val entity = adaptEntity(pojo, EntityAdaptingMode.SKIP_MEMOIZATION); - getEntityChangeTracker().recognizePersisting(entity); + objectLifecyclePublisher.onPrePersist(entity); } } @@ -116,23 +118,22 @@ DetachLifecycleListener, DirtyLifecycleListener, LoadLifecycleListener, StoreLif log.debug("postStore {}", ()->_Utils.debug(event)); final Persistable pojo = _Utils.persistableFor(event); - val entity = adaptEntityAndInjectServices(pojo, EntityAdaptingMode.MEMOIZE_BOOKMARK); - eventBusService.post(PostStoreEvent.of(pojo)); + if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) { - /* Called either when an entity is initially persisted, or when an entity is updated; - * fires the appropriate lifecycle callback.*/ - if(pojo.dnGetStateManager().isNew(pojo)) { + /* Called either when an entity is initially persisted, or when an entity is updated; + * fires the appropriate lifecycle callback.*/ + if(pojo.dnGetStateManager().isNew(pojo)) { - getEntityChangeTracker().enlistCreated(entity); + objectLifecyclePublisher.onPostPersist(entity); - } else { - // the callback and transaction.enlist are done in the preStore callback - // (can't be done here, as the enlist requires to capture the 'before' values) - getEntityChangeTracker().recognizeUpdating(entity); + } else { + // the callback and transaction.enlist are done in the preStore callback + // (can't be done here, as the enlist requires to capture the 'before' values) + objectLifecyclePublisher.onPostUpdate(entity); + } } - } @@ -142,7 +143,8 @@ DetachLifecycleListener, DirtyLifecycleListener, LoadLifecycleListener, StoreLif final Persistable pojo = _Utils.persistableFor(event); val entity = adaptEntity(pojo, EntityAdaptingMode.MEMOIZE_BOOKMARK); - getEntityChangeTracker().enlistUpdating(entity); + + objectLifecyclePublisher.onPreUpdate(entity, null); } @Override @@ -156,7 +158,8 @@ DetachLifecycleListener, DirtyLifecycleListener, LoadLifecycleListener, StoreLif final Persistable pojo = _Utils.persistableFor(event); val entity = adaptEntity(pojo, EntityAdaptingMode.SKIP_MEMOIZATION); - getEntityChangeTracker().enlistDeleting(entity); + + objectLifecyclePublisher.onPreRemove(entity); } @Override diff --git a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java index f2ccfe1251..4c92f3a0b1 100644 --- a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java +++ b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java @@ -38,6 +38,7 @@ import org.apache.isis.applib.query.NamedQuery; import org.apache.isis.applib.query.Query; import org.apache.isis.applib.services.bookmark.Bookmark; import org.apache.isis.applib.services.bookmark.IdStringifier; +import org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher; import org.apache.isis.core.runtime.idstringifier.IdStringifierLookupService; import org.apache.isis.applib.services.exceprecog.Category; import org.apache.isis.applib.services.exceprecog.ExceptionRecognizerService; @@ -412,16 +413,16 @@ implements EntityFacet { private Can<ManagedObject> fetchWithinTransaction(final Supplier<List<?>> fetcher) { - val entityChangeTracker = getFacetHolder().getServiceRegistry().lookupServiceElseFail(EntityChangeTracker.class); + val objectLifecyclePublisher = getFacetHolder().getServiceRegistry().lookupServiceElseFail(ObjectLifecyclePublisher.class); return getTransactionalProcessor().callWithinCurrentTransactionElseCreateNew( ()->_NullSafe.stream(fetcher.get()) - .map(fetchedObject->adopt(entityChangeTracker, fetchedObject)) + .map(fetchedObject->adopt(objectLifecyclePublisher, fetchedObject)) .collect(Can.toCan())) .getValue().orElseThrow(); } - private ManagedObject adopt(final EntityChangeTracker entityChangeTracker, final Object fetchedObject) { + private ManagedObject adopt(final ObjectLifecyclePublisher objectLifecyclePublisher, final Object fetchedObject) { // handles lifecycle callbacks and injects services // ought not to be necessary, however for some queries it seems that the @@ -429,8 +430,8 @@ implements EntityFacet { if(fetchedObject instanceof Persistable) { // an entity val entity = objectManager.adapt(fetchedObject); - //fetchResultHandler.initializeEntityAfterFetched((Persistable) fetchedObject); - entityChangeTracker.recognizeLoaded(entity); + + objectLifecyclePublisher.onPostLoad(entity); return entity; } else { // a value type diff --git a/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/integration/IsisEntityListener.java b/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/integration/IsisEntityListener.java index fd70d1a2ff..f49bc6672c 100644 --- a/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/integration/IsisEntityListener.java +++ b/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/integration/IsisEntityListener.java @@ -31,6 +31,7 @@ import javax.persistence.PreUpdate; import org.eclipse.persistence.sessions.UnitOfWork; import org.eclipse.persistence.sessions.changesets.DirectToFieldChangeRecord; +import org.apache.isis.applib.services.eventbus.EventBusService; import org.apache.isis.applib.services.inject.ServiceInjector; import org.apache.isis.commons.collections.Can; import org.apache.isis.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet; @@ -39,6 +40,8 @@ import org.apache.isis.core.metamodel.objectmanager.ObjectManager; import org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher; import org.apache.isis.core.metamodel.services.objectlifecycle.PreAndPostValue; import org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecord; +import org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecordId; +import org.apache.isis.core.transaction.changetracking.events.PreStoreEvent; import org.apache.isis.persistence.jpa.applib.services.JpaSupportService; import lombok.val; @@ -67,18 +70,30 @@ public class IsisEntityListener { @Inject private ObjectLifecyclePublisher objectLifecyclePublisher; @Inject private Provider<JpaSupportService> jpaSupportServiceProvider; @Inject private ObjectManager objectManager; + @Inject private EventBusService eventBusService; @PrePersist void onPrePersist(final Object entityPojo) { log.debug("onPrePersist: {}", entityPojo); serviceInjector.injectServicesInto(entityPojo); val entity = objectManager.adapt(entityPojo); + objectLifecyclePublisher.onPrePersist(entity); } + @PostLoad void onPostLoad(final Object entityPojo) { + log.debug("onPostLoad: {}", entityPojo); + serviceInjector.injectServicesInto(entityPojo); + val entity = objectManager.adapt(entityPojo); + objectLifecyclePublisher.onPostLoad(entity); + } + + @PreUpdate void onPreUpdate(final Object entityPojo) { log.debug("onPreUpdate: {}", entityPojo); + serviceInjector.injectServicesInto(entityPojo); val entity = objectManager.adapt(entityPojo); + val entityManagerResult = jpaSupportServiceProvider.get().getEntityManager(entityPojo.getClass()); entityManagerResult.getValue().ifPresent(em -> { // https://wiki.eclipse.org/EclipseLink/FAQ/JPA#How_to_access_what_changed_in_an_object_or_transaction.3F val unwrap = em.unwrap(UnitOfWork.class); @@ -89,32 +104,27 @@ public class IsisEntityListener { } final Can<PropertyChangeRecord> propertyChangeRecords = - objectChanges - .getChanges() - .stream() - .filter(property-> EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) - .filter(DirectToFieldChangeRecord.class::isInstance) - .map(DirectToFieldChangeRecord.class::cast) - .map(changeRecord -> { - //XXX lombok val issue with nested lambda - final String propertyName = changeRecord.getAttribute(); - return entity - .getSpecification() - .getProperty(propertyName) - .filter(property->!property.isMixedIn()) - .filter(property->!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property)) - .map(property->PropertyChangeRecord.of( - entity, - property, - PreAndPostValue - .pre(changeRecord.getOldValue()) - .withPost(changeRecord.getNewValue()))) - .orElse(null); // ignore - }) - .collect(Can.toCan()); // a Can<T> only collects non-null elements + objectChanges + .getChanges() + .stream() + .filter(property-> EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) + .filter(DirectToFieldChangeRecord.class::isInstance) + .map(DirectToFieldChangeRecord.class::cast) + .map(ormChangeRecord -> { + //XXX lombok val issue with nested lambda + final String propertyName = ormChangeRecord.getAttribute(); + return entity + .getSpecification() + .getProperty(propertyName) + .filter(property->!property.isMixedIn()) + .filter(property->!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property)) + .map(property->PropertyChangeRecord.of(PropertyChangeRecordId.of(entity, property), + PreAndPostValue.pre(ormChangeRecord.getOldValue()))) + .orElse(null); // ignore + }) + .collect(Can.toCan()); // a Can<T> only collects non-null elements objectLifecyclePublisher.onPreUpdate(entity, propertyChangeRecords); - }); } @@ -141,11 +151,4 @@ public class IsisEntityListener { log.debug("onPostRemove: {}", entityPojo); } - @PostLoad void onPostLoad(final Object entityPojo) { - log.debug("onPostLoad: {}", entityPojo); - serviceInjector.injectServicesInto(entityPojo); - val entity = objectManager.adapt(entityPojo); - objectLifecyclePublisher.onPostLoad(entity); - } - }
