You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2024/11/17 15:03:42 UTC

(causeway) branch 2297-member.container created (now f43d8082bc8)

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

ahuber pushed a change to branch 2297-member.container
in repository https://gitbox.apache.org/repos/asf/causeway.git


      at f43d8082bc8 CAUSEWAY-2297: lazy hierarchical introspection

This branch includes the following new commits:

     new c565894c28e CAUSEWAY-2297: refactor member container into standalone class
     new f43d8082bc8 CAUSEWAY-2297: lazy hierarchical introspection

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(causeway) 01/02: CAUSEWAY-2297: refactor member container into standalone class

Posted by ah...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch 2297-member.container
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit c565894c28e7339d30d983ebd9ebfde867323501
Author: andi-huber <ah...@apache.org>
AuthorDate: Sat Nov 16 19:09:13 2024 +0100

    CAUSEWAY-2297: refactor member container into standalone class
---
 .../specloader/specimpl/ObjectActionMixedIn.java   | 22 +++++----
 .../specloader/specimpl/ObjectMemberAbstract.java  |  7 ++-
 .../specloader/specimpl/ObjectMemberContainer.java |  8 ++--
 .../specimpl/ObjectSpecificationAbstract.java      | 55 +++++++++++++--------
 .../specimpl/ObjectSpecificationBody.java          | 56 ++++++++++++++++++++++
 .../specimpl/OneToManyAssociationMixedIn.java      | 14 +++---
 .../specimpl/OneToOneAssociationMixedIn.java       | 14 +++---
 .../specloader/specimpl/_MixedInMemberFactory.java | 10 ++--
 .../metamodel/facets/FacetFactoryTestAbstract.java |  9 ++--
 9 files changed, 134 insertions(+), 61 deletions(-)

diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectActionMixedIn.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectActionMixedIn.java
index 8530baf7e42..60d4d7e6be5 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectActionMixedIn.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectActionMixedIn.java
@@ -58,20 +58,22 @@ implements MixedInMember {
     public static class forTesting {
         public static ObjectActionMixedIn forMixinMain(
                 final ObjectSpecification mixeeSpec,
-                final Class<?> mixinType,
+                final ObjectSpecification mixinSpec,
                 final String mixinMethodName,
                 final FacetedMethod facetedMethod) {
             final ObjectActionDefault mixinAction = (ObjectActionDefault) ObjectActionDefault.forTesting.forMixinMain(facetedMethod);
-            return new ObjectActionMixedIn(mixinType, mixinMethodName, mixinAction, mixeeSpec);
+            return new ObjectActionMixedIn(mixinSpec, mixinMethodName, mixinAction, mixeeSpec);
         }
     }
 
     // -- CONSTRUCTION
 
     /**
-     * The type of the mixin (providing the action), eg annotated with @{@link DomainObject DomainObject}#{@link DomainObject#nature() nature} of {@link org.apache.causeway.applib.annotation.Nature#MIXIN MIXIN}.
+     * The type of the mixin (providing the action), 
+     * eg annotated with @{@link DomainObject DomainObject}#{@link DomainObject#nature() nature} 
+     * of {@link org.apache.causeway.applib.annotation.Nature#MIXIN MIXIN}.
      */
-    private final Class<?> mixinType;
+    private final ObjectSpecification mixinSpec;
 
     /**
      * The {@link ObjectActionDefault} for the action being mixed in (ie on the {@link #mixinType}.
@@ -90,7 +92,7 @@ implements MixedInMember {
     private final FacetHolder facetHolder;
 
     public ObjectActionMixedIn(
-            final Class<?> mixinType,
+            final ObjectSpecification mixinSpec,
             final String mixinMethodName,
             final ObjectActionDefault mixinAction,
             final ObjectSpecification mixeeSpec) {
@@ -106,7 +108,7 @@ implements MixedInMember {
         this.facetHolder = FacetHolder.layered(
                 super.getFeatureIdentifier(),
                 mixinAction.getFacetedMethod());
-        this.mixinType = mixinType;
+        this.mixinSpec = mixinSpec;
         this.mixinAction = mixinAction;
         this.mixeeSpec = mixeeSpec;
 
@@ -126,7 +128,7 @@ implements MixedInMember {
     protected InteractionHead headFor(final ManagedObject owner) {
         return InteractionHead.mixin(
                 owner,
-                mixinAdapterFor(mixinType, owner));
+                mixinAdapterFor(mixinSpec, owner));
     }
 
     @Override
@@ -163,7 +165,7 @@ implements MixedInMember {
     }
 
     protected ManagedObject mixinAdapterFor(final ManagedObject mixeeAdapter) {
-        return mixinAdapterFor(mixinType, mixeeAdapter);
+        return mixinAdapterFor(mixinSpec, mixeeAdapter);
     }
 
     @Override
@@ -173,7 +175,7 @@ implements MixedInMember {
             final InteractionInitiatedBy interactionInitiatedBy) {
 
         final ManagedObject owner = head.getOwner();
-        final ManagedObject target = mixinAdapterFor(mixinType, owner);
+        final ManagedObject target = mixinAdapterFor(mixinSpec, owner);
         _Assert.assertEquals(target.getSpecification(), head.getTarget().getSpecification(),
                 "head has the wrong target (should be a mixed-in adapter, but is the mixee adapter)");
 
@@ -199,7 +201,7 @@ implements MixedInMember {
 
     @Override
     public ObjectSpecification getMixinType() {
-        return getSpecificationLoader().loadSpecification(mixinType);
+        return mixinSpec;
     }
 
     @Getter(lazy=true, onMethod_ = {@Override})
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberAbstract.java
index 362ccefc1e9..0073c560a0b 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberAbstract.java
@@ -51,6 +51,7 @@ import org.apache.causeway.core.metamodel.interactions.VisibilityContext;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.services.command.CommandDtoFactory;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedInMember;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
 import org.apache.causeway.schema.cmd.v2.CommandDto;
@@ -265,17 +266,15 @@ implements
     // -- MIXIN ADAPTER FACTORY
 
     protected ManagedObject mixinAdapterFor(
-            final @NonNull Class<?> mixinType,
+            final @NonNull ObjectSpecification mixinSpec,
             final @NonNull ManagedObject mixee) {
 
-        var mixinSpec = getSpecificationLoader().loadSpecification(mixinType);
-
         // nullable for action parameter mixins
         if(ManagedObjects.isNullOrUnspecifiedOrEmpty(mixee)) {
             return ManagedObject.empty(mixinSpec);
         }
 
-        var mixinPojo = getMetaModelContext().getFactoryService().mixin(mixinType, mixee.getPojo());
+        var mixinPojo = getFactoryService().mixin(mixinSpec.getCorrespondingClass(), mixee.getPojo());
         return ManagedObject.mixin(mixinSpec, mixinPojo);
     }
 
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberContainer.java
index b5a8801e48c..f03616c9d12 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberContainer.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberContainer.java
@@ -67,7 +67,7 @@ implements
     // -- ACTIONS
 
     @Override
-    public Optional<ObjectAction> getAction(
+    public final Optional<ObjectAction> getAction(
             final String id, final ImmutableEnumSet<ActionScope> scopes, final MixedIn mixedIn) {
 
         var declaredAction = getDeclaredAction(id, mixedIn); // no inheritance nor type considered
@@ -86,7 +86,7 @@ implements
     }
 
     @Override
-    public Stream<ObjectAction> streamActions(
+    public final Stream<ObjectAction> streamActions(
             final ImmutableEnumSet<ActionScope> actionTypes,
             final MixedIn mixedIn,
             final Consumer<ObjectAction> onActionOverloaded) {
@@ -125,7 +125,7 @@ implements
     // -- ASSOCIATIONS
 
     @Override
-    public Optional<ObjectAssociation> getAssociation(final String id, final MixedIn mixedIn) {
+    public final Optional<ObjectAssociation> getAssociation(final String id, final MixedIn mixedIn) {
 
         var declaredAssociation = getDeclaredAssociation(id, mixedIn); // no inheritance considered
 
@@ -139,7 +139,7 @@ implements
     }
 
     @Override
-    public Stream<ObjectAssociation> streamAssociations(final MixedIn mixedIn) {
+    public final Stream<ObjectAssociation> streamAssociations(final MixedIn mixedIn) {
 
         if(isTypeHierarchyRoot()) {
             return streamDeclaredAssociations(mixedIn); // stop going deeper
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
index 968f87655a3..53472c6bb04 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
@@ -22,12 +22,11 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.Stack;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import org.apache.causeway.core.metamodel.spi.EntityTitleSubscriber;
-
 import org.springframework.util.ClassUtils;
 
 import org.apache.causeway.applib.Identifier;
@@ -91,6 +90,7 @@ import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
 import org.apache.causeway.core.metamodel.specloader.facetprocessor.FacetProcessor;
 import org.apache.causeway.core.metamodel.specloader.postprocessor.PostProcessor;
+import org.apache.causeway.core.metamodel.spi.EntityTitleSubscriber;
 import org.apache.causeway.core.metamodel.util.Facets;
 
 import static org.apache.causeway.commons.internal.base._NullSafe.stream;
@@ -138,27 +138,22 @@ implements ObjectSpecification {
 
     private final PostProcessor postProcessor;
     private final FacetProcessor facetProcessor;
+    private final ObjectSpecificationBody body = new ObjectSpecificationBody();
 
     @Getter private final BeanSort beanSort;
 
     // -- ASSOCIATIONS
 
-    private final List<ObjectAssociation> associations = _Lists.newArrayList();
-
     // defensive immutable lazy copy of associations
-    private final _Lazy<Can<ObjectAssociation>> unmodifiableAssociations =
-            _Lazy.threadSafe(()->Can.ofCollection(associations));
+    private final _Lazy<Can<ObjectAssociation>> unmodifiableAssociations = _Lazy.threadSafe(body::snapshotAssociations);
 
     // -- ACTIONS
-
-    private final List<ObjectAction> objectActions = _Lists.newArrayList();
-
+    
     /** not API, used for validation */
     @Getter private final Set<ResolvedMethod> potentialOrphans = _Sets.newHashSet();
 
     // defensive immutable lazy copy of objectActions
-    private final _Lazy<Can<ObjectAction>> unmodifiableActions =
-            _Lazy.threadSafe(()->Can.ofCollection(objectActions));
+    private final _Lazy<Can<ObjectAction>> unmodifiableActions = _Lazy.threadSafe(body::snapshotActions);
 
     // partitions and caches objectActions by type; updated in sortCacheAndUpdateActions()
     private final ListMultimap<ActionScope, ObjectAction> objectActionsByType =
@@ -311,11 +306,23 @@ implements ObjectSpecification {
         this.introspectionState = IntrospectionState.TYPE_INTROSPECTED;
     }
 
+    //TODO[causeway-core-metamodel] remove 
+    final static ThreadLocal<Stack<String>> callStack = ThreadLocal.withInitial(Stack::new);
+    
     private void introspectFully() {
 
         // set to avoid infinite loops
         this.introspectionState = IntrospectionState.MEMBERS_BEING_INTROSPECTED;
+        
+//        callStack.get().push(shortName);
+    //    long start = System.nanoTime();
         introspectMembers();
+  //      long dura = (System.nanoTime() - start)/1000_000;
+      //  callStack.get().pop();
+        
+//        if(dura>5) System.err.printf("%s %s %s->%dms%n", 
+//                Can.ofCollection(callStack.get()).join(", "), beanSort.name(), logicalType.getClassName(), dura);        
+        
         this.introspectionState = IntrospectionState.FULLY_INTROSPECTED;
 
         // make sure we've loaded the facets from layout.xml also.
@@ -373,8 +380,7 @@ implements ObjectSpecification {
     protected final void replaceAssociations(final Stream<ObjectAssociation> associations) {
         var orderedAssociations = _MemberSortingUtils.sortAssociationsIntoList(associations);
         synchronized (unmodifiableAssociations) {
-            this.associations.clear();
-            this.associations.addAll(orderedAssociations);
+            body.replaceAssociations(orderedAssociations);
             unmodifiableAssociations.clear(); // invalidate
         }
     }
@@ -382,8 +388,7 @@ implements ObjectSpecification {
     protected final void replaceActions(final Stream<ObjectAction> objectActions) {
         var orderedActions = _MemberSortingUtils.sortActionsIntoList(objectActions);
         synchronized (unmodifiableActions){
-            this.objectActions.clear();
-            this.objectActions.addAll(orderedActions);
+            body.replaceActions(orderedActions);
             unmodifiableActions.clear(); // invalidate
 
             // rebuild objectActionsByType multi-map
@@ -765,10 +770,14 @@ implements ObjectSpecification {
                 .flatMap(this::createMixedInAssociation);
     }
 
+    @Deprecated //REFACTOR: remove circular dependency specloader<->objecspec
     private Stream<ObjectAssociation> createMixedInAssociation(final Class<?> mixinType) {
-
         var mixinSpec = getSpecificationLoader().loadSpecification(mixinType,
                 IntrospectionState.FULLY_INTROSPECTED);
+        return createMixedInAssociation(mixinSpec);
+    }
+    
+    private Stream<ObjectAssociation> createMixedInAssociation(final ObjectSpecification mixinSpec) {
         if (mixinSpec == null
                 || mixinSpec == this) {
             return Stream.empty();
@@ -786,7 +795,7 @@ implements ObjectSpecification {
         return mixinSpec.streamActions(ActionScope.ANY, MixedIn.EXCLUDED)
         .filter(_SpecPredicates::isMixedInAssociation)
         .map(ObjectActionDefault.class::cast)
-        .map(_MixedInMemberFactory.mixedInAssociation(this, mixinType, mixinMethodName))
+        .map(_MixedInMemberFactory.mixedInAssociation(this, mixinSpec, mixinMethodName))
         .peek(facetProcessor::processMemberOrder);
     }
 
@@ -799,10 +808,14 @@ implements ObjectSpecification {
             .flatMap(this::createMixedInAction);
     }
 
+    @Deprecated //REFACTOR: remove circular dependency specloader<->objecspec
     private Stream<ObjectActionMixedIn> createMixedInAction(final Class<?> mixinType) {
-
         var mixinSpec = getSpecificationLoader().loadSpecification(mixinType,
                 IntrospectionState.FULLY_INTROSPECTED);
+        return createMixedInAction(mixinSpec);
+    }
+    
+    private Stream<ObjectActionMixedIn> createMixedInAction(final ObjectSpecification mixinSpec) {
         if (mixinSpec == null
                 || mixinSpec == this) {
             return Stream.empty();
@@ -828,7 +841,7 @@ implements ObjectSpecification {
         .filter(this::whenIsValueThenIsAlsoConstructorMixin)
         .filter(_SpecPredicates::isMixedInAction)
         .map(ObjectActionDefault.class::cast)
-        .map(_MixedInMemberFactory.mixedInAction(this, mixinType, mixinMethodName))
+        .map(_MixedInMemberFactory.mixedInAction(this, mixinSpec, mixinMethodName))
         .peek(facetProcessor::processMemberOrder);
     }
 
@@ -912,7 +925,7 @@ implements ObjectSpecification {
            return; // nothing to do (this spec has no mixed-in actions, regular actions have already been added)
         }
 
-        var regularActions = _Lists.newArrayList(objectActions); // defensive copy
+        var regularActions = body.snapshotActions(); // defensive copy
 
         // note: we are doing this before any member sorting
         _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularActions, mixedInActions);
@@ -935,7 +948,7 @@ implements ObjectSpecification {
            return; // nothing to do (this spec has no mixed-in associations, regular associations have already been added)
         }
 
-        var regularAssociations = _Lists.newArrayList(associations); // defensive copy
+        var regularAssociations = body.snapshotAssociations(); // defensive copy
 
         // note: we are doing this before any member sorting
         _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularAssociations, mixedInAssociations);
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationBody.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationBody.java
new file mode 100644
index 00000000000..a98e4c2d7ca
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationBody.java
@@ -0,0 +1,56 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.core.metamodel.specloader.specimpl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
+
+record ObjectSpecificationBody(
+        List<ObjectAction> objectActions,
+        List<ObjectAssociation> associations
+        ) {
+    
+    ObjectSpecificationBody() {
+        this(new ArrayList<>(), new ArrayList<>());
+    }
+    
+    Can<ObjectAction> snapshotActions() {
+        return Can.ofCollection(objectActions);
+    }
+
+    Can<ObjectAssociation> snapshotAssociations() {
+        return Can.ofCollection(associations);
+    }
+    
+    //synchronized by caller
+    void replaceActions(List<ObjectAction> orderedActions) {
+        objectActions.clear();
+        objectActions.addAll(orderedActions);
+    }
+    
+  //synchronized by caller
+    void replaceAssociations(List<ObjectAssociation> orderedAssociations) {
+        associations.clear();
+        associations.addAll(orderedAssociations);
+    }
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/OneToManyAssociationMixedIn.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/OneToManyAssociationMixedIn.java
index 6010bf065c9..9355df2f50a 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/OneToManyAssociationMixedIn.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/OneToManyAssociationMixedIn.java
@@ -55,11 +55,11 @@ implements MixedInMember {
     public static class forTesting {
         public static OneToManyAssociationMixedIn forMixinMain(
                 final ObjectSpecification mixeeSpec,
-                final Class<?> mixinType,
+                final ObjectSpecification mixinSpec,
                 final String mixinMethodName,
                 final FacetedMethod facetedMethod) {
             final ObjectActionDefault mixinAction = (ObjectActionDefault) ObjectActionDefault.forTesting.forMixinMain(facetedMethod);
-            return new OneToManyAssociationMixedIn(mixeeSpec, mixinAction, mixinType, mixinMethodName);
+            return new OneToManyAssociationMixedIn(mixeeSpec, mixinAction, mixinSpec, mixinMethodName);
         }
     }
 
@@ -70,7 +70,7 @@ implements MixedInMember {
      * {@link org.apache.causeway.applib.annotation.DomainObject} with a {@link DomainObject#nature() nature} of
      * {@link org.apache.causeway.applib.annotation.Nature#MIXIN MIXIN}.
      */
-    private final Class<?> mixinType;
+    private final ObjectSpecification mixinSpec;
 
     /**
      * The {@link ObjectActionDefault} for the action being mixed in (ie on the {@link #mixinType}).
@@ -106,7 +106,7 @@ implements MixedInMember {
     public OneToManyAssociationMixedIn(
             final ObjectSpecification mixeeSpec,
             final ObjectActionDefault mixinAction,
-            final Class<?> mixinType,
+            final ObjectSpecification mixinSpec,
             final String mixinMethodName) {
 
         super(identifierForMixedInCollection(mixeeSpec, mixinAction),
@@ -116,7 +116,7 @@ implements MixedInMember {
                 super.getFeatureIdentifier(),
                 mixinAction.getFacetedMethod());
 
-        this.mixinType = mixinType;
+        this.mixinSpec = mixinSpec;
         this.mixinAction = mixinAction;
         this.mixeeSpec = mixeeSpec;
 
@@ -140,7 +140,7 @@ implements MixedInMember {
 
     @Override
     protected InteractionHead headFor(final ManagedObject mixedInAdapter) {
-        var mixinAdapter = mixinAdapterFor(mixinType, mixedInAdapter);
+        var mixinAdapter = mixinAdapterFor(mixinSpec, mixedInAdapter);
         return InteractionHead.mixin(mixedInAdapter, mixinAdapter);
     }
 
@@ -171,7 +171,7 @@ implements MixedInMember {
 
     @Override
     public ObjectSpecification getMixinType() {
-        return getSpecificationLoader().loadSpecification(mixinType);
+        return mixinSpec;
     }
 
     @Override
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/OneToOneAssociationMixedIn.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/OneToOneAssociationMixedIn.java
index 3a82a7efa64..b1b30ca7c9e 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/OneToOneAssociationMixedIn.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/OneToOneAssociationMixedIn.java
@@ -52,11 +52,11 @@ implements MixedInMember {
     public static class forTesting {
         public static OneToOneAssociationMixedIn forMixinMain(
                 final ObjectSpecification mixeeSpec,
-                final Class<?> mixinType,
+                final ObjectSpecification mixinSpec,
                 final String mixinMethodName,
                 final FacetedMethod facetedMethod) {
             final ObjectActionDefault mixinAction = (ObjectActionDefault) ObjectActionDefault.forTesting.forMixinMain(facetedMethod);
-            return new OneToOneAssociationMixedIn(mixeeSpec, mixinAction, mixinType, mixinMethodName);
+            return new OneToOneAssociationMixedIn(mixeeSpec, mixinAction, mixinSpec, mixinMethodName);
         }
     }
 
@@ -65,7 +65,7 @@ implements MixedInMember {
     /**
      * The type of the mixin (providing the action), eg annotated with {@link org.apache.causeway.applib.annotation.Mixin}.
      */
-    private final Class<?> mixinType;
+    private final ObjectSpecification mixinSpec;
 
     /**
      * The {@link ObjectActionDefault} for the action being mixed in (ie on the {@link #mixinType}.
@@ -86,7 +86,7 @@ implements MixedInMember {
     public OneToOneAssociationMixedIn(
             final ObjectSpecification mixeeSpec,
             final ObjectActionDefault mixinAction,
-            final Class<?> mixinType,
+            final ObjectSpecification mixinSpec,
             final String mixinMethodName) {
         super(
                 identifierForMixedInProperty(mixeeSpec, mixinAction),
@@ -97,7 +97,7 @@ implements MixedInMember {
                 super.getFeatureIdentifier(),
                 mixinAction.getFacetedMethod());
 
-        this.mixinType = mixinType;
+        this.mixinSpec = mixinSpec;
         this.mixinAction = mixinAction;
         this.mixeeSpec = mixeeSpec;
 
@@ -122,7 +122,7 @@ implements MixedInMember {
     protected InteractionHead headFor(final ManagedObject mixeeAdapter) {
         return InteractionHead.mixin(
                 mixeeAdapter,
-                mixinAdapterFor(mixinType, mixeeAdapter));
+                mixinAdapterFor(mixinSpec, mixeeAdapter));
     }
 
     private DisabledFacet disabledFacet() {
@@ -154,7 +154,7 @@ implements MixedInMember {
 
     @Override
     public ObjectSpecification getMixinType() {
-        return getSpecificationLoader().loadSpecification(mixinType);
+        return mixinSpec;
     }
 
     @Override
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/_MixedInMemberFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/_MixedInMemberFactory.java
index 34f6d9c7829..d860d91112c 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/_MixedInMemberFactory.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/_MixedInMemberFactory.java
@@ -33,24 +33,24 @@ class _MixedInMemberFactory {
 
     Function<ObjectActionDefault, ObjectActionMixedIn> mixedInAction(
             final ObjectSpecification mixeeSpec,
-            final Class<?> mixinType,
+            final ObjectSpecification mixinSpec,
             final String mixinMethodName) {
 
         return mixinAction -> new ObjectActionMixedIn(
-                mixinType, mixinMethodName, mixinAction, mixeeSpec);
+                mixinSpec, mixinMethodName, mixinAction, mixeeSpec);
     }
 
     Function<ObjectActionDefault, ObjectAssociation> mixedInAssociation(
             final ObjectSpecification mixeeSpec,
-            final Class<?> mixinType,
+            final ObjectSpecification mixinSpec,
             final String mixinMethodName) {
 
         return mixinAction ->
             mixinAction.getReturnType().isSingular()
                 ? new OneToOneAssociationMixedIn(
-                        mixeeSpec, mixinAction, mixinType, mixinMethodName)
+                        mixeeSpec, mixinAction, mixinSpec, mixinMethodName)
                 : new OneToManyAssociationMixedIn(
-                        mixeeSpec, mixinAction, mixinType, mixinMethodName);
+                        mixeeSpec, mixinAction, mixinSpec, mixinMethodName);
     }
 
 }
diff --git a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/FacetFactoryTestAbstract.java b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/FacetFactoryTestAbstract.java
index 17bcccd1ae2..0914f05292a 100644
--- a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/FacetFactoryTestAbstract.java
+++ b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/facets/FacetFactoryTestAbstract.java
@@ -253,8 +253,9 @@ implements HasMetaModelContext {
                 methodRemover, facetedMethod, true);
 
         final ObjectSpecification mixeeSpec = getSpecificationLoader().loadSpecification(declaringClass);
+        final ObjectSpecification mixinSpec = getSpecificationLoader().loadSpecification(mixinClass);
         final ObjectActionMixedIn mixedInAct =
-                ObjectActionMixedIn.forTesting.forMixinMain(mixeeSpec, mixinClass, "act", facetedMethod);
+                ObjectActionMixedIn.forTesting.forMixinMain(mixeeSpec, mixinSpec, "act", facetedMethod);
 
         consumer.accept(processMethodContext, mixeeSpec, facetedMethod, mixedInAct);
     }
@@ -347,8 +348,9 @@ implements HasMetaModelContext {
                 methodRemover, facetedMethod, true);
 
         final ObjectSpecification mixeeSpec = getSpecificationLoader().loadSpecification(declaringClass);
+        final ObjectSpecification mixinSpec  = getSpecificationLoader().loadSpecification(mixinClass);
         final OneToOneAssociationMixedIn mixedInProp =
-                OneToOneAssociationMixedIn.forTesting.forMixinMain(mixeeSpec, mixinClass, "prop", facetedMethod);
+                OneToOneAssociationMixedIn.forTesting.forMixinMain(mixeeSpec, mixinSpec, "prop", facetedMethod);
 
         consumer.accept(processMethodContext, mixeeSpec, facetedMethod, mixedInProp);
     }
@@ -407,8 +409,9 @@ implements HasMetaModelContext {
                 methodRemover, facetedMethod, true);
 
         final ObjectSpecification mixeeSpec = getSpecificationLoader().loadSpecification(declaringClass);
+        final ObjectSpecification mixinSpec = getSpecificationLoader().loadSpecification(mixinClass);
         final OneToManyAssociationMixedIn mixedInColl =
-                OneToManyAssociationMixedIn.forTesting.forMixinMain(mixeeSpec, mixinClass, "coll", facetedMethod);
+                OneToManyAssociationMixedIn.forTesting.forMixinMain(mixeeSpec, mixinSpec, "coll", facetedMethod);
 
         consumer.accept(processMethodContext, mixeeSpec, facetedMethod, mixedInColl);
     }


(causeway) 02/02: CAUSEWAY-2297: lazy hierarchical introspection

Posted by ah...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch 2297-member.container
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit f43d8082bc875563ad57625206086dde2a48fb24
Author: andi-huber <ah...@apache.org>
AuthorDate: Sat Nov 16 22:37:03 2024 +0100

    CAUSEWAY-2297: lazy hierarchical introspection
---
 .../core/metamodel/spec/HierarchicalRecord.java    | 180 +++++++++++
 .../specloader/SpecificationLoaderDefault.java     |   6 +-
 .../specloader/specimpl/FacetedMethodsBuilder.java |  16 +-
 .../specimpl/ObjectActionContainerRecord.java      | 262 ++++++++++++++++
 ...nBody.java => ObjectMemberContainerRecord.java} |  29 +-
 .../specimpl/ObjectSpecificationAbstract.java      | 337 +++------------------
 .../specimpl/dflt/ObjectSpecificationDefault.java  |  82 ++---
 7 files changed, 533 insertions(+), 379 deletions(-)

diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/HierarchicalRecord.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/HierarchicalRecord.java
new file mode 100644
index 00000000000..da1bb292f79
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/HierarchicalRecord.java
@@ -0,0 +1,180 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.core.metamodel.spec;
+
+import java.util.List;
+
+import org.springframework.util.ClassUtils;
+
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.internal.base._Lazy;
+import org.apache.causeway.commons.internal.collections._Lists;
+import org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutorRegistry;
+import org.apache.causeway.core.metamodel.specloader.specimpl.ObjectSpecificationAbstract;
+
+public record HierarchicalRecord(
+        ObjectSpecification self,
+        _Lazy<ObjectSpecification> superclassLazy,
+        _Lazy<Can<ObjectSpecification>> interfacesLazy,
+        Subclasses directSubclasses,
+        // built lazily
+        _Lazy<Subclasses> transitiveSubclassesLazy
+        ) implements Hierarchical {
+    
+    /**
+     * @implNote thread-safe
+     */
+    public static class Subclasses {
+
+        // List performs better compared to a Set, when the number of elements is low
+        private Can<ObjectSpecification> classes = Can.empty();
+        private final Object $lock = new Object();
+
+        public void addSubclass(final ObjectSpecification subclass) {
+            synchronized($lock) {
+                classes = classes.addUnique(subclass);
+            }
+        }
+
+        public boolean hasSubclasses() {
+            synchronized($lock) {
+                return classes.isNotEmpty();
+            }
+        }
+
+        public Can<ObjectSpecification> snapshot() {
+            synchronized($lock) {
+                return classes;
+            }
+        }
+    }
+    
+    public HierarchicalRecord(ObjectSpecification self) {
+        this(self, 
+                _Lazy.threadSafe(()->populateSuperclass(self)), 
+                _Lazy.threadSafe(()->populateInterfaces(self)), 
+                new Subclasses(), 
+                _Lazy.threadSafe(()->populateTransitiveSubclasses(self)));
+    }
+
+    private static ObjectSpecification populateSuperclass(ObjectSpecification self) {
+        final Class<?> superclass = self.getCorrespondingClass().getSuperclass();
+        if (superclass == null) return null;
+        var superclassSpec = self.getSpecificationLoader()
+                .loadSpecification(superclass, IntrospectionState.NOT_INTROSPECTED);
+        if (superclassSpec != null) {
+            updateAsSubclassTo(self, superclassSpec);
+        }
+        return superclassSpec;
+    }
+    private static void updateAsSubclassTo(
+            final ObjectSpecification self, 
+            final List<ObjectSpecification> supertypeSpecs) {
+        for (final ObjectSpecification supertypeSpec : supertypeSpecs) {
+            updateAsSubclassTo(self, supertypeSpec);
+        }
+    }
+    private static void updateAsSubclassTo(
+            final ObjectSpecification self, 
+            final ObjectSpecification supertypeSpec) {
+        if (supertypeSpec instanceof ObjectSpecificationAbstract introspectableSpec) {
+            introspectableSpec.hierarchy.directSubclasses().addSubclass(self);    
+        }
+    }
+    
+    private static Can<ObjectSpecification> populateInterfaces(ObjectSpecification self) {
+        final Class<?>[] interfaceTypes = self.getCorrespondingClass().getInterfaces();
+        final List<ObjectSpecification> interfaceSpecs = _Lists.newArrayList();
+        
+        var classSubstitutorRegistry = self
+                .getServiceRegistry().lookupServiceElseFail(ClassSubstitutorRegistry.class);
+        
+        for (var interfaceType : interfaceTypes) {
+            var interfaceSubstitute = classSubstitutorRegistry.getSubstitution(interfaceType);
+            if (interfaceSubstitute.isReplace()) {
+                var interfaceSpec = self.getSpecificationLoader().loadSpecification(interfaceSubstitute.getReplacement());
+                interfaceSpecs.add(interfaceSpec);
+            }
+        }
+        updateAsSubclassTo(self, interfaceSpecs);
+        return Can.ofCollection(interfaceSpecs);
+    }
+    
+    private static Subclasses populateTransitiveSubclasses(ObjectSpecification self) {
+        final Subclasses appendTo = new Subclasses();
+        appendSubclasses(self, appendTo);
+        Subclasses transitiveSubclasses = appendTo;
+        return transitiveSubclasses;
+    }
+    
+    private static void appendSubclasses(
+            final ObjectSpecification objectSpecification,
+            final Subclasses appendTo) {
+        var directSubclasses = objectSpecification.subclasses(Depth.DIRECT);
+        for (ObjectSpecification subclass : directSubclasses) {
+            appendTo.addSubclass(subclass);
+            appendSubclasses(subclass, appendTo);
+        }
+    }
+    
+    public final Class<?> getCorrespondingClass() {
+        return self.getCorrespondingClass();
+    }
+    
+    @Override
+    public ObjectSpecification superclass() {
+        return superclassLazy.get();
+    }
+    
+    @Override
+    public boolean hasSubclasses() {
+        return directSubclasses.hasSubclasses();
+    }
+    
+    @Override
+    public Can<ObjectSpecification> subclasses(Depth depth) {
+        return depth == Depth.DIRECT
+                ? directSubclasses().snapshot()
+                : transitiveSubclassesLazy().get().snapshot();
+    }
+
+    @Override
+    public boolean isOfType(ObjectSpecification other) {
+        var thisClass = this.getCorrespondingClass();
+        var otherClass = other.getCorrespondingClass();
+
+        return thisClass == otherClass
+                || otherClass.isAssignableFrom(thisClass);
+    }
+
+    @Override
+    public boolean isOfTypeResolvePrimitive(ObjectSpecification other) {
+        var thisClass = ClassUtils.resolvePrimitiveIfNecessary(this.getCorrespondingClass());
+        var otherClass = ClassUtils.resolvePrimitiveIfNecessary(other.getCorrespondingClass());
+
+        return thisClass == otherClass
+                || otherClass.isAssignableFrom(thisClass);
+    }
+    
+    @Override
+    public Can<ObjectSpecification> interfaces() {
+        return interfacesLazy.get(); 
+    }
+    
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java
index 4b0958c72c9..86c4439b1c1 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java
@@ -576,16 +576,14 @@ implements
             final @NonNull Function<Class<?>, CausewayBeanMetaData> beanClassifier,
             final @NonNull IntrospectionState upTo) {
 
-        if(type==null) {
-            return null;
-        }
+        if(type==null) return null;
 
         var substitute = classSubstitutorRegistry.getSubstitution(type);
         if (substitute.isNeverIntrospect()) return null; // never inspect
 
         var substitutedType = substitute.apply(type);
 
-        var spec = cache.computeIfAbsent(substitutedType, _spec->
+        var spec = cache.computeIfAbsent(substitutedType, cls->
             logicalTypeResolver
                 .register(
                         createSpecification(beanClassifier.apply(substitutedType))));
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/FacetedMethodsBuilder.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/FacetedMethodsBuilder.java
index 1cff4348643..c4f7a7bc4c1 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/FacetedMethodsBuilder.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/FacetedMethodsBuilder.java
@@ -67,13 +67,15 @@ public class FacetedMethodsBuilder
 implements HasMetaModelContext {
 
     /* thread-safety ... make sure every methodsRemaining access is synchronized! */
-    private static final class ConcurrentMethodRemover implements MethodRemover {
-
-        private final Set<ResolvedMethod> methodsRemaining;
-
-        private ConcurrentMethodRemover(final Class<?> introspectedClass, final Stream<ResolvedMethod> methodStream) {
-            this.methodsRemaining = methodStream
-                    .collect(Collectors.toCollection(_Sets::newConcurrentHashSet));
+    private record ConcurrentMethodRemover(
+            Set<ResolvedMethod> methodsRemaining) 
+    implements MethodRemover {
+
+        ConcurrentMethodRemover(
+                final Class<?> introspectedClass, 
+                final Stream<ResolvedMethod> methodStream) {
+            this(methodStream
+                    .collect(Collectors.toCollection(_Sets::newConcurrentHashSet)));
         }
 
         @Override
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectActionContainerRecord.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectActionContainerRecord.java
new file mode 100644
index 00000000000..b3e91821715
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectActionContainerRecord.java
@@ -0,0 +1,262 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.core.metamodel.specloader.specimpl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.commons.collections.ImmutableEnumSet;
+import org.apache.causeway.commons.internal.base._NullSafe;
+import org.apache.causeway.commons.internal.base._Oneshot;
+import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.internal.collections._Multimaps;
+import org.apache.causeway.commons.internal.collections._Multimaps.ListMultimap;
+import org.apache.causeway.commons.internal.collections._Sets;
+import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry;
+import org.apache.causeway.core.metamodel.spec.ActionScope;
+import org.apache.causeway.core.metamodel.spec.IntrospectionState;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectActionContainer;
+import org.apache.causeway.core.metamodel.specloader.facetprocessor.FacetProcessor;
+
+public record ObjectActionContainerRecord(
+        ObjectSpecification self,
+        /**
+         * Get the specification for this specification's class's superclass.
+         */
+        Supplier<ObjectSpecification> superclassSupplier,
+        List<ObjectAction> regularActions,
+        List<ObjectAction> mixedinActions,
+        // partitions and caches objectActions by type; updated in sortCacheAndUpdateActions()
+        ListMultimap<ActionScope, ObjectAction> objectActionsByType,
+        _Oneshot mixedInActionAdder,
+        FacetProcessor facetProcessor) 
+implements ObjectActionContainer {
+    
+    public ObjectActionContainerRecord(
+            ObjectSpecification self,
+            Supplier<ObjectSpecification> superclassSupplier,
+            FacetProcessor facetProcessor) {
+        this(self, superclassSupplier, 
+                new ArrayList<>(), new ArrayList<>(), _Multimaps.newListMultimap(),
+                new _Oneshot(), facetProcessor);
+    }
+    
+    public boolean isTypeHierarchyRoot() {
+        return superclassSupplier().get()==null;
+    }
+    
+    public Stream<ObjectAction> stream(
+            final ImmutableEnumSet<ActionScope> actionScopes,
+            final MixedIn mixedIn) {
+        return actionScopes.stream()
+                .flatMap(actionScope->_NullSafe.stream(objectActionsByType.get(actionScope)))
+                .filter(mixedIn.toFilter());
+    }
+
+    @Override
+    public final Optional<ObjectAction> getAction(
+            final String id, final ImmutableEnumSet<ActionScope> scopes, final MixedIn mixedIn) {
+
+        var declaredAction = getDeclaredAction(id, mixedIn); // no inheritance nor type considered
+
+        if(declaredAction.isPresent()) {
+            // action found but if its not the right type, stop searching
+            if(!scopes.contains(declaredAction.get().getScope())) {
+                return Optional.empty();
+            }
+            return declaredAction;
+        }
+
+        return isTypeHierarchyRoot()
+                ? Optional.empty() // stop searching
+                : superclassSupplier().get().getAction(id, scopes, mixedIn);
+    }
+
+    @Override
+    public final Stream<ObjectAction> streamActions(
+            final ImmutableEnumSet<ActionScope> actionTypes,
+            final MixedIn mixedIn,
+            final Consumer<ObjectAction> onActionOverloaded) {
+
+        var actionStream = isTypeHierarchyRoot()
+                ? streamDeclaredActions(actionTypes, mixedIn) // stop going deeper
+                : Stream.concat(
+                        streamDeclaredActions(actionTypes, mixedIn),
+                        superclassSupplier().get().streamActions(actionTypes, mixedIn));
+
+        var actionSignatures = _Sets.<String>newHashSet();
+        var actionIds = _Sets.<String>newHashSet();
+
+        return actionStream
+
+            // as of contributing super-classes same actions might appear more than once (overriding)
+            .filter(action->{
+                if(action.isMixedIn()) {
+                    return true; // do not filter mixedIn actions based on signature
+                }
+                var isUnique = actionSignatures
+                        .add(action.getFeatureIdentifier().getMemberNameAndParameterClassNamesIdentityString());
+                return isUnique;
+            })
+    
+            // ensure we don't emit duplicates
+            .filter(action->{
+                var isUnique = actionIds.add(action.getId());
+                if(!isUnique) {
+                    onActionOverloaded.accept(action);
+                }
+                return isUnique;
+            });
+    }
+
+    @Override
+    public Stream<ObjectAction> streamDeclaredActions(ImmutableEnumSet<ActionScope> actionScopes, MixedIn mixedIn) {
+        self.introspectUpTo(IntrospectionState.FULLY_INTROSPECTED);
+        mixedInActionAdder.trigger(this::createMixedInActionsAndResort);
+        return stream(actionScopes, mixedIn);
+    }
+    /**
+     * one-shot: must be no-op, if already created
+     */
+    private void createMixedInActionsAndResort() {
+        var include = self.isEntityOrViewModelOrAbstract()
+                || self.getBeanSort().isManagedBeanContributing()
+                // in support of composite value-type constructor mixins
+                || self.getBeanSort().isValue();
+        if(!include) return;
+        addMixedinActions(createMixedInActions());
+    }
+    /**
+     * Creates all mixed in actions for this spec.
+     */
+    private Stream<ObjectActionMixedIn> createMixedInActions() {
+        return self.getServiceRegistry()
+                .lookupServiceElseFail(CausewayBeanTypeRegistry.class)
+                .streamMixinTypes()
+            .flatMap(this::createMixedInAction);
+    }
+    @Deprecated //REFACTOR: remove circular dependency specloader<->objecspec
+    private Stream<ObjectActionMixedIn> createMixedInAction(final Class<?> mixinType) {
+        var mixinSpec = self.getSpecificationLoader().loadSpecification(mixinType,
+                IntrospectionState.FULLY_INTROSPECTED);
+        return createMixedInAction(mixinSpec);
+    }
+    private Stream<ObjectActionMixedIn> createMixedInAction(final ObjectSpecification mixinSpec) {
+        if (mixinSpec == null
+                || mixinSpec == self) {
+            return Stream.empty();
+        }
+        var mixinFacet = mixinSpec.mixinFacet().orElse(null);
+        if(mixinFacet == null) {
+            // this shouldn't happen; to be covered by meta-model validation later
+            return Stream.empty();
+        }
+        if(!mixinFacet.isMixinFor(self.getCorrespondingClass())) {
+            return Stream.empty();
+        }
+        // don't mixin Object_ mixins to domain services
+        if(self.getBeanSort().isManagedBeanContributing()
+                && mixinFacet.isMixinFor(java.lang.Object.class)) {
+            return Stream.empty();
+        }
+
+        var mixinMethodName = mixinFacet.getMainMethodName();
+
+        return mixinSpec.streamActions(ActionScope.ANY, MixedIn.EXCLUDED)
+        // value types only support constructor mixins
+        .filter(this::whenIsValueThenIsAlsoConstructorMixin)
+        .filter(_SpecPredicates::isMixedInAction)
+        .map(ObjectActionDefault.class::cast)
+        .map(_MixedInMemberFactory.mixedInAction(self, mixinSpec, mixinMethodName))
+        .peek(facetProcessor::processMemberOrder);
+    }
+    /**
+     * Whether the mixin's main method returns an instance of type equal to the mixee's type.
+     * <p>
+     * Introduced to support constructor mixins for value-types and
+     * also to support associated <i>Actions</i> for <i>Action Parameters</i>.
+     */
+    private boolean whenIsValueThenIsAlsoConstructorMixin(final ObjectAction act) {
+        return self.getBeanSort().isValue()
+                ? Objects.equals(self, act.getReturnType())
+                : true;
+    }
+    
+    @Override
+    public Optional<ObjectAction> getDeclaredAction(String id, ImmutableEnumSet<ActionScope> actionScopes,
+            MixedIn mixedIn) {
+        self.introspectUpTo(IntrospectionState.FULLY_INTROSPECTED);
+
+        return _Strings.isEmpty(id)
+            ? Optional.empty()
+            : streamDeclaredActions(actionScopes, mixedIn)
+                .filter(action->
+                    id.equals(action.getFeatureIdentifier().getMemberNameAndParameterClassNamesIdentityString())
+                            || id.equals(action.getFeatureIdentifier().getMemberLogicalName())
+                )
+                .findFirst();
+    }
+
+    @Override
+    public Stream<ObjectAction> streamRuntimeActions(MixedIn mixedIn) {
+        var actionScopes = ActionScope.forEnvironment(self.getSystemEnvironment());
+        return streamActions(actionScopes, mixedIn);
+    }
+
+    @Override
+    public Stream<ObjectAction> streamActionsForColumnRendering(Identifier memberIdentifier) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public void rebuild(List<ObjectAction> orderedActions) {
+        for (var actionType : ActionScope.values()) {
+            var objectActionForType = objectActionsByType.getOrElseNew(actionType);
+            objectActionForType.clear();
+            orderedActions.stream()
+            .filter(ObjectAction.Predicates.ofActionType(actionType))
+            .forEach(objectActionForType::add);
+        }
+    }
+
+    public void addRegularActions(Stream<ObjectAction> actions) {
+        regularActions.clear();
+        actions.forEach(regularActions::add);
+    }
+    
+    public void addMixedinActions(Stream<ObjectActionMixedIn> actions) {
+        mixedinActions.clear();
+        actions.forEach(mixedinActions::add);
+        
+        if(mixedinActions.isEmpty()) return;
+
+        // note: we are doing this before any member sorting
+        _MemberIdClashReporting.flagAnyMemberIdClashes(self, regularActions, mixedinActions);
+    }
+    
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationBody.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberContainerRecord.java
similarity index 69%
rename from core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationBody.java
rename to core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberContainerRecord.java
index a98e4c2d7ca..e2aad6d8cf2 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationBody.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectMemberContainerRecord.java
@@ -20,37 +20,36 @@ package org.apache.causeway.core.metamodel.specloader.specimpl;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Supplier;
 
 import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.causeway.core.metamodel.specloader.facetprocessor.FacetProcessor;
 
-record ObjectSpecificationBody(
-        List<ObjectAction> objectActions,
+public record ObjectMemberContainerRecord(
+        ObjectSpecification self,
+        ObjectActionContainerRecord actions,
         List<ObjectAssociation> associations
         ) {
     
-    ObjectSpecificationBody() {
-        this(new ArrayList<>(), new ArrayList<>());
-    }
-    
-    Can<ObjectAction> snapshotActions() {
-        return Can.ofCollection(objectActions);
+    ObjectMemberContainerRecord(
+            ObjectSpecification self,
+            Supplier<ObjectSpecification> superclassSupplier,
+            FacetProcessor facetProcessor) {
+        this(self, 
+                new ObjectActionContainerRecord(self, superclassSupplier, facetProcessor), 
+                new ArrayList<>());
     }
 
     Can<ObjectAssociation> snapshotAssociations() {
         return Can.ofCollection(associations);
     }
     
-    //synchronized by caller
-    void replaceActions(List<ObjectAction> orderedActions) {
-        objectActions.clear();
-        objectActions.addAll(orderedActions);
-    }
-    
   //synchronized by caller
     void replaceAssociations(List<ObjectAssociation> orderedAssociations) {
         associations.clear();
         associations.addAll(orderedAssociations);
     }
+
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
index 53472c6bb04..08be3d2afe6 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
@@ -18,8 +18,6 @@
  */
 package org.apache.causeway.core.metamodel.specloader.specimpl;
 
-import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.Stack;
@@ -27,8 +25,6 @@ import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import org.springframework.util.ClassUtils;
-
 import org.apache.causeway.applib.Identifier;
 import org.apache.causeway.applib.annotation.Domain;
 import org.apache.causeway.applib.annotation.Introspection.IntrospectionPolicy;
@@ -42,9 +38,6 @@ import org.apache.causeway.commons.internal.base._Lazy;
 import org.apache.causeway.commons.internal.base._NullSafe;
 import org.apache.causeway.commons.internal.base._Oneshot;
 import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.collections._Lists;
-import org.apache.causeway.commons.internal.collections._Multimaps;
-import org.apache.causeway.commons.internal.collections._Multimaps.ListMultimap;
 import org.apache.causeway.commons.internal.collections._Sets;
 import org.apache.causeway.commons.internal.collections._Streams;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
@@ -82,6 +75,7 @@ import org.apache.causeway.core.metamodel.interactions.ObjectValidityContext;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.spec.ActionScope;
+import org.apache.causeway.core.metamodel.spec.HierarchicalRecord;
 import org.apache.causeway.core.metamodel.spec.IntrospectionState;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
@@ -106,70 +100,23 @@ public abstract class ObjectSpecificationAbstract
 extends ObjectMemberContainer
 implements ObjectSpecification {
 
-    /**
-     * @implNote thread-safe
-     */
-    private static class Subclasses {
-
-        // List performs better compared to a Set, when the number of elements is low
-        private Can<ObjectSpecification> classes = Can.empty();
-        private final Object $lock = new Object();
-
-        public void addSubclass(final ObjectSpecification subclass) {
-            synchronized($lock) {
-                classes = classes.addUnique(subclass);
-            }
-        }
-
-        public boolean hasSubclasses() {
-            synchronized($lock) {
-                return classes.isNotEmpty();
-            }
-        }
-
-        public Can<ObjectSpecification> snapshot() {
-            synchronized($lock) {
-                return classes;
-            }
-        }
-    }
-
     // -- FIELDS
 
     private final PostProcessor postProcessor;
     private final FacetProcessor facetProcessor;
-    private final ObjectSpecificationBody body = new ObjectSpecificationBody();
+    
+    public final HierarchicalRecord hierarchy;
+    protected final ObjectMemberContainerRecord members;
 
     @Getter private final BeanSort beanSort;
 
-    // -- ASSOCIATIONS
-
-    // defensive immutable lazy copy of associations
-    private final _Lazy<Can<ObjectAssociation>> unmodifiableAssociations = _Lazy.threadSafe(body::snapshotAssociations);
-
-    // -- ACTIONS
-    
     /** not API, used for validation */
     @Getter private final Set<ResolvedMethod> potentialOrphans = _Sets.newHashSet();
+    
+    // -- MEMBERS
 
-    // defensive immutable lazy copy of objectActions
-    private final _Lazy<Can<ObjectAction>> unmodifiableActions = _Lazy.threadSafe(body::snapshotActions);
-
-    // partitions and caches objectActions by type; updated in sortCacheAndUpdateActions()
-    private final ListMultimap<ActionScope, ObjectAction> objectActionsByType =
-            _Multimaps.newConcurrentListMultimap();
-
-    // -- INTERFACES
-
-    private final List<ObjectSpecification> interfaces = _Lists.newArrayList();
-
-    // defensive immutable lazy copy of interfaces
-    private final _Lazy<Can<ObjectSpecification>> unmodifiableInterfaces =
-            _Lazy.threadSafe(()->Can.ofCollection(interfaces));
-
-    private final Subclasses directSubclasses = new Subclasses();
-    // built lazily
-    private Subclasses transitiveSubclasses;
+    // defensive immutable lazy copy of associations
+    private final _Lazy<Can<ObjectAssociation>> unmodifiableAssociations;
 
     private final Class<?> correspondingClass;
     private final String fullName;
@@ -177,8 +124,6 @@ implements ObjectSpecification {
 
     private final LogicalType logicalType;
 
-    private ObjectSpecification superclassSpec;
-
     private ValueFacet valueFacet;
     private EntityFacet entityFacet;
     private ViewModelFacet viewmodelFacet;
@@ -210,6 +155,10 @@ implements ObjectSpecification {
 
         this.facetProcessor = facetProcessor;
         this.postProcessor = postProcessor;
+        
+        this.hierarchy = new HierarchicalRecord(this);
+        this.members = new ObjectMemberContainerRecord(this, hierarchy::superclass, facetProcessor);
+        this.unmodifiableAssociations = _Lazy.threadSafe(members::snapshotAssociations);
     }
 
     // -- Stuff immediately derivable from class
@@ -336,72 +285,14 @@ implements ObjectSpecification {
     protected abstract void introspectTypeHierarchy();
     protected abstract void introspectMembers();
 
-    protected void loadSpecOfSuperclass(final Class<?> superclass) {
-        if (superclass == null) {
-            return;
-        }
-        superclassSpec = getSpecificationLoader().loadSpecification(superclass);
-        if (superclassSpec != null) {
-            if (log.isDebugEnabled()) {
-                log.debug("  Superclass {}", superclass.getName());
-            }
-            updateAsSubclassTo(superclassSpec);
-        }
-    }
-
-    protected void updateInterfaces(final List<ObjectSpecification> interfaces) {
-        synchronized(unmodifiableInterfaces) {
-            this.interfaces.clear();
-            this.interfaces.addAll(interfaces);
-            unmodifiableInterfaces.clear();
-        }
-    }
-
-    private void updateAsSubclassTo(final ObjectSpecification supertypeSpec) {
-        if (!(supertypeSpec instanceof ObjectSpecificationAbstract)) {
-            return;
-        }
-        // downcast required because addSubclass is (deliberately) not public
-        // API
-        var introspectableSpec = (ObjectSpecificationAbstract) supertypeSpec;
-        introspectableSpec.updateSubclasses(this);
-    }
-
-    protected void updateAsSubclassTo(final List<ObjectSpecification> supertypeSpecs) {
-        for (final ObjectSpecification supertypeSpec : supertypeSpecs) {
-            updateAsSubclassTo(supertypeSpec);
-        }
-    }
-
-    private void updateSubclasses(final ObjectSpecification subclass) {
-        this.directSubclasses.addSubclass(subclass);
-    }
-
     protected final void replaceAssociations(final Stream<ObjectAssociation> associations) {
         var orderedAssociations = _MemberSortingUtils.sortAssociationsIntoList(associations);
         synchronized (unmodifiableAssociations) {
-            body.replaceAssociations(orderedAssociations);
+            members.replaceAssociations(orderedAssociations);
             unmodifiableAssociations.clear(); // invalidate
         }
     }
 
-    protected final void replaceActions(final Stream<ObjectAction> objectActions) {
-        var orderedActions = _MemberSortingUtils.sortActionsIntoList(objectActions);
-        synchronized (unmodifiableActions){
-            body.replaceActions(orderedActions);
-            unmodifiableActions.clear(); // invalidate
-
-            // rebuild objectActionsByType multi-map
-            for (var actionType : ActionScope.values()) {
-                var objectActionForType = objectActionsByType.getOrElseNew(actionType);
-                objectActionForType.clear();
-                orderedActions.stream()
-                .filter(ObjectAction.Predicates.ofActionType(actionType))
-                .forEach(objectActionForType::add);
-            }
-        }
-    }
-
     public void invalidateCachedFacets() {
         valueFacet = getFacet(ValueFacet.class);
         titleFacet = lookupNonFallbackFacet(TitleFacet.class).orElse(null);
@@ -525,23 +416,13 @@ implements ObjectSpecification {
     // -- HIERARCHICAL
 
     @Override
-    public boolean isOfType(final ObjectSpecification other) {
-
-        var thisClass = this.getCorrespondingClass();
-        var otherClass = other.getCorrespondingClass();
-
-        return thisClass == otherClass
-                || otherClass.isAssignableFrom(thisClass);
+    public final boolean isOfType(final ObjectSpecification other) {
+        return hierarchy.isOfType(other);
     }
 
     @Override
-    public boolean isOfTypeResolvePrimitive(final ObjectSpecification other) {
-
-        var thisClass = ClassUtils.resolvePrimitiveIfNecessary(this.getCorrespondingClass());
-        var otherClass = ClassUtils.resolvePrimitiveIfNecessary(other.getCorrespondingClass());
-
-        return thisClass == otherClass
-                || otherClass.isAssignableFrom(thisClass);
+    public final boolean isOfTypeResolvePrimitive(final ObjectSpecification other) {
+        return hierarchy.isOfTypeResolvePrimitive(other);
     }
 
     // -- NAME, DESCRIPTION, PERSISTABILITY
@@ -587,31 +468,26 @@ implements ObjectSpecification {
 
     @Override
     public <Q extends Facet> Q getFacet(final Class<Q> facetType) {
+        // lookup facet holder's facet
+        var facets1 = _NullSafe.streamNullable(super.getFacet(facetType));
 
-        synchronized(unmodifiableInterfaces) {
-
-            // lookup facet holder's facet
-            var facets1 = _NullSafe.streamNullable(super.getFacet(facetType));
-
-            // lookup all interfaces
-            var facets2 = _NullSafe.stream(interfaces())
-                    .filter(_NullSafe::isPresent) // just in case
-                    .map(interfaceSpec->interfaceSpec.getFacet(facetType));
-
-            // search up the inheritance hierarchy
-            var facets3 = _NullSafe.streamNullable(superclass())
-                    .map(superSpec->superSpec.getFacet(facetType));
+        // lookup all interfaces
+        var facets2 = _NullSafe.stream(interfaces())
+                .filter(_NullSafe::isPresent) // just in case
+                .map(interfaceSpec->interfaceSpec.getFacet(facetType));
 
-            var facetsCombined = _Streams.concat(facets1, facets2, facets3);
+        // search up the inheritance hierarchy
+        var facets3 = _NullSafe.streamNullable(superclass())
+                .map(superSpec->superSpec.getFacet(facetType));
 
-            var notANoopFacetFilter = new NotANoopFacetFilter<Q>();
+        var facetsCombined = _Streams.concat(facets1, facets2, facets3);
 
-            return facetsCombined
-                    .filter(notANoopFacetFilter)
-                    .findFirst()
-                    .orElse(notANoopFacetFilter.noopFacet);
+        var notANoopFacetFilter = new NotANoopFacetFilter<Q>();
 
-        }
+        return facetsCombined
+                .filter(notANoopFacetFilter)
+                .findFirst()
+                .orElse(notANoopFacetFilter.noopFacet);
     }
 
     @Domain.Exclude
@@ -646,51 +522,23 @@ implements ObjectSpecification {
     // -- SUPERCLASS, INTERFACES, SUBCLASSES, IS-ABSTRACT
 
     @Override
-    public ObjectSpecification superclass() {
-        return superclassSpec;
+    public final ObjectSpecification superclass() {
+        return hierarchy.superclass();
     }
 
     @Override
-    public Can<ObjectSpecification> interfaces() {
-        return unmodifiableInterfaces.get();
+    public final Can<ObjectSpecification> interfaces() {
+        return hierarchy.interfaces();
     }
 
     @Override
-    public Can<ObjectSpecification> subclasses(final Depth depth) {
-        if (depth == Depth.DIRECT) {
-            return directSubclasses.snapshot();
-        }
-
-        // depth == Depth.TRANSITIVE)
-        if (transitiveSubclasses == null) {
-            transitiveSubclasses = transitiveSubclasses();
-        }
-
-        return transitiveSubclasses.snapshot();
-    }
-
-    private synchronized Subclasses transitiveSubclasses() {
-        final Subclasses appendTo = new Subclasses();
-        appendSubclasses(this, appendTo);
-        transitiveSubclasses = appendTo;
-        return transitiveSubclasses;
-    }
-
-    private void appendSubclasses(
-            final ObjectSpecification objectSpecification,
-            final Subclasses appendTo) {
-
-        var directSubclasses = objectSpecification.subclasses(Depth.DIRECT);
-        for (ObjectSpecification subclass : directSubclasses) {
-            appendTo.addSubclass(subclass);
-            appendSubclasses(subclass, appendTo);
-        }
-
+    public final Can<ObjectSpecification> subclasses(final Depth depth) {
+        return hierarchy.subclasses(depth);
     }
 
     @Override
-    public boolean hasSubclasses() {
-        return directSubclasses.hasSubclasses();
+    public final boolean hasSubclasses() {
+        return hierarchy.hasSubclasses();
     }
 
     // -- ASSOCIATIONS
@@ -740,22 +588,15 @@ implements ObjectSpecification {
     }
 
     @Override
-    public Stream<ObjectAction> streamRuntimeActions(final MixedIn mixedIn) {
-        var actionScopes = ActionScope.forEnvironment(getMetaModelContext().getSystemEnvironment());
-        return streamActions(actionScopes, mixedIn);
+    public final Stream<ObjectAction> streamRuntimeActions(final MixedIn mixedIn) {
+        return members.actions().streamRuntimeActions(mixedIn);
     }
 
     @Override
-    public Stream<ObjectAction> streamDeclaredActions(
+    public final Stream<ObjectAction> streamDeclaredActions(
             final ImmutableEnumSet<ActionScope> actionScopes,
             final MixedIn mixedIn) {
-        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED);
-
-        mixedInActionAdder.trigger(this::createMixedInActionsAndResort);
-
-        return actionScopes.stream()
-                .flatMap(actionScope->stream(objectActionsByType.get(actionScope)))
-                .filter(mixedIn.toFilter());
+        return members.actions().streamDeclaredActions(actionScopes, mixedIn);
     }
 
     // -- mixin associations (properties and collections)
@@ -798,65 +639,7 @@ implements ObjectSpecification {
         .map(_MixedInMemberFactory.mixedInAssociation(this, mixinSpec, mixinMethodName))
         .peek(facetProcessor::processMemberOrder);
     }
-
-    // -- mixin actions
-    /**
-     * Creates all mixed in actions for this spec.
-     */
-    private Stream<ObjectActionMixedIn> createMixedInActions() {
-        return getCausewayBeanTypeRegistry().streamMixinTypes()
-            .flatMap(this::createMixedInAction);
-    }
-
-    @Deprecated //REFACTOR: remove circular dependency specloader<->objecspec
-    private Stream<ObjectActionMixedIn> createMixedInAction(final Class<?> mixinType) {
-        var mixinSpec = getSpecificationLoader().loadSpecification(mixinType,
-                IntrospectionState.FULLY_INTROSPECTED);
-        return createMixedInAction(mixinSpec);
-    }
-    
-    private Stream<ObjectActionMixedIn> createMixedInAction(final ObjectSpecification mixinSpec) {
-        if (mixinSpec == null
-                || mixinSpec == this) {
-            return Stream.empty();
-        }
-        var mixinFacet = mixinSpec.mixinFacet().orElse(null);
-        if(mixinFacet == null) {
-            // this shouldn't happen; to be covered by meta-model validation later
-            return Stream.empty();
-        }
-        if(!mixinFacet.isMixinFor(getCorrespondingClass())) {
-            return Stream.empty();
-        }
-        // don't mixin Object_ mixins to domain services
-        if(getBeanSort().isManagedBeanContributing()
-                && mixinFacet.isMixinFor(java.lang.Object.class)) {
-            return Stream.empty();
-        }
-
-        var mixinMethodName = mixinFacet.getMainMethodName();
-
-        return mixinSpec.streamActions(ActionScope.ANY, MixedIn.EXCLUDED)
-        // value types only support constructor mixins
-        .filter(this::whenIsValueThenIsAlsoConstructorMixin)
-        .filter(_SpecPredicates::isMixedInAction)
-        .map(ObjectActionDefault.class::cast)
-        .map(_MixedInMemberFactory.mixedInAction(this, mixinSpec, mixinMethodName))
-        .peek(facetProcessor::processMemberOrder);
-    }
-
-    /**
-     * Whether the mixin's main method returns an instance of type equal to the mixee's type.
-     * <p>
-     * Introduced to support constructor mixins for value-types and
-     * also to support associated <i>Actions</i> for <i>Action Parameters</i>.
-     */
-    private boolean whenIsValueThenIsAlsoConstructorMixin(final ObjectAction act) {
-        return getBeanSort().isValue()
-                ? Objects.equals(this, act.getReturnType())
-                : true;
-    }
-
+  
     // -- VALIDITY
 
     @Override
@@ -905,36 +688,8 @@ implements ObjectSpecification {
 
     // -- MIXIN ADDER ONESHOTs
 
-    private final _Oneshot mixedInActionAdder = new _Oneshot();
     private final _Oneshot mixedInAssociationAdder = new _Oneshot();
 
-    /**
-     * one-shot: must be no-op, if already created
-     */
-    private void createMixedInActionsAndResort() {
-        var include = isEntityOrViewModelOrAbstract()
-                || getBeanSort().isManagedBeanContributing()
-                // in support of composite value-type constructor mixins
-                || getBeanSort().isValue();
-        if(!include) {
-            return;
-        }
-        var mixedInActions = createMixedInActions()
-                .collect(Collectors.toList());
-        if(mixedInActions.isEmpty()) {
-           return; // nothing to do (this spec has no mixed-in actions, regular actions have already been added)
-        }
-
-        var regularActions = body.snapshotActions(); // defensive copy
-
-        // note: we are doing this before any member sorting
-        _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularActions, mixedInActions);
-
-        replaceActions(Stream.concat(
-                regularActions.stream(),
-                mixedInActions.stream()));
-    }
-
     /**
      * one-shot: must be no-op, if already created
      */
@@ -948,7 +703,7 @@ implements ObjectSpecification {
            return; // nothing to do (this spec has no mixed-in associations, regular associations have already been added)
         }
 
-        var regularAssociations = body.snapshotAssociations(); // defensive copy
+        var regularAssociations = members.snapshotAssociations(); // defensive copy
 
         // note: we are doing this before any member sorting
         _MemberIdClashReporting.flagAnyMemberIdClashes(this, regularAssociations, mixedInAssociations);
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
index a3ba504ac6f..108b59ffad3 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
@@ -18,7 +18,6 @@
  */
 package org.apache.causeway.core.metamodel.specloader.specimpl.dflt;
 
-import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.BiConsumer;
@@ -35,7 +34,6 @@ import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.base._Lazy;
 import org.apache.causeway.commons.internal.base._NullSafe;
 import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.collections._Lists;
 import org.apache.causeway.commons.internal.collections._Maps;
 import org.apache.causeway.commons.internal.reflection._ClassCache;
 import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
@@ -69,7 +67,6 @@ import org.apache.causeway.core.metamodel.specloader.specimpl.ObjectSpecificatio
 import org.apache.causeway.core.metamodel.specloader.specimpl.OneToManyAssociationDefault;
 import org.apache.causeway.core.metamodel.specloader.specimpl.OneToOneAssociationDefault;
 
-import lombok.Getter;
 import lombok.extern.log4j.Log4j2;
 
 @Log4j2
@@ -82,11 +79,13 @@ implements FacetHolder {
      */
     private Map<ResolvedMethod, ObjectMember> membersByMethod = null;
 
-    private final FacetedMethodsBuilder facetedMethodsBuilder;
-    private final ClassSubstitutorRegistry classSubstitutorRegistry;
+    private final _Lazy<FacetedMethodsBuilder> facetedMethodsBuilderLazy;
 
-    @Getter(onMethod_ = {@Override})
-    private final IntrospectionPolicy introspectionPolicy;
+    private final _Lazy<IntrospectionPolicy> introspectionPolicyLazy;
+    @Override
+    public IntrospectionPolicy getIntrospectionPolicy() {
+        return introspectionPolicyLazy.get();
+    }
 
     public ObjectSpecificationDefault(
             final CausewayBeanMetaData typeMeta,
@@ -104,33 +103,28 @@ implements FacetHolder {
             case NONE, CAUSEWAY, PERSISTENCE -> true;
             case UNSPECIFIED, SPRING  -> false;
         }; 
-        this.classSubstitutorRegistry = classSubstitutorRegistry;
 
         // must install EncapsulationFacet (if any) and MemberAnnotationPolicyFacet (if any)
         facetProcessor.processObjectType(typeMeta.getCorrespondingClass(), this);
 
         // naturally supports attribute inheritance from the type's hierarchy
-        final IntrospectionPolicy introspectionPolicy =
-                this.lookupFacet(IntrospectionPolicyFacet.class)
+        this.introspectionPolicyLazy = _Lazy.threadSafe(()->
+            this.lookupFacet(IntrospectionPolicyFacet.class)
                 .map(introspectionPolicyFacet->
                         introspectionPolicyFacet
                         .getIntrospectionPolicy(mmc.getConfiguration()))
-                .orElseGet(()->mmc.getConfiguration().getCore().getMetaModel().getIntrospector().getPolicy());
+                .orElseGet(()->mmc.getConfiguration().getCore().getMetaModel().getIntrospector().getPolicy())
+        );
 
-        this.introspectionPolicy = introspectionPolicy;
-
-        this.facetedMethodsBuilder =
-                new FacetedMethodsBuilder(this, facetProcessor, classSubstitutorRegistry);
+        this.facetedMethodsBuilderLazy = _Lazy.threadSafe(()->
+                new FacetedMethodsBuilder(this, facetProcessor, classSubstitutorRegistry));
     }
 
     @Override
     protected void introspectTypeHierarchy() {
-
-        facetedMethodsBuilder.introspectClass();
-
+        facetedMethodsBuilderLazy.get().introspectClass();
         // name
         addNamedFacetIfRequired();
-
         // go no further if a value
         if(this.isValue()) {
             if (log.isDebugEnabled()) {
@@ -138,39 +132,12 @@ implements FacetHolder {
             }
             return;
         }
-
-        // superclass
-        final Class<?> superclass = getCorrespondingClass().getSuperclass();
-        loadSpecOfSuperclass(superclass);
-
-        // walk superinterfaces
-
-        //
-        // REVIEW: the processing here isn't quite the same as with
-        // superclasses, in that with superclasses the superclass adds this type as its
-        // subclass, whereas here this type defines itself as the subtype.
-        //
-        // it'd be nice to push the responsibility for adding subclasses to
-        // the interface type... needs some tests around it, though, before
-        // making that refactoring.
-        //
-        final Class<?>[] interfaceTypes = getCorrespondingClass().getInterfaces();
-        final List<ObjectSpecification> interfaceSpecList = _Lists.newArrayList();
-        for (var interfaceType : interfaceTypes) {
-            var interfaceSubstitute = classSubstitutorRegistry.getSubstitution(interfaceType);
-            if (interfaceSubstitute.isReplace()) {
-                var interfaceSpec = getSpecificationLoader().loadSpecification(interfaceSubstitute.getReplacement());
-                interfaceSpecList.add(interfaceSpec);
-            }
-        }
-
-        updateAsSubclassTo(interfaceSpecList);
-        updateInterfaces(interfaceSpecList);
+        hierarchy.superclassLazy().get();
+        hierarchy.interfacesLazy().get();
     }
 
     @Override
     protected void introspectMembers() {
-
         // yet this logic does not skip UNKNONW
         if(this.getBeanSort().isCollection()
                 || this.getBeanSort().isVetoed()
@@ -183,7 +150,8 @@ implements FacetHolder {
 
         // create associations and actions
         replaceAssociations(createAssociations());
-        replaceActions(createActions());
+        
+        members.actions().addRegularActions(createActions());
 
         postProcess();
     }
@@ -198,7 +166,7 @@ implements FacetHolder {
 
     // -- create associations and actions
     private Stream<ObjectAssociation> createAssociations() {
-        return facetedMethodsBuilder.getAssociationFacetedMethods()
+        return facetedMethodsBuilderLazy.get().getAssociationFacetedMethods()
                 .stream()
                 .map(this::createAssociation)
                 .filter(_NullSafe::isPresent);
@@ -215,7 +183,7 @@ implements FacetHolder {
     }
 
     private Stream<ObjectAction> createActions() {
-        return facetedMethodsBuilder.getActionFacetedMethods()
+        return facetedMethodsBuilderLazy.get().getActionFacetedMethods()
                 .stream()
                 .map(this::createAction)
                 .filter(_NullSafe::isPresent);
@@ -246,17 +214,7 @@ implements FacetHolder {
             final @Nullable String id,
             final ImmutableEnumSet<ActionScope> actionScopes,
             final MixedIn mixedIn) {
-
-        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED);
-
-        return _Strings.isEmpty(id)
-            ? Optional.empty()
-            : streamDeclaredActions(actionScopes, mixedIn)
-                .filter(action->
-                    id.equals(action.getFeatureIdentifier().getMemberNameAndParameterClassNamesIdentityString())
-                            || id.equals(action.getFeatureIdentifier().getMemberLogicalName())
-                )
-                .findFirst();
+        return members.actions().getDeclaredAction(id, actionScopes, mixedIn);
     }
 
     @Override