You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2021/04/06 09:12:38 UTC
[isis] 01/01: ISIS-2573: entity change tracking: convert
interaction scoped to tx-scoped
This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch 2573_entity.ch.tr
in repository https://gitbox.apache.org/repos/asf/isis.git
commit d6e8521c95a337f86d4571ea02f2154d4f864e42
Author: ahuber@apache.org <ah...@luna>
AuthorDate: Tue Apr 6 11:12:15 2021 +0200
ISIS-2573: entity change tracking: convert interaction scoped to
tx-scoped
---
.../isis/applib/annotation/InteractionScope.java | 2 +-
.../core/interaction/scope/InteractionScope.java | 28 ++-
.../scope/InteractionScopeLifecycleHandler.java | 4 +-
.../session/InteractionFactoryDefault.java | 12 +-
.../changetracking/EntityChangeTrackerDefault.java | 192 +++++++--------------
.../changetracking/_ChangingEntitiesFactory.java | 4 +-
.../changetracking/_TransactionScopedContext.java | 161 +++++++++++++++++
.../core/transaction/changetracking/_Xray.java | 47 ++++-
.../jdo/JdoEntityPropertyChangePublishingTest.java | 1 +
.../applayer/ApplicationLayerTestFactory.java | 66 +++----
.../EntityPropertyChangeSubscriberForTesting.java | 3 +-
11 files changed, 328 insertions(+), 192 deletions(-)
diff --git a/api/applib/src/main/java/org/apache/isis/applib/annotation/InteractionScope.java b/api/applib/src/main/java/org/apache/isis/applib/annotation/InteractionScope.java
index a8b2632..821ebc3 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/annotation/InteractionScope.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/annotation/InteractionScope.java
@@ -28,7 +28,7 @@ import org.springframework.context.annotation.Scope;
/**
* {@code @InteractionScope} is a specialization of {@link Scope @Scope} for a
- * component whose lifecycle is bound to the current top-level Interaction,
+ * component whose lifecycle is bound to the current Interaction,
* in other words that it is private to the "current user".
*
* <p>Specifically, {@code @InteractionScope} is a <em>composed annotation</em> that
diff --git a/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScope.java b/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScope.java
index d73ec8b..9a4dd01 100644
--- a/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScope.java
+++ b/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScope.java
@@ -39,7 +39,10 @@ import lombok.extern.log4j.Log4j2;
* @since 2.0
*/
@Log4j2
-class InteractionScope implements Scope, InteractionScopeLifecycleHandler {
+class InteractionScope
+implements
+ Scope,
+ InteractionScopeLifecycleHandler {
@Inject private InteractionTracker isisInteractionTracker;
@@ -49,7 +52,7 @@ class InteractionScope implements Scope, InteractionScopeLifecycleHandler {
Object instance;
Runnable destructionCallback;
void preDestroy() {
- log.debug("destroy isis-session scoped {}", name);
+ log.debug("destroy isis-interaction scoped {}", name);
if(destructionCallback!=null) {
destructionCallback.run();
}
@@ -75,15 +78,26 @@ class InteractionScope implements Scope, InteractionScopeLifecycleHandler {
val existingScopedObject = scopedObjects.get().get(name);
if(existingScopedObject!=null) {
+
+ _Probe.errOut("INTERACTION_SCOPE [%s:%s] reuse existing %s",
+ _Probe.currentThreadId(),
+ getConversationId(),
+ Integer.toHexString(existingScopedObject.hashCode()));
+
return existingScopedObject.getInstance();
}
val newScopedObject = ScopedObject.of(name);
scopedObjects.get().put(name, newScopedObject); // just set a stub with a name only
- log.debug("create new isis-session scoped {}", name);
+ log.debug("create new isis-interaction scoped {}", name);
newScopedObject.setInstance(objectFactory.getObject()); // triggers call to registerDestructionCallback
+ _Probe.errOut("INTERACTION_SCOPE [%s:%s] create new %s",
+ _Probe.currentThreadId(),
+ getConversationId(),
+ Integer.toHexString(newScopedObject.hashCode()));
+
return newScopedObject.getInstance();
}
@@ -116,12 +130,16 @@ class InteractionScope implements Scope, InteractionScopeLifecycleHandler {
}
@Override
- public void onTopLevelInteractionOpened() {
+ public void onInteractionOpened() {
// nothing to do
+ _Probe.errOut("INTERACTION_SCOPE opening");
}
@Override
- public void onTopLevelInteractionClosing() {
+ public void onInteractionClosing() {
+
+ _Probe.errOut("INTERACTION_SCOPE closing");
+
try {
scopedObjects.get().values().forEach(ScopedObject::preDestroy);
} finally {
diff --git a/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeLifecycleHandler.java b/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeLifecycleHandler.java
index 82ad852..a09db78 100644
--- a/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeLifecycleHandler.java
+++ b/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeLifecycleHandler.java
@@ -23,7 +23,7 @@ package org.apache.isis.core.interaction.scope;
*/
public interface InteractionScopeLifecycleHandler {
- void onTopLevelInteractionOpened();
- void onTopLevelInteractionClosing();
+ void onInteractionOpened();
+ void onInteractionClosing();
}
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/InteractionFactoryDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/InteractionFactoryDefault.java
index ba6b67f..6fe80cd 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/InteractionFactoryDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/InteractionFactoryDefault.java
@@ -187,7 +187,7 @@ implements
authenticationStack.get().push(authenticationLayer);
if(isInBaseLayer()) {
- postSessionOpened(interactionSession);
+ postInteractionOpened(interactionSession);
}
if(log.isDebugEnabled()) {
@@ -312,18 +312,18 @@ implements
return authenticationStack.get().size()==1;
}
- private void postSessionOpened(IsisInteraction interaction) {
+ private void postInteractionOpened(IsisInteraction interaction) {
interactionId.set(interaction.getInteractionId());
interactionScopeAwareBeans.forEach(bean->bean.beforeEnteringTransactionalBoundary(interaction));
txBoundaryHandler.onOpen(interaction);
val isSynchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
interactionScopeAwareBeans.forEach(bean->bean.afterEnteringTransactionalBoundary(interaction, isSynchronizationActive));
- interactionScopeLifecycleHandler.onTopLevelInteractionOpened();
+ interactionScopeLifecycleHandler.onInteractionOpened();
}
- private void preSessionClosed(IsisInteraction interaction) {
+ private void preInteractionClosed(IsisInteraction interaction) {
completeAndPublishCurrentCommand();
- interactionScopeLifecycleHandler.onTopLevelInteractionClosing(); // cleanup the isis-session scope
+ interactionScopeLifecycleHandler.onInteractionClosing(); // cleanup the isis-session scope
val isSynchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
interactionScopeAwareBeans.forEach(bean->bean.beforeLeavingTransactionalBoundary(interaction, isSynchronizationActive));
txBoundaryHandler.onClose(interaction);
@@ -343,7 +343,7 @@ implements
while(stack.size()>downToStackSize) {
if(isInBaseLayer()) {
// keep the stack unmodified yet, to allow for callbacks to properly operate
- preSessionClosed(stack.peek().getInteraction());
+ preInteractionClosed(stack.peek().getInteraction());
}
_Xray.closeAuthenticationLayer(stack);
stack.pop();
diff --git a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTrackerDefault.java b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTrackerDefault.java
index ace47ef..6b4121a 100644
--- a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTrackerDefault.java
+++ b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTrackerDefault.java
@@ -20,7 +20,6 @@ package org.apache.isis.core.transaction.changetracking;
import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
@@ -29,15 +28,10 @@ import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener;
-import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.apache.isis.applib.annotation.EntityChangeKind;
-import org.apache.isis.applib.annotation.InteractionScope;
-import org.apache.isis.applib.annotation.OrderPrecedence;
import org.apache.isis.applib.events.lifecycle.AbstractLifecycleEvent;
import org.apache.isis.applib.services.bookmark.Bookmark;
import org.apache.isis.applib.services.eventbus.EventBusService;
@@ -48,9 +42,7 @@ import org.apache.isis.applib.services.publishing.spi.EntityChanges;
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._Maps;
-import org.apache.isis.commons.internal.collections._Sets;
+import org.apache.isis.commons.internal.debug._Probe;
import org.apache.isis.commons.internal.exceptions._Exceptions;
import org.apache.isis.commons.internal.factory._InstanceUtil;
import org.apache.isis.core.metamodel.facets.object.callbacks.CallbackFacet;
@@ -75,8 +67,6 @@ import org.apache.isis.core.security.authentication.AuthenticationContext;
import org.apache.isis.core.transaction.changetracking.events.IsisTransactionPlaceholder;
import org.apache.isis.core.transaction.events.TransactionBeforeCompletionEvent;
-import lombok.AccessLevel;
-import lombok.Getter;
import lombok.NonNull;
import lombok.val;
import lombok.extern.log4j.Log4j2;
@@ -86,10 +76,6 @@ import lombok.extern.log4j.Log4j2;
*/
@Service
@Named("isis.transaction.EntityChangeTrackerDefault")
-@Order(OrderPrecedence.EARLY)
-@Primary
-@Qualifier("Default")
-@InteractionScope
@Log4j2
public class EntityChangeTrackerDefault
implements
@@ -104,75 +90,59 @@ implements
@Inject private Provider<InteractionContext> interactionContextProvider;
@Inject private Provider<AuthenticationContext> authenticationContextProvider;
-
- /**
- * Contains initial change records having set the pre-values of every property of every object that was enlisted.
- */
- private final Set<_PropertyChangeRecord> entityPropertyChangeRecords = _Sets.newLinkedHashSet();
-
- /**
- * Contains pre- and post- values of every property of every object that actually changed. A lazy snapshot,
- * triggered by internal call to {@link #snapshotPropertyChangeRecords()}.
- */
- private final _Lazy<Set<_PropertyChangeRecord>> entityPropertyChangeRecordsForPublishing
- = _Lazy.threadSafe(this::capturePostValuesAndDrain);
-
- @Getter(AccessLevel.PACKAGE)
- private final Map<Bookmark, EntityChangeKind> changeKindByEnlistedAdapter = _Maps.newLinkedHashMap();
+ private ThreadLocal<_TransactionScopedContext> transactionScopedContext
+ = ThreadLocal.withInitial(()->
+ new _TransactionScopedContext(interactionContextProvider, authenticationContextProvider));
private boolean isEnlisted(final @NonNull ManagedObject adapter) {
return ManagedObjects.bookmark(adapter)
- .map(changeKindByEnlistedAdapter::containsKey)
+ .map(bookmark->transactionScopedContext.get()
+ .changeKindByEnlistedAdapter()
+ .containsKey(bookmark))
.orElse(false);
}
- private void enlistCreatedInternal(final @NonNull ManagedObject adapter) {
- if(!isEntityEnabledForChangePublishing(adapter)) {
- return;
+ private void enlistCreatedInternal(final @NonNull ManagedObject entity) {
+ onEntityChangeRecognized();
+ if(isEntityEnabledForChangePublishing(entity)) {
+ transactionScopedContext.get()
+ .enlistForChangeKindPublishing(entity, EntityChangeKind.CREATE);
+ enlistForPreAndPostValuePublishing(entity, record->record.setPreValue(IsisTransactionPlaceholder.NEW));
}
- enlistForChangeKindPublishing(adapter, EntityChangeKind.CREATE);
- enlistForPreAndPostValuePublishing(adapter, record->record.setPreValue(IsisTransactionPlaceholder.NEW));
}
- private void enlistUpdatingInternal(final @NonNull ManagedObject adapter) {
- if(!isEntityEnabledForChangePublishing(adapter)) {
- return;
+ private void enlistUpdatingInternal(final @NonNull ManagedObject entity) {
+ onEntityChangeRecognized();
+ if(isEntityEnabledForChangePublishing(entity)) {
+ transactionScopedContext.get()
+ .enlistForChangeKindPublishing(entity, EntityChangeKind.UPDATE);
+ enlistForPreAndPostValuePublishing(entity, _PropertyChangeRecord::updatePreValue);
}
- enlistForChangeKindPublishing(adapter, EntityChangeKind.UPDATE);
- enlistForPreAndPostValuePublishing(adapter, _PropertyChangeRecord::updatePreValue);
}
- private void enlistDeletingInternal(final @NonNull ManagedObject adapter) {
- if(!isEntityEnabledForChangePublishing(adapter)) {
- return;
- }
- final boolean enlisted = enlistForChangeKindPublishing(adapter, EntityChangeKind.DELETE);
- if(enlisted) {
- enlistForPreAndPostValuePublishing(adapter, _PropertyChangeRecord::updatePreValue);
+ private void enlistDeletingInternal(final @NonNull ManagedObject entity) {
+ onEntityChangeRecognized();
+ if(isEntityEnabledForChangePublishing(entity)) {
+ val successfullyEnlisted = transactionScopedContext.get()
+ .enlistForChangeKindPublishing(entity, EntityChangeKind.DELETE);
+ if(successfullyEnlisted) {
+ enlistForPreAndPostValuePublishing(entity, _PropertyChangeRecord::updatePreValue);
+ }
}
}
- private 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 isEntityEnabledForChangePublishing(final @NonNull ManagedObject adapter) {
-
- if(entityPropertyChangeRecordsForPublishing.isMemoized()) {
+ private void onEntityChangeRecognized() {
+ if(transactionScopedContext.get().isAlreadyPreparedForPublishing()) {
throw _Exceptions.illegalState("Cannot enlist additional changes for auditing, "
+ "since changedObjectPropertiesRef was already prepared (memoized) for auditing.");
}
-
entityChangeEventCount.increment();
enableCommandPublishing();
-
- if(!EntityChangePublishingFacet.isPublishingEnabled(adapter.getSpecification())) {
- return false; // ignore entities that are not enabled for entity change publishing
- }
-
- return true;
+ }
+
+ boolean isEntityEnabledForChangePublishing(final @NonNull ManagedObject entity) {
+ // ignore entities that are not enabled for entity change publishing
+ return EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification());
}
/**
@@ -197,14 +167,20 @@ implements
}
private void postPublishing() {
+ debug("PURGE");
log.debug("purging entity change records");
- entityPropertyChangeRecords.clear();
- changeKindByEnlistedAdapter.clear();
- entityPropertyChangeRecordsForPublishing.clear();
+ transactionScopedContext.remove();
entityChangeEventCount.reset();
numberEntitiesLoaded.reset();
}
+ private void debug(String label) {
+ _Probe.errOut("!!![%s] %s %d",
+ Integer.toHexString(EntityChangeTrackerDefault.this.hashCode()),
+ label,
+ transactionScopedContext.get().entityPropertyChangeRecords().size());
+ }
+
private void enableCommandPublishing() {
val alreadySet = persitentChangesEncountered.getAndSet(true);
if(!alreadySet) {
@@ -226,79 +202,26 @@ implements
// -- HELPER
- /**
- * @return <code>true</code> if successfully enlisted, <code>false</code> if was already enlisted
- */
- private boolean enlistForChangeKindPublishing(
- final @NonNull ManagedObject entity,
- final @NonNull EntityChangeKind changeKind) {
-
- val bookmark = ManagedObjects.bookmarkElseFail(entity);
-
- val previousChangeKind = changeKindByEnlistedAdapter.get(bookmark);
- if(previousChangeKind == null) {
- changeKindByEnlistedAdapter.put(bookmark, changeKind);
- return true;
- }
- switch (previousChangeKind) {
- case CREATE:
- switch (changeKind) {
- case DELETE:
- changeKindByEnlistedAdapter.remove(bookmark);
- case CREATE:
- case UPDATE:
- return false;
- }
- break;
- case UPDATE:
- switch (changeKind) {
- case DELETE:
- changeKindByEnlistedAdapter.put(bookmark, changeKind);
- return true;
- case CREATE:
- case UPDATE:
- return false;
- }
- break;
- case DELETE:
- return false;
- }
- return previousChangeKind == null;
- }
-
private void enlistForPreAndPostValuePublishing(
final ManagedObject entity,
- final Consumer<_PropertyChangeRecord> fun) {
+ final Consumer<_PropertyChangeRecord> onNewRecord) {
log.debug("enlist entity's property changes for publishing {}", entity);
+ val entityPropertyChangeRecords = transactionScopedContext.get()
+ .entityPropertyChangeRecords();
entity.getSpecification().streamProperties(MixedIn.EXCLUDED)
.filter(property->!property.isNotPersisted())
.map(property->_PropertyChangeRecord.of(entity, property))
.filter(record->!entityPropertyChangeRecords.contains(record)) // already enlisted, so ignore
.forEach(record->{
- fun.accept(record);
+ onNewRecord.accept(record);
+ debug("ADD");
entityPropertyChangeRecords.add(record);
+ debug("ADDED");
});
}
- /**
- * For any enlisted Object Properties collects those, that are meant for publishing,
- * then clears enlisted objects.
- */
- private Set<_PropertyChangeRecord> capturePostValuesAndDrain() {
-
- val records = entityPropertyChangeRecords.stream()
- .peek(managedProperty->managedProperty.updatePostValue()) // set post values, which have been left empty up to now
- .filter(managedProperty->managedProperty.getPreAndPostValue().shouldPublish())
- .collect(_Sets.toUnmodifiable());
-
- entityPropertyChangeRecords.clear();
-
- return records;
-
- }
-
// -- METRICS SERVICE
@Override
@@ -308,11 +231,11 @@ implements
@Override
public int numberEntitiesDirtied() {
- return changeKindByEnlistedAdapter.size();
+ return transactionScopedContext.get().numberEntitiesDirtied();
}
-
+
int propertyChangeRecordCount() {
- return snapshotPropertyChangeRecords().size();
+ return transactionScopedContext.get().propertyChangeRecordCount();
}
// -- ENTITY CHANGE TRACKING
@@ -339,6 +262,9 @@ implements
@Override
public void enlistUpdating(ManagedObject entity) {
+
+ _Exceptions.dumpStackTrace();
+
_Xray.enlistUpdating(entity, interactionContextProvider, authenticationContextProvider);
val hasAlreadyBeenEnlisted = isEnlisted(entity);
// we call this come what may;
@@ -409,10 +335,16 @@ implements
final String userName,
final TransactionId txId) {
- return snapshotPropertyChangeRecords().stream()
+ return transactionScopedContext.get()
+ .snapshotPropertyChangeRecords()
+ .stream()
.map(propertyChangeRecord->_EntityPropertyChangeFactory
.createEntityPropertyChange(timestamp, userName, txId, propertyChangeRecord))
.collect(Can.toCan());
}
+ Map<Bookmark, EntityChangeKind> changeKindByEnlistedAdapter() {
+ return transactionScopedContext.get().changeKindByEnlistedAdapter();
+ }
+
}
diff --git a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_ChangingEntitiesFactory.java b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_ChangingEntitiesFactory.java
index 82737f1..93fa754 100644
--- a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_ChangingEntitiesFactory.java
+++ b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_ChangingEntitiesFactory.java
@@ -42,7 +42,7 @@ final class _ChangingEntitiesFactory {
final String userName,
final EntityChangeTrackerDefault entityChangeTracker) {
- if(entityChangeTracker.getChangeKindByEnlistedAdapter().isEmpty()) {
+ if(entityChangeTracker.changeKindByEnlistedAdapter().isEmpty()) {
return Optional.empty();
}
@@ -50,7 +50,7 @@ final class _ChangingEntitiesFactory {
// creates further entities which would be enlisted;
// taking copy of the map avoids ConcurrentModificationException
val changeKindByEnlistedAdapter = new HashMap<>(
- entityChangeTracker.getChangeKindByEnlistedAdapter());
+ entityChangeTracker.changeKindByEnlistedAdapter());
val changingEntities = newChangingEntities(
completedAt,
diff --git a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_TransactionScopedContext.java b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_TransactionScopedContext.java
new file mode 100644
index 0000000..370c230
--- /dev/null
+++ b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_TransactionScopedContext.java
@@ -0,0 +1,161 @@
+/*
+ * 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.transaction.changetracking;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Provider;
+
+import org.apache.isis.applib.annotation.EntityChangeKind;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.iactn.InteractionContext;
+import org.apache.isis.commons.collections.Can;
+import org.apache.isis.commons.internal.base._Lazy;
+import org.apache.isis.commons.internal.collections._Maps;
+import org.apache.isis.commons.internal.collections._Sets;
+import org.apache.isis.commons.internal.debug._Probe;
+import org.apache.isis.core.metamodel.spec.ManagedObject;
+import org.apache.isis.core.metamodel.spec.ManagedObjects;
+import org.apache.isis.core.security.authentication.AuthenticationContext;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+import lombok.experimental.Accessors;
+
+@RequiredArgsConstructor
+final class _TransactionScopedContext {
+
+ private final Provider<InteractionContext> interactionContextProvider;
+ private final Provider<AuthenticationContext> authenticationContextProvider;
+
+ /**
+ * Contains initial change records having set the pre-values of every property of every object that was enlisted.
+ */
+ @Getter(AccessLevel.PACKAGE)
+ @Accessors(fluent = true)
+ private final Set<_PropertyChangeRecord> entityPropertyChangeRecords = _Sets.newLinkedHashSet();
+
+ /**
+ * Contains pre- and post- values of every property of every object that actually changed. A lazy snapshot,
+ * triggered by internal call to {@link #snapshotPropertyChangeRecords()}.
+ */
+ private final _Lazy<Can<_PropertyChangeRecord>> entityPropertyChangeRecordsForPublishing
+ = _Lazy.threadSafe(this::capturePostValuesAndDrain);
+
+ @Getter(AccessLevel.PACKAGE)
+ @Accessors(fluent = true)
+ private final Map<Bookmark, EntityChangeKind> changeKindByEnlistedAdapter = _Maps.newLinkedHashMap();
+
+ /**
+ * For any enlisted Object Properties collects those, that are meant for publishing,
+ * then clears enlisted objects.
+ * @param interactionContextProvider
+ * @param authenticationContextProvider
+ */
+ Can<_PropertyChangeRecord> capturePostValuesAndDrain() {
+
+ _Xray.capturePostValuesAndDrain(
+ entityPropertyChangeRecords,
+ interactionContextProvider,
+ authenticationContextProvider);
+
+ debug("BEFORE CLEAR");
+
+ val records = entityPropertyChangeRecords.stream()
+ .peek(managedProperty->managedProperty.updatePostValue()) // set post values, which have been left empty up to now
+ .filter(managedProperty->managedProperty.getPreAndPostValue().shouldPublish())
+ .collect(Can.toCan());
+
+ debug("CLEAR");
+
+ entityPropertyChangeRecords.clear();
+
+ return records;
+
+ }
+
+ Can<_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();
+ }
+
+ boolean isAlreadyPreparedForPublishing() {
+ return entityPropertyChangeRecordsForPublishing.isMemoized();
+ }
+
+ int propertyChangeRecordCount() {
+ return snapshotPropertyChangeRecords().size();
+ }
+
+ int numberEntitiesDirtied() {
+ return changeKindByEnlistedAdapter.size();
+ }
+
+ /**
+ * @return <code>true</code> if successfully enlisted, <code>false</code> if was already enlisted
+ */
+ boolean enlistForChangeKindPublishing(
+ final @NonNull ManagedObject entity,
+ final @NonNull EntityChangeKind changeKind) {
+
+ val bookmark = ManagedObjects.bookmarkElseFail(entity);
+
+ val previousChangeKind = changeKindByEnlistedAdapter.get(bookmark);
+ if(previousChangeKind == null) {
+ changeKindByEnlistedAdapter.put(bookmark, changeKind);
+ return true;
+ }
+ switch (previousChangeKind) {
+ case CREATE:
+ switch (changeKind) {
+ case DELETE:
+ changeKindByEnlistedAdapter.remove(bookmark);
+ case CREATE:
+ case UPDATE:
+ return false;
+ }
+ break;
+ case UPDATE:
+ switch (changeKind) {
+ case DELETE:
+ changeKindByEnlistedAdapter.put(bookmark, changeKind);
+ return true;
+ case CREATE:
+ case UPDATE:
+ return false;
+ }
+ break;
+ case DELETE:
+ return false;
+ }
+ return previousChangeKind == null;
+ }
+
+ private void debug(String label) {
+ _Probe.errOut("!!! %s %d",
+ label,
+ entityPropertyChangeRecords().size());
+ }
+
+}
diff --git a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_Xray.java b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_Xray.java
index 596d74d..f7ff624 100644
--- a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_Xray.java
+++ b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/_Xray.java
@@ -19,11 +19,14 @@
package org.apache.isis.core.transaction.changetracking;
import java.awt.Color;
+import java.util.Objects;
+import java.util.Set;
import javax.inject.Provider;
import org.apache.isis.applib.services.iactn.InteractionContext;
import org.apache.isis.commons.internal.debug.xray.XrayUi;
+import org.apache.isis.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet;
import org.apache.isis.core.metamodel.spec.ManagedObject;
import org.apache.isis.core.metamodel.spec.ManagedObjects;
import org.apache.isis.core.security.authentication.AuthenticationContext;
@@ -33,6 +36,35 @@ import lombok.val;
final class _Xray {
+ public static void capturePostValuesAndDrain(
+ final Set<_PropertyChangeRecord> entityPropertyChangeRecords,
+ final Provider<InteractionContext> iaContextProvider,
+ final Provider<AuthenticationContext> authContextProvider) {
+
+ if(!XrayUi.isXrayEnabled()) {
+ return;
+ }
+
+ // records enlisted before post values have been set
+ final int propertyChangeRecordCount = entityPropertyChangeRecords.size();
+ val enteringLabel = String.format("capture %d post-values", propertyChangeRecordCount);
+
+ XrayUtil.createSequenceHandle(iaContextProvider.get(), authContextProvider.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);
+ });
+
+ });
+ }
+
+
public static void publish(
final EntityChangeTrackerDefault entityChangeTrackerDefault,
final Provider<InteractionContext> iaContextProvider,
@@ -42,6 +74,7 @@ final class _Xray {
return;
}
+ // records enlisted after post values have been set
final int propertyChangeRecordCount = entityChangeTrackerDefault.propertyChangeRecordCount();
val enteringLabel = String.format("do publish %d entity change records",
@@ -121,13 +154,17 @@ final class _Xray {
return;
}
+ //XXX using the same logic as in EntityChangeTrackerDefault; keep that in sync!
+ val isPublishingEnabled = EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification());
+
val enteringLabel = String.format("%s %s",
what,
ManagedObjects.isNullOrUnspecifiedOrEmpty(entity)
? "<empty>"
- : String.format("%s:\n%s",
+ : String.format("%s@%s ... publishing %s",
entity.getSpecification().getLogicalTypeName(),
- "" + entity.getPojo()));
+ Integer.toHexString(Objects.hash(entity.getPojo())),
+ isPublishingEnabled ? "enabled" : "disabled"));
XrayUtil.createSequenceHandle(iaContextProvider.get(), authContextProvider.get(), "ec-tracker")
.ifPresent(handle->{
@@ -136,6 +173,11 @@ final class _Xray {
sequenceData.alias("ec-tracker", "EntityChange-\nTracker-\n(Default)");
+ if(!isPublishingEnabled) {
+ sequenceData.setConnectionArrowColor(Color.GRAY);
+ sequenceData.setConnectionLabelColor(Color.GRAY);
+ }
+
val callee = handle.getCallees().getFirstOrFail();
sequenceData.enter(handle.getCaller(), callee, enteringLabel);
//sequenceData.activate(callee);
@@ -146,6 +188,7 @@ final class _Xray {
}
+
}
diff --git a/regressiontests/incubating/src/test/java/org/apache/isis/testdomain/applayer/publishing/jdo/JdoEntityPropertyChangePublishingTest.java b/regressiontests/incubating/src/test/java/org/apache/isis/testdomain/applayer/publishing/jdo/JdoEntityPropertyChangePublishingTest.java
index 18624bd..c424406 100644
--- a/regressiontests/incubating/src/test/java/org/apache/isis/testdomain/applayer/publishing/jdo/JdoEntityPropertyChangePublishingTest.java
+++ b/regressiontests/incubating/src/test/java/org/apache/isis/testdomain/applayer/publishing/jdo/JdoEntityPropertyChangePublishingTest.java
@@ -55,6 +55,7 @@ import lombok.val;
"logging.level.org.apache.isis.testdomain.util.rest.KVStoreForTesting=DEBUG",
"logging.level.org.apache.isis.persistence.jdo.integration.changetracking.JdoLifecycleListener=DEBUG",
"logging.level.org.apache.isis.core.transaction.changetracking.EntityChangeTrackerDefault=DEBUG",
+ "logging.level.org.apache.isis.testdomain.applayer.publishing.EntityPropertyChangeSubscriberForTesting=DEBUG",
})
@TestPropertySource({
IsisPresets.UseLog4j2Test
diff --git a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/applayer/ApplicationLayerTestFactory.java b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/applayer/ApplicationLayerTestFactory.java
index e4728a5..993e481 100644
--- a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/applayer/ApplicationLayerTestFactory.java
+++ b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/applayer/ApplicationLayerTestFactory.java
@@ -27,7 +27,6 @@ import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import java.util.HashSet;
import java.util.List;
-import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -46,7 +45,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.apache.isis.applib.annotation.Where;
-import org.apache.isis.applib.services.iactn.Interaction;
import org.apache.isis.applib.services.repository.RepositoryService;
import org.apache.isis.applib.services.wrapper.DisabledException;
import org.apache.isis.applib.services.wrapper.WrapperFactory;
@@ -59,7 +57,6 @@ import org.apache.isis.commons.internal.debug.xray.XrayUi;
import org.apache.isis.commons.internal.exceptions._Exceptions;
import org.apache.isis.commons.internal.functions._Functions.CheckedConsumer;
import org.apache.isis.core.interaction.session.InteractionFactory;
-import org.apache.isis.core.interaction.session.InteractionTracker;
import org.apache.isis.core.metamodel.interactions.managed.PropertyInteraction;
import org.apache.isis.core.metamodel.objectmanager.ObjectManager;
import org.apache.isis.core.metamodel.spec.ManagedObject;
@@ -89,7 +86,6 @@ public class ApplicationLayerTestFactory {
private final FixtureScripts fixtureScripts;
private final PreCommitListener preCommitListener;
private final InteractionFactory interactionFactory;
- private final InteractionTracker interactionTracker;
@Named("transaction-aware-pmf-proxy")
private final PersistenceManagerFactory pmf;
@@ -123,30 +119,30 @@ public class ApplicationLayerTestFactory {
val dynamicTests = Can.<DynamicTest>of(
- interactionTest("Programmatic Execution",
- given, verifier,
- VerificationStage.POST_INTERACTION_WHEN_PROGRAMMATIC,
- this::programmaticExecution),
+// interactionTest("Programmatic Execution",
+// given, verifier,
+// VerificationStage.POST_INTERACTION_WHEN_PROGRAMMATIC,
+// this::programmaticExecution),
interactionTest("Interaction Api Execution",
given, verifier,
VerificationStage.POST_INTERACTION,
- this::interactionApiExecution),
- interactionTest("Wrapper Sync Execution w/o Rules",
- given, verifier,
- VerificationStage.POST_INTERACTION,
- this::wrapperSyncExecutionNoRules),
- interactionTest("Wrapper Sync Execution w/ Rules (expected to fail w/ DisabledException)",
- given, verifier,
- VerificationStage.POST_INTERACTION,
- this::wrapperSyncExecutionWithFailure),
- interactionTest("Wrapper Async Execution w/o Rules",
- given, verifier,
- VerificationStage.POST_INTERACTION,
- this::wrapperAsyncExecutionNoRules),
- interactionTest("Wrapper Async Execution w/ Rules (expected to fail w/ DisabledException)",
- given, verifier,
- VerificationStage.POST_INTERACTION,
- this::wrapperAsyncExecutionWithFailure)
+ this::interactionApiExecution)
+// interactionTest("Wrapper Sync Execution w/o Rules",
+// given, verifier,
+// VerificationStage.POST_INTERACTION,
+// this::wrapperSyncExecutionNoRules),
+// interactionTest("Wrapper Sync Execution w/ Rules (expected to fail w/ DisabledException)",
+// given, verifier,
+// VerificationStage.POST_INTERACTION,
+// this::wrapperSyncExecutionWithFailure),
+// interactionTest("Wrapper Async Execution w/o Rules",
+// given, verifier,
+// VerificationStage.POST_INTERACTION,
+// this::wrapperAsyncExecutionNoRules),
+// interactionTest("Wrapper Async Execution w/ Rules (expected to fail w/ DisabledException)",
+// given, verifier,
+// VerificationStage.POST_INTERACTION,
+// this::wrapperAsyncExecutionWithFailure)
);
return XrayUi.isXrayEnabled()
@@ -180,10 +176,7 @@ public class ApplicationLayerTestFactory {
assert_no_initial_tx_context();
final boolean isSuccesfulRun = interactionFactory.callAnonymous(()->{
- val currentInteraction = interactionTracker.currentInteraction();
- xrayEnterInteraction(currentInteraction);
val result = interactionTestRunner.run(given, verifier);
- xrayExitInteraction();
return result;
});
@@ -438,22 +431,20 @@ public class ApplicationLayerTestFactory {
private void withBookDoTransactional(CheckedConsumer<JdoBook> transactionalBookConsumer) {
- xrayEnterTansaction(Propagation.REQUIRES_NEW);
-
transactionService.runTransactional(Propagation.REQUIRES_NEW, ()->{
val book = repository.allInstances(JdoBook.class).listIterator().next();
transactionalBookConsumer.accept(book);
})
.optionalElseFail();
-
- xrayExitTansaction();
}
// -- XRAY
private void xrayAddTest(String name) {
+ _Probe.errOut("TESTING %s", name);
+
val threadId = XrayUtil.currentThreadAsMemento();
XrayUi.updateModel(model->{
@@ -465,16 +456,5 @@ public class ApplicationLayerTestFactory {
}
- private void xrayEnterTansaction(Propagation propagation) {
- }
-
- private void xrayExitTansaction() {
- }
-
- private void xrayEnterInteraction(Optional<Interaction> currentInteraction) {
- }
-
- private void xrayExitInteraction() {
- }
}
diff --git a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/applayer/publishing/EntityPropertyChangeSubscriberForTesting.java b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/applayer/publishing/EntityPropertyChangeSubscriberForTesting.java
index 8ba9862..66f87b2 100644
--- a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/applayer/publishing/EntityPropertyChangeSubscriberForTesting.java
+++ b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/applayer/publishing/EntityPropertyChangeSubscriberForTesting.java
@@ -34,7 +34,8 @@ import org.apache.isis.testdomain.util.kv.KVStoreForTesting;
import lombok.val;
import lombok.extern.log4j.Log4j2;
-@Service @Log4j2
+@Service
+@Log4j2
public class EntityPropertyChangeSubscriberForTesting
implements EntityPropertyChangeSubscriber {