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 2022/10/27 11:30:34 UTC

[isis] branch master updated: ISIS-3265: init event sources only lazily

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

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


The following commit(s) were added to refs/heads/master by this push:
     new aecc7a3a92 ISIS-3265: init event sources only lazily
aecc7a3a92 is described below

commit aecc7a3a92855ce7ea1f8eba280e3b555b6cb015
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Oct 27 13:30:26 2022 +0200

    ISIS-3265: init event sources only lazily
    
    - don't fetch entities if not used
---
 .../causeway/applib/events/EventObjectBase.java    | 78 ++++++++++++++--------
 .../events/lifecycle/ObjectCreatedEvent.java       |  2 +-
 .../core/metamodel/facets/DomainEventHelper.java   | 19 ++----
 .../facets/object/callbacks/CallbackFacet.java     | 22 +-----
 ...ObjectLayoutAnnotationUsingCssClassUiEvent.java | 22 ++----
 ...mainObjectLayoutAnnotationUsingIconUiEvent.java | 23 ++-----
 ...inObjectLayoutAnnotationUsingLayoutUiEvent.java | 25 ++-----
 ...ainObjectLayoutAnnotationUsingTitleUiEvent.java | 30 ++-------
 .../core/metamodel/object/MmEntityUtil.java        | 11 +++
 .../publish/LifecycleCallbackNotifier.java         | 63 +++++++++--------
 10 files changed, 125 insertions(+), 170 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/events/EventObjectBase.java b/api/applib/src/main/java/org/apache/causeway/applib/events/EventObjectBase.java
index 3afd67bdbb..407a014c42 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/events/EventObjectBase.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/events/EventObjectBase.java
@@ -18,62 +18,88 @@
  */
 package org.apache.causeway.applib.events;
 
+import java.util.Optional;
+import java.util.function.Supplier;
+
 import org.springframework.lang.Nullable;
 
-import org.apache.causeway.commons.internal.exceptions._Exceptions;
+import org.apache.causeway.commons.functional.Try;
+import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.commons.internal.reflection._Reflect;
 
-import lombok.NonNull;
+import static org.apache.causeway.commons.internal.reflection._Reflect.Filter.paramCount;
 
 /**
  * @since 2.0 {@index}
  */
 public abstract class EventObjectBase<T> {
 
+    // -- FACTORIES
+
     /**
-     * The object on which the Event initially occurred.
+     * Optionally returns a new event instance,
+     * based on whether the eventType has a public no-arg constructor.
+     * <p>
+     * Initializes the event's source with given {@code source}.
      */
-    protected transient T source;
+    public static <T, E extends EventObjectBase<T>> Optional<E> getInstanceWithSource(
+            final Class<E> eventType, final T source) {
+        return getInstanceWithSourceSupplier(eventType, (Supplier<T>) ()->source);
+    }
 
     /**
-     * Constructs a prototypical Event.
-     *
-     * @param    source    The object on which the Event initially occurred.
+     * Optionally returns a new event instance,
+     * based on whether the eventType has a public no-arg constructor.
+     * <p>
+     * Initializes the event's source lazily, that is using given {@code eventSourceSupplier}.
      */
-    protected EventObjectBase(final @Nullable T source) {
-        this.source = source;
+    public static <T, E extends EventObjectBase<T>> Optional<E> getInstanceWithSourceSupplier(
+            final Class<E> eventType, final Supplier<T> eventSourceSupplier) {
+        return _Reflect.getPublicConstructors(eventType)
+            .filter(paramCount(0))
+            .getFirst()
+            .map(_Reflect::invokeConstructor)
+            .flatMap(Try::getValue)
+            .map(evnt->{
+                final E event = _Casts.uncheckedCast(evnt);
+                event.sourceSupplier = eventSourceSupplier;
+                return event;
+            });
     }
 
+    // --
+
     /**
-     * The object on which the Event initially occurred.
+     * Provides the object on which the Event initially occurred.
+     */
+    protected transient Supplier<T> sourceSupplier = null;
+
+    /**
+     * Constructs a prototypical Event.
      *
-     * @return   The object on which the Event initially occurred.
+     * @param source object on which the Event initially occurred (nullable)
      */
-    public @Nullable T getSource() {
-        return source;
+    protected EventObjectBase(final @Nullable T source) {
+        this.sourceSupplier = source!=null
+                ? ()->source
+                : null;
     }
 
     /**
-     * A one-shot function. Only allowed to be called if a source has not already been set.
-     *
-     * @apiNote reserved for framework internal use
-     *
-     * @param source non-null
+     * Returns the object on which the Event initially occurred.
      */
-    public void initSource(final @NonNull T source) {
-        if(this.source!=null) {
-            throw _Exceptions.illegalState(getClass().getName() + " cannot init when source is already set");
-        }
-        this.source = source;
+    public @Nullable T getSource() {
+        return sourceSupplier!=null
+                ? sourceSupplier.get()
+                : null;
     }
 
     /**
      * Returns a String representation of this EventObject.
-     *
-     * @return  a String representation of this EventObject
      */
     @Override
     public String toString() {
-        return getClass().getName() + "[source=" + source + "]";
+        return getClass().getName() + "[source=" + getSource() + "]";
     }
 
 }
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/events/lifecycle/ObjectCreatedEvent.java b/api/applib/src/main/java/org/apache/causeway/applib/events/lifecycle/ObjectCreatedEvent.java
index 4825cbeebb..20eee89c97 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/events/lifecycle/ObjectCreatedEvent.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/events/lifecycle/ObjectCreatedEvent.java
@@ -19,7 +19,7 @@
 package org.apache.causeway.applib.events.lifecycle;
 
 /**
- * Broadcast when an object (entiy or view model) is first instantiated using
+ * Broadcast when an object (entity or view model) is first instantiated using
  * the {@link org.apache.causeway.applib.services.factory.FactoryService}.
  *
  * @see org.apache.causeway.applib.services.factory.FactoryService#detachedEntity(Class)
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/DomainEventHelper.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/DomainEventHelper.java
index cac0ec18bf..a7dafe25b2 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/DomainEventHelper.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/DomainEventHelper.java
@@ -27,6 +27,7 @@ import java.util.List;
 import org.springframework.lang.Nullable;
 
 import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.applib.events.EventObjectBase;
 import org.apache.causeway.applib.events.domain.AbstractDomainEvent;
 import org.apache.causeway.applib.events.domain.ActionDomainEvent;
 import org.apache.causeway.applib.events.domain.CollectionDomainEvent;
@@ -178,11 +179,7 @@ public class DomainEventHelper {
                 .filter(paramCount(0))
                 .getFirst().orElse(null);
         if(noArgConstructor!=null) {
-
-            final Object event = invokeConstructor(noArgConstructor);
-            final ActionDomainEvent<S> ade = uncheckedCast(event);
-
-            ade.initSource(source);
+            final ActionDomainEvent<S> ade = EventObjectBase.getInstanceWithSource(type, source).orElseThrow();
             ade.setIdentifier(identifier);
             ade.setArguments(asList(arguments));
             return ade;
@@ -283,17 +280,14 @@ public class DomainEventHelper {
             final T oldValue,
             final T newValue) throws NoSuchMethodException, SecurityException, IllegalArgumentException {
 
+
         val constructors = _Reflect.getPublicConstructors(type);
 
         val noArgonstructor = constructors
                 .filter(paramCount(0))
                 .getFirst().orElse(null);
         if(noArgonstructor != null) {
-            final Object event = invokeConstructor(noArgonstructor);
-            final PropertyDomainEvent<S, T> pde = uncheckedCast(event);
-            if(source!=null) {
-                pde.initSource(source);
-            }
+            final PropertyDomainEvent<S, T> pde = EventObjectBase.getInstanceWithSource(type, source).orElseThrow();
             pde.setIdentifier(identifier);
             pde.setOldValue(oldValue);
             pde.setNewValue(newValue);
@@ -376,10 +370,7 @@ public class DomainEventHelper {
                 .filter(paramCount(0))
                 .getFirst().orElse(null);
         if(noArgConstructor != null) {
-            final Object event = invokeConstructor(noArgConstructor);
-            final CollectionDomainEvent<S, T> cde = uncheckedCast(event);
-
-            cde.initSource(source);
+            final CollectionDomainEvent<S, T> cde = EventObjectBase.getInstanceWithSource(type, source).orElseThrow();;
             cde.setIdentifier(identifier);
             return cde;
         }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/callbacks/CallbackFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/callbacks/CallbackFacet.java
index 6d94ee1026..3a3104908d 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/callbacks/CallbackFacet.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/callbacks/CallbackFacet.java
@@ -18,11 +18,9 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.callbacks;
 
-import org.apache.causeway.applib.exceptions.unrecoverable.DomainModelException;
 import org.apache.causeway.core.metamodel.facetapi.Facet;
 import org.apache.causeway.core.metamodel.facets.ImperativeFacet;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
-import org.apache.causeway.core.metamodel.object.ManagedObjects;
 
 /**
  * A {@link Facet} that represents some type of lifecycle callback on the object
@@ -31,24 +29,6 @@ import org.apache.causeway.core.metamodel.object.ManagedObjects;
 public interface CallbackFacet
 extends ImperativeFacet {
 
-    public void invoke(ManagedObject object);
-
-    public static void callCallback(
-            final ManagedObject object,
-            final Class<? extends CallbackFacet> callbackFacetType) {
-
-        ManagedObjects.asSpecified(object)
-        .map(ManagedObject::getSpecification)
-        .flatMap(spec->spec.lookupFacet(callbackFacetType))
-        .ifPresent(callbackFacet->{
-            try {
-                callbackFacet.invoke(object);
-            } catch (final RuntimeException e) {
-                throw new DomainModelException(
-                        "Callback failed.  Calling " + callbackFacet + " on " + object, e);
-            }
-        });
-
-    }
+    void invoke(ManagedObject object);
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/CssClassFacetViaDomainObjectLayoutAnnotationUsingCssClassUiEvent.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/CssClassFacetViaDomainObjectLayoutAnnotationUsingCssClassUiEvent.java
index 36abd7e042..080606fcd5 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/CssClassFacetViaDomainObjectLayoutAnnotationUsingCssClassUiEvent.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/CssClassFacetViaDomainObjectLayoutAnnotationUsingCssClassUiEvent.java
@@ -18,13 +18,12 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.domainobjectlayout;
 
-import java.lang.reflect.InvocationTargetException;
 import java.util.Optional;
 import java.util.function.BiConsumer;
 
 import org.apache.causeway.applib.annotation.DomainObjectLayout;
+import org.apache.causeway.applib.events.EventObjectBase;
 import org.apache.causeway.applib.events.ui.CssClassUiEvent;
-import org.apache.causeway.applib.exceptions.UnrecoverableException;
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.facets.members.cssclass.CssClassFacet;
@@ -57,16 +56,16 @@ extends CssClassFacetAbstract {
 
     }
 
-    private final Class<? extends CssClassUiEvent<?>> cssClassUiEventClass;
+    private final Class<? extends CssClassUiEvent<Object>> cssClassUiEventClass;
     private final MetamodelEventService metamodelEventService;
 
     private CssClassFacetViaDomainObjectLayoutAnnotationUsingCssClassUiEvent(
             final Class<? extends CssClassUiEvent<?>> cssClassUiEventClass,
-                    final MetamodelEventService metamodelEventService,
-                    final FacetHolder holder) {
+            final MetamodelEventService metamodelEventService,
+            final FacetHolder holder) {
 
         super(holder, Precedence.EVENT);
-        this.cssClassUiEventClass = cssClassUiEventClass;
+        this.cssClassUiEventClass = _Casts.uncheckedCast(cssClassUiEventClass);
         this.metamodelEventService = metamodelEventService;
     }
 
@@ -104,16 +103,7 @@ extends CssClassFacetAbstract {
     }
 
     private CssClassUiEvent<Object> newCssClassUiEventForPojo(final Object domainObject) {
-        try {
-            final CssClassUiEvent<Object> cssClassUiEvent = _Casts.uncheckedCast(
-                    cssClassUiEventClass.getConstructor().newInstance());
-            cssClassUiEvent.initSource(domainObject);
-            return cssClassUiEvent;
-        } catch (InstantiationException | IllegalAccessException
-                | IllegalArgumentException | InvocationTargetException
-                | NoSuchMethodException | SecurityException ex) {
-            throw new UnrecoverableException(ex);
-        }
+        return EventObjectBase.getInstanceWithSource(cssClassUiEventClass, domainObject).orElseThrow();
     }
 
     @Override
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/IconFacetViaDomainObjectLayoutAnnotationUsingIconUiEvent.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/IconFacetViaDomainObjectLayoutAnnotationUsingIconUiEvent.java
index 20f4b64ef5..2e1e1fba8f 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/IconFacetViaDomainObjectLayoutAnnotationUsingIconUiEvent.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/IconFacetViaDomainObjectLayoutAnnotationUsingIconUiEvent.java
@@ -18,13 +18,12 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.domainobjectlayout;
 
-import java.lang.reflect.InvocationTargetException;
 import java.util.Optional;
 import java.util.function.BiConsumer;
 
 import org.apache.causeway.applib.annotation.DomainObjectLayout;
+import org.apache.causeway.applib.events.EventObjectBase;
 import org.apache.causeway.applib.events.ui.IconUiEvent;
-import org.apache.causeway.applib.exceptions.UnrecoverableException;
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.facets.object.icon.IconFacet;
@@ -56,7 +55,7 @@ extends IconFacetAbstract {
                 });
     }
 
-    private final Class<? extends IconUiEvent<?>> iconUiEventClass;
+    private final Class<? extends IconUiEvent<Object>> iconUiEventClass;
     private final MetamodelEventService metamodelEventService;
 
     public IconFacetViaDomainObjectLayoutAnnotationUsingIconUiEvent(
@@ -64,7 +63,7 @@ extends IconFacetAbstract {
                     final MetamodelEventService metamodelEventService,
                     final FacetHolder holder) {
         super(holder, Precedence.EVENT);
-        this.iconUiEventClass = iconUiEventClass;
+        this.iconUiEventClass = _Casts.uncheckedCast(iconUiEventClass);
         this.metamodelEventService = metamodelEventService;
     }
 
@@ -97,21 +96,7 @@ extends IconFacetAbstract {
     }
 
     private IconUiEvent<Object> newIconUiEvent(final ManagedObject owningAdapter) {
-        final Object domainObject = owningAdapter.getPojo();
-        return newIconUiEventForPojo(domainObject);
-    }
-
-    private IconUiEvent<Object> newIconUiEventForPojo(final Object domainObject) {
-        try {
-            final IconUiEvent<Object> iconUiEvent = _Casts.uncheckedCast(
-                    iconUiEventClass.getConstructor().newInstance());
-            iconUiEvent.initSource(domainObject);
-            return iconUiEvent;
-        } catch (InstantiationException | IllegalAccessException
-                | IllegalArgumentException | InvocationTargetException
-                | NoSuchMethodException | SecurityException ex) {
-            throw new UnrecoverableException(ex);
-        }
+        return EventObjectBase.getInstanceWithSourceSupplier(iconUiEventClass, owningAdapter::getPojo).orElseThrow();
     }
 
     @Override
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/LayoutFacetViaDomainObjectLayoutAnnotationUsingLayoutUiEvent.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/LayoutFacetViaDomainObjectLayoutAnnotationUsingLayoutUiEvent.java
index 3d72be8fdf..46ba23e5d9 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/LayoutFacetViaDomainObjectLayoutAnnotationUsingLayoutUiEvent.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/LayoutFacetViaDomainObjectLayoutAnnotationUsingLayoutUiEvent.java
@@ -18,15 +18,13 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.domainobjectlayout;
 
-import java.lang.reflect.InvocationTargetException;
 import java.util.Optional;
 import java.util.function.BiConsumer;
 
 import org.apache.causeway.applib.annotation.DomainObjectLayout;
+import org.apache.causeway.applib.events.EventObjectBase;
 import org.apache.causeway.applib.events.ui.LayoutUiEvent;
-import org.apache.causeway.applib.exceptions.UnrecoverableException;
 import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.core.metamodel.facetapi.Facet.Precedence;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.facets.object.layout.LayoutFacet;
 import org.apache.causeway.core.metamodel.facets.object.layout.LayoutFacetAbstract;
@@ -58,7 +56,7 @@ implements LayoutFacet {
                 });
     }
 
-    private final Class<? extends LayoutUiEvent<?>> layoutUiEventClass;
+    private final Class<? extends LayoutUiEvent<Object>> layoutUiEventClass;
     private final MetamodelEventService metamodelEventService;
 
     private LayoutFacetViaDomainObjectLayoutAnnotationUsingLayoutUiEvent(
@@ -66,7 +64,7 @@ implements LayoutFacet {
                     final MetamodelEventService metamodelEventService,
                     final FacetHolder holder) {
         super(holder, Precedence.EVENT);
-        this.layoutUiEventClass = layoutUiEventClass;
+        this.layoutUiEventClass = _Casts.uncheckedCast(layoutUiEventClass);
         this.metamodelEventService = metamodelEventService;
     }
 
@@ -99,22 +97,7 @@ implements LayoutFacet {
     }
 
     private LayoutUiEvent<Object> newLayoutUiEvent(final ManagedObject owningAdapter) {
-        final Object domainObject = owningAdapter.getPojo();
-        return newLayoutUiEvent(domainObject);
-    }
-
-    private LayoutUiEvent<Object> newLayoutUiEvent(final Object domainObject) {
-        try {
-            final LayoutUiEvent<Object> layoutUiEvent =
-                    _Casts.uncheckedCast(
-                            layoutUiEventClass.getConstructor().newInstance());
-            layoutUiEvent.initSource(domainObject);
-            return layoutUiEvent;
-        } catch (InstantiationException | IllegalAccessException
-                | IllegalArgumentException | InvocationTargetException
-                | NoSuchMethodException | SecurityException ex) {
-            throw new UnrecoverableException(ex);
-        }
+        return EventObjectBase.getInstanceWithSourceSupplier(layoutUiEventClass, owningAdapter::getPojo).orElseThrow();
     }
 
     @Override
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/TitleFacetViaDomainObjectLayoutAnnotationUsingTitleUiEvent.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/TitleFacetViaDomainObjectLayoutAnnotationUsingTitleUiEvent.java
index e42e34e43a..cc84cbe2e6 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/TitleFacetViaDomainObjectLayoutAnnotationUsingTitleUiEvent.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobjectlayout/TitleFacetViaDomainObjectLayoutAnnotationUsingTitleUiEvent.java
@@ -18,18 +18,16 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.domainobjectlayout;
 
-import java.lang.reflect.InvocationTargetException;
 import java.util.Optional;
 import java.util.function.BiConsumer;
 
 import org.apache.causeway.applib.annotation.DomainObjectLayout;
+import org.apache.causeway.applib.events.EventObjectBase;
 import org.apache.causeway.applib.events.ui.TitleUiEvent;
-import org.apache.causeway.applib.exceptions.UnrecoverableException;
 import org.apache.causeway.applib.services.i18n.TranslatableString;
 import org.apache.causeway.applib.services.i18n.TranslationContext;
 import org.apache.causeway.applib.services.i18n.TranslationService;
 import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.core.metamodel.facetapi.Facet.Precedence;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.facets.object.title.TitleFacet;
 import org.apache.causeway.core.metamodel.facets.object.title.TitleFacetAbstract;
@@ -73,18 +71,18 @@ extends TitleFacetAbstract {
                 });
     }
 
-    private final Class<? extends TitleUiEvent<?>> titleUiEventClass;
+    private final Class<? extends TitleUiEvent<Object>> titleUiEventClass;
     private final TranslationService translationService;
     private final TranslationContext translationContext;
     private final MetamodelEventService metamodelEventService;
 
     public TitleFacetViaDomainObjectLayoutAnnotationUsingTitleUiEvent(
             final Class<? extends TitleUiEvent<?>> titleUiEventClass,
-                    final TranslationContext translationContext,
-                    final MetamodelEventService metamodelEventService,
-                    final FacetHolder holder) {
+            final TranslationContext translationContext,
+            final MetamodelEventService metamodelEventService,
+            final FacetHolder holder) {
         super(holder, Precedence.EVENT);
-        this.titleUiEventClass = titleUiEventClass;
+        this.titleUiEventClass = _Casts.uncheckedCast(titleUiEventClass);
         this.translationService = super.getTranslationService();
         this.translationContext = translationContext;
         this.metamodelEventService = metamodelEventService;
@@ -139,21 +137,7 @@ extends TitleFacetAbstract {
     }
 
     private TitleUiEvent<Object> newTitleUiEvent(final ManagedObject owningAdapter) {
-        final Object domainObject = owningAdapter.getPojo();
-        return newTitleUiEvent(domainObject);
-    }
-
-    private TitleUiEvent<Object> newTitleUiEvent(final Object domainObject) {
-        try {
-            final TitleUiEvent<Object> titleUiEvent = _Casts.uncheckedCast(
-                    titleUiEventClass.getConstructor().newInstance());
-            titleUiEvent.initSource(domainObject);
-            return titleUiEvent;
-        } catch (InstantiationException | IllegalAccessException
-                | IllegalArgumentException | InvocationTargetException
-                | NoSuchMethodException | SecurityException ex) {
-            throw new UnrecoverableException(ex);
-        }
+        return EventObjectBase.getInstanceWithSourceSupplier(titleUiEventClass, owningAdapter::getPojo).orElseThrow();
     }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtil.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtil.java
index 5c3de0e3ec..fd17e5b76f 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtil.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtil.java
@@ -100,6 +100,17 @@ public final class MmEntityUtil {
         }
     }
 
+    /**
+     * Side-effect free check for whether given entity is attached.
+     */
+    public static boolean isAttachedEntity(final @Nullable ManagedObject entity) {
+        return entity!=null
+                ? entity.getSpecialization().isEntity()
+                    && entity.isBookmarkMemoized()
+                    && entity.getEntityState().isAttached()
+                : false;
+    }
+
     /**
      * @param managedObject
      * @return managedObject
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/LifecycleCallbackNotifier.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/LifecycleCallbackNotifier.java
index 982ecad65d..fe8092077d 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/LifecycleCallbackNotifier.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/LifecycleCallbackNotifier.java
@@ -28,11 +28,12 @@ import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Component;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.events.EventObjectBase;
 import org.apache.causeway.applib.events.lifecycle.AbstractLifecycleEvent;
+import org.apache.causeway.applib.exceptions.unrecoverable.DomainModelException;
 import org.apache.causeway.applib.services.eventbus.EventBusService;
 import org.apache.causeway.commons.functional.Either;
 import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.factory._InstanceUtil;
 import org.apache.causeway.core.metamodel.facets.object.callbacks.CallbackFacet;
 import org.apache.causeway.core.metamodel.facets.object.callbacks.CreatedCallbackFacet;
 import org.apache.causeway.core.metamodel.facets.object.callbacks.CreatedLifecycleEventFacet;
@@ -55,6 +56,7 @@ import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeService
 import org.apache.causeway.core.transaction.changetracking.events.PostStoreEvent;
 import org.apache.causeway.core.transaction.changetracking.events.PreStoreEvent;
 
+import lombok.NonNull;
 import lombok.RequiredArgsConstructor;
 import lombok.val;
 
@@ -70,17 +72,14 @@ import lombok.val;
 //@Log4j2
 public class LifecycleCallbackNotifier {
 
-    final EventBusService eventBusService;
-    //final SpecificationLoader specLoader;
+    final @NonNull EventBusService eventBusService;
 
     public void postCreate(final ManagedObject entity) {
-        CallbackFacet.callCallback(entity, CreatedCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, CreatedLifecycleEventFacet.class);
+        dispatch(entity, CreatedCallbackFacet.class, CreatedLifecycleEventFacet.class);
     }
 
     public void postLoad(final ManagedObject entity) {
-        CallbackFacet.callCallback(entity, LoadedCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, LoadedLifecycleEventFacet.class);
+        dispatch(entity, LoadedCallbackFacet.class, LoadedLifecycleEventFacet.class);
     }
 
     /**
@@ -92,56 +91,62 @@ public class LifecycleCallbackNotifier {
         if(pojo==null) {return;}
         eventBusService.post(PreStoreEvent.of(pojo));
         val entity = eitherWithOrWithoutOid.fold(UnaryOperator.identity(), UnaryOperator.identity());
-        CallbackFacet.callCallback(entity, PersistingCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, PersistingLifecycleEventFacet.class);
+        dispatch(entity, PersistingCallbackFacet.class, PersistingLifecycleEventFacet.class);
     }
 
     public void postPersist(final ManagedObject entity) {
         eventBusService.post(PostStoreEvent.of(entity.getPojo()));
-        CallbackFacet.callCallback(entity, PersistedCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, PersistedLifecycleEventFacet.class);
+        dispatch(entity, PersistedCallbackFacet.class, PersistedLifecycleEventFacet.class);
     }
 
     public void preUpdate(final ManagedObject entity) {
         eventBusService.post(PreStoreEvent.of(entity.getPojo()));
-        CallbackFacet.callCallback(entity, UpdatingCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, UpdatingLifecycleEventFacet.class);
+        dispatch(entity, UpdatingCallbackFacet.class, UpdatingLifecycleEventFacet.class);
     }
 
     public void postUpdate(final ManagedObject entity) {
-        CallbackFacet.callCallback(entity, UpdatedCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, UpdatedLifecycleEventFacet.class);
+        dispatch(entity, UpdatedCallbackFacet.class, UpdatedLifecycleEventFacet.class);
     }
 
     public void preRemove(final ManagedObject entity) {
-        CallbackFacet.callCallback(entity, RemovingCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, RemovingLifecycleEventFacet.class);
+        dispatch(entity, RemovingCallbackFacet.class, RemovingLifecycleEventFacet.class);
     }
 
     //  -- HELPER
 
-    private void postLifecycleEventIfRequired(
-            final ManagedObject object,
+    private void dispatch(
+            final ManagedObject entity,
+            final Class<? extends CallbackFacet> callbackFacetType,
             final Class<? extends LifecycleEventFacet> lifecycleEventFacetClass) {
-        ManagedObjects.asSpecified(object)
+
+        ManagedObjects.asSpecified(entity)
         .map(ManagedObject::getSpecification)
         .ifPresent(spec->{
 
+            spec.lookupFacet(callbackFacetType)
+            .ifPresent(callbackFacet->invokeCallback(callbackFacet, entity));
+
             spec.lookupFacet(lifecycleEventFacetClass)
             .map(LifecycleEventFacet::getEventType)
-            .map(_InstanceUtil::createInstance)
-            .ifPresent(eventInstance->{
-                postEvent(_Casts.uncheckedCast(eventInstance), object.getPojo());
-            });
-
+            .ifPresent(eventType->postEvent(_Casts.uncheckedCast(eventType), entity));
         });
     }
 
-    protected void postEvent(final AbstractLifecycleEvent<Object> event, final Object pojo) {
-        if(eventBusService!=null) {
-            event.initSource(pojo);
-            eventBusService.post(event);
+    protected void invokeCallback(final CallbackFacet callbackFacet, final ManagedObject entity) {
+        try {
+            callbackFacet.invoke(entity);
+        } catch (final RuntimeException e) {
+            throw new DomainModelException(
+                    "Callback failed.  Calling " + callbackFacet + " on " + entity, e);
         }
     }
 
+    protected void postEvent(
+            final Class<? extends AbstractLifecycleEvent<Object>> eventType,
+            final ManagedObject entity) {
+        EventObjectBase
+            .getInstanceWithSourceSupplier(eventType, entity::getPojo)
+            .ifPresent(eventBusService::post);
+    }
+
 }