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

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


The following commit(s) were added to refs/heads/v4 by this push:
     new fc255d426c8 CAUSEWAY-3892: improved logging of re-validation causes
fc255d426c8 is described below

commit fc255d426c8915161f6150242f646eb8370a6d28
Author: a.huber <[email protected]>
AuthorDate: Mon Aug 25 09:28:59 2025 +0200

    CAUSEWAY-3892: improved logging of re-validation causes
---
 .../metamodel/spec/impl/FacetedMethodsBuilder.java |   8 +-
 .../spec/impl/ObjectSpecificationDefault.java      | 155 ++++++++++++---------
 .../spec/impl/ObjectSpecificationMutable.java      |  36 ++---
 .../spec/impl/SpecificationLoaderDefault.java      |  84 +++++------
 .../spec/impl/SpecificationLoaderInternal.java     |  47 +++++--
 .../metamodel/specloader/SpecificationLoader.java  |   9 +-
 .../impl/IntrospectionState_comparable_Test.java   |   2 +-
 7 files changed, 181 insertions(+), 160 deletions(-)

diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java
index 6b59700e33e..c4be6bfda46 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/FacetedMethodsBuilder.java
@@ -54,7 +54,7 @@
 import org.apache.causeway.core.metamodel.facets.actcoll.typeof.TypeOfFacet;
 import org.apache.causeway.core.metamodel.facets.object.mixin.MixinFacet;
 import 
org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutorRegistry;
-import 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionState;
+import 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest;
 import org.apache.causeway.core.metamodel.specloader.typeextract.TypeExtractor;
 
 import lombok.Getter;
@@ -195,7 +195,7 @@ private List<FacetedMethod> 
createAssociationFacetedMethods() {
         // Ensure all return types are known
         TypeExtractor.streamMethodReturn(associationCandidateMethods)
             .filter(typeToLoad->typeToLoad!=introspectedClass)
-            .forEach(typeToLoad->specLoader.loadSpecification(typeToLoad, 
IntrospectionState.TYPE_INTROSPECTED));
+            .forEach(typeToLoad->specLoader.loadSpecification(typeToLoad, 
IntrospectionRequest.TYPE_ONLY));
 
         // now create FacetedMethods for collections and for properties
         var associationFacetedMethods = new ArrayList<FacetedMethod>();
@@ -376,7 +376,7 @@ private boolean representsAction(final ResolvedMethod 
actionMethod) {
 
         // ensure we can load returned element type; otherwise ignore method
         var anyLoadedAsNull = TypeExtractor.streamMethodReturn(actionMethod)
-        .map(typeToLoad->specLoaderInternal().loadSpecification(typeToLoad, 
IntrospectionState.TYPE_INTROSPECTED))
+        .map(typeToLoad->specLoaderInternal().loadSpecification(typeToLoad, 
IntrospectionRequest.TYPE_ONLY))
         .anyMatch(Objects::isNull);
         if (anyLoadedAsNull) {
             return false;
@@ -431,7 +431,7 @@ private boolean isMixinMain(final ResolvedMethod method) {
                 .orElse(null);
         if(mixinFacet==null) return false;
 
-        
if(inspectedTypeSpec.isLessThan(IntrospectionState.FULLY_INTROSPECTED)) {
+        if(!inspectedTypeSpec.isFullyIntrospected()) {
             // members are not introspected yet, so make a guess
             return mixinFacet.isCandidateForMain(method);
         }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java
index c53bb29536d..fef33c364eb 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationDefault.java
@@ -26,6 +26,7 @@
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -55,7 +56,6 @@
 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;
 import org.apache.causeway.commons.internal.reflection._ClassCache;
 import 
org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
 import 
org.apache.causeway.commons.internal.reflection._MethodFacades.MethodFacade;
@@ -114,6 +114,7 @@
 
 import static org.apache.causeway.commons.internal.base._NullSafe.stream;
 
+import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 
@@ -315,7 +316,8 @@ public Optional<ObjectAction> getDeclaredAction(
             final ImmutableEnumSet<ActionScope> actionScopes,
             final MixedIn mixedIn) {
 
-        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED);
+        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED,
+                ()->"getDeclaredAction %s on %s".formatted(id, 
this.getFeatureIdentifier()));
 
         return _Strings.isEmpty(id)
             ? Optional.empty()
@@ -329,7 +331,8 @@ public Optional<ObjectAction> getDeclaredAction(
 
     @Override
     public Optional<? extends ObjectMember> getMember(final ResolvedMethod 
method) {
-        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED);
+        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED,
+                ()->"getMember %s on %s".formatted(method.name(), 
this.getFeatureIdentifier()));
 
         if (membersByMethod == null) {
             this.membersByMethod = catalogueMembers();
@@ -555,60 +558,85 @@ public final String getFullIdentifier() {
     }
 
     @Override
-    public void introspectUpTo(final IntrospectionState upTo) {
+    public void introspect(IntrospectionRequest request) {
+        switch (request) {
+            case REGISTER -> 
introspectUpTo(IntrospectionState.NOT_INTROSPECTED,
+                ()->"introspect(%s)".formatted(request));
+            case TYPE_ONLY -> 
introspectUpTo(IntrospectionState.TYPE_INTROSPECTED,
+                ()->"introspect(%s)".formatted(request));
+            case FULL -> introspectUpTo(IntrospectionState.FULLY_INTROSPECTED,
+                ()->"introspect(%s)".formatted(request));
+        }
+    }
+
+    enum IntrospectionState {
+        /**
+         * At this stage, {@link LogicalType} only.
+         */
+        NOT_INTROSPECTED,
+        /**
+         * Interim stage, to avoid infinite loops while on way to being {@link 
#TYPE_INTROSPECTED}
+         */
+        TYPE_BEING_INTROSPECTED,
+        /**
+         * Type has been introspected (but not its members).
+         */
+        TYPE_INTROSPECTED,
+        /**
+         * Interim stage, to avoid infinite loops while on way to being {@link 
#FULLY_INTROSPECTED}
+         */
+        MEMBERS_BEING_INTROSPECTED,
+        /**
+         * Fully introspected... class and also its members.
+         */
+        FULLY_INTROSPECTED
+    }
 
-        if(!isLessThan(upTo)) {
-            return; // optimization
+    /**
+     * keeps track of the causal chain of introspection requests
+     */
+    @AllArgsConstructor
+    private final static class IntrospectionContext {
+        final String info;
+        final IntrospectionContext cause;
+        @Override
+        public String toString() {
+            return cause!=null
+                    ? info + "caused by " + cause.toString()
+                    : info;
         }
+    }
+
+    private void introspectUpTo(final IntrospectionState upTo, 
Supplier<String> introspectionContextProvider) {
+        if(!isLessThan(upTo)) return; // optimization
 
         if(log.isDebugEnabled()) {
             log.debug("introspectingUpTo: {}, {}", getFullIdentifier(), upTo);
         }
 
-        boolean revalidate = false;
-
         switch (introspectionState) {
-        case NOT_INTROSPECTED:
-            if(isLessThan(upTo)) {
-                introspectType();
+            case NOT_INTROSPECTED->{
+                if(isLessThan(upTo)) {
+                    introspectType();
+                }
+                if(isLessThan(upTo)) {
+                    introspectFully();
+                    specLoaderInternal().validateLater(this, 
introspectionContextProvider);
+                }
             }
-            if(isLessThan(upTo)) {
-                introspectFully();
-                revalidate = true;
+            case TYPE_BEING_INTROSPECTED->{} // nothing to do (interim state 
during introspectType)
+            case TYPE_INTROSPECTED->{
+                if(isLessThan(upTo)) {
+                    introspectFully();
+                    specLoaderInternal().validateLater(this, 
introspectionContextProvider);
+                }
             }
-            // set to avoid infinite loops
-            break;
-
-        case TYPE_BEING_INTROSPECTED:
-            // nothing to do (interim state during introspectType)
-            break;
-
-        case TYPE_INTROSPECTED:
-            if(isLessThan(upTo)) {
-                introspectFully();
-                revalidate = true;
-            }
-            break;
-
-        case MEMBERS_BEING_INTROSPECTED:
-            // nothing to do (interim state during introspectully)
-            break;
-
-        case FULLY_INTROSPECTED:
-            // nothing to do ... all done
-            break;
-
-        default:
-            throw _Exceptions.unexpectedCodeReach();
-        }
-
-        if(revalidate) {
-            getSpecificationLoader().validateLater(this);
+            case MEMBERS_BEING_INTROSPECTED->{}// nothing to do (interim state 
during introspect fully)
+            case FULLY_INTROSPECTED->{}// nothing to do ... all done
         }
     }
 
     private void introspectType() {
-
         // set to avoid infinite loops
         this.introspectionState = IntrospectionState.TYPE_BEING_INTROSPECTED;
         introspectTypeHierarchy();
@@ -627,7 +655,7 @@ private void introspectFully() {
         Facets.gridPreload(this, null);
     }
 
-    boolean isLessThan(final IntrospectionState upTo) {
+    private boolean isLessThan(final IntrospectionState upTo) {
         return this.introspectionState.compareTo(upTo) < 0;
     }
 
@@ -988,7 +1016,8 @@ public boolean hasSubclasses() {
 
     @Override
     public Stream<ObjectAssociation> streamDeclaredAssociations(final MixedIn 
mixedIn) {
-        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED);
+        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED,
+                ()->"streamDeclaredAssociations of 
%s".formatted(this.getFeatureIdentifier()));
 
         
mixedInAssociationAdder.trigger(this::createMixedInAssociationsAndResort); // 
only if not already
 
@@ -1000,30 +1029,26 @@ public Stream<ObjectAssociation> 
streamDeclaredAssociations(final MixedIn mixedI
 
     @Override
     public Optional<? extends ObjectMember> getMember(final String memberId) {
-        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED);
+        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED,
+                ()->"getMember %s of %s".formatted(memberId, 
this.getFeatureIdentifier()));
 
-        if(_Strings.isEmpty(memberId)) {
-            return Optional.empty();
-        }
+        if(_Strings.isEmpty(memberId)) return Optional.empty();
 
         var objectAction = getAction(memberId);
-        if(objectAction.isPresent()) {
-            return objectAction;
-        }
+        if(objectAction.isPresent()) return objectAction;
+
         var association = getAssociation(memberId);
-        if(association.isPresent()) {
-            return association;
-        }
+        if(association.isPresent()) return association;
+
         return Optional.empty();
     }
 
     @Override
     public Optional<ObjectAssociation> getDeclaredAssociation(final String id, 
final MixedIn mixedIn) {
-        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED);
+        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED,
+                ()->"getDeclaredAssociation %s of %s".formatted(id, 
this.getFeatureIdentifier()));
 
-        if(_Strings.isEmpty(id)) {
-            return Optional.empty();
-        }
+        if(_Strings.isEmpty(id)) return Optional.empty();
 
         return streamDeclaredAssociations(mixedIn)
                 
.filter(objectAssociation->objectAssociation.getId().equals(id))
@@ -1040,7 +1065,8 @@ public Stream<ObjectAction> streamRuntimeActions(final 
MixedIn mixedIn) {
     public Stream<ObjectAction> streamDeclaredActions(
             final ImmutableEnumSet<ActionScope> actionScopes,
             final MixedIn mixedIn) {
-        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED);
+        introspectUpTo(IntrospectionState.FULLY_INTROSPECTED,
+                ()->"streamDeclaredActions of 
%s".formatted(this.getFeatureIdentifier()));
 
         mixedInActionAdder.trigger(this::createMixedInActionsAndResort);
 
@@ -1062,9 +1088,8 @@ private Stream<ObjectAssociation> 
createMixedInAssociations() {
     }
 
     private Stream<ObjectAssociation> createMixedInAssociation(final Class<?> 
mixinType) {
-
         var mixinSpec = specLoaderInternal().loadSpecification(mixinType,
-                IntrospectionState.FULLY_INTROSPECTED);
+                IntrospectionRequest.FULL);
         if (mixinSpec == null
                 || mixinSpec == this) {
             return Stream.empty();
@@ -1098,7 +1123,7 @@ private Stream<ObjectActionMixedIn> 
createMixedInActions() {
     private Stream<ObjectActionMixedIn> createMixedInAction(final Class<?> 
mixinType) {
 
         var mixinSpec = specLoaderInternal().loadSpecification(mixinType,
-                IntrospectionState.FULLY_INTROSPECTED);
+                IntrospectionRequest.FULL);
         if (mixinSpec == null
                 || mixinSpec == this) {
             return Stream.empty();
@@ -1250,4 +1275,8 @@ private void createMixedInAssociationsAndResort() {
     private final Can<EntityTitleSubscriber> titleSubscribers =
         getServiceRegistry().select(EntityTitleSubscriber.class);
 
+    boolean isFullyIntrospected() {
+        return this.introspectionState == 
IntrospectionState.FULLY_INTROSPECTED;
+    }
+
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationMutable.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationMutable.java
index 8f3ec29427c..68f625db7be 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationMutable.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectSpecificationMutable.java
@@ -18,43 +18,25 @@
  */
 package org.apache.causeway.core.metamodel.spec.impl;
 
-import org.apache.causeway.applib.id.LogicalType;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 
 interface ObjectSpecificationMutable extends ObjectSpecification {
-    
-    public enum IntrospectionState implements Comparable<IntrospectionState> {
-        /**
-         * At this stage, {@link LogicalType} only.
-         */
-        NOT_INTROSPECTED,
 
+    enum IntrospectionRequest {
         /**
-         * Interim stage, to avoid infinite loops while on way to being {@link 
#TYPE_INTROSPECTED}
+         * No introspection, just register the type, that is, create an 
initial yet empty {@link ObjectSpecification}.
          */
-        TYPE_BEING_INTROSPECTED,
-
+        REGISTER,
         /**
-         * Type has been introspected (but not its members).
+         * Partial introspection, that only includes type-hierarchy but not 
members.
          */
-        TYPE_INTROSPECTED,
-
+        TYPE_ONLY,
         /**
-         * Interim stage, to avoid infinite loops while on way to being {@link 
#FULLY_INTROSPECTED}
+         * Full introspection, that includes type-hierarchy and members.
          */
-        MEMBERS_BEING_INTROSPECTED,
+        FULL
+    }
 
-        /**
-         * Fully introspected... class and also its members.
-         */
-        FULLY_INTROSPECTED
+    void introspect(IntrospectionRequest request);
 
-    }
-    
-    /**
-     * Introspecting up to the level required.
-     * @since 2.0
-     */
-    void introspectUpTo(IntrospectionState upTo);
-    
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderDefault.java
index 99678aeafa3..be0a24ebda4 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderDefault.java
@@ -31,6 +31,7 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import java.util.stream.Stream;
 
 import jakarta.annotation.PostConstruct;
@@ -40,8 +41,10 @@
 import jakarta.inject.Named;
 import jakarta.inject.Provider;
 
-import org.springframework.beans.factory.annotation.Qualifier;
+import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
+
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
 import org.apache.causeway.applib.Identifier;
@@ -77,14 +80,13 @@
 import 
org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutorRegistry;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
-import 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionState;
+import 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailures;
 import 
org.apache.causeway.core.metamodel.valuetypes.ValueSemanticsResolverDefault;
 import 
org.apache.causeway.core.security.authorization.manager.ActionSemanticsResolver;
 
 import lombok.Getter;
-import org.jspecify.annotations.NonNull;
 import lombok.Setter;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
@@ -267,21 +269,21 @@ public void createMetaModel() {
             .map(this::primeSpecification)
             .forEach(specs::collect);
 
-        introspectAndLog("type hierarchies", specs.knownSpecs, 
IntrospectionState.TYPE_INTROSPECTED);
-        introspectAndLog("value types", specs.valueSpecs.values(), 
IntrospectionState.FULLY_INTROSPECTED);
-        introspectAndLog("mixins", specs.mixinSpecs, 
IntrospectionState.FULLY_INTROSPECTED);
-        introspectAndLog("domain services", specs.domainServiceSpecs, 
IntrospectionState.FULLY_INTROSPECTED);
+        introspectAndLog("type hierarchies", specs.knownSpecs, 
IntrospectionRequest.TYPE_ONLY);
+        introspectAndLog("value types", specs.valueSpecs.values(), 
IntrospectionRequest.FULL);
+        introspectAndLog("mixins", specs.mixinSpecs, 
IntrospectionRequest.FULL);
+        introspectAndLog("domain services", specs.domainServiceSpecs, 
IntrospectionRequest.FULL);
         introspectAndLog("entities 
(%s)".formatted(causewayBeanTypeRegistry.persistenceStack().name()),
-                specs.entitySpecs(), IntrospectionState.FULLY_INTROSPECTED);
-        introspectAndLog("view models", specs.viewmodelSpecs(), 
IntrospectionState.FULLY_INTROSPECTED);
+                specs.entitySpecs(), IntrospectionRequest.FULL);
+        introspectAndLog("view models", specs.viewmodelSpecs(), 
IntrospectionRequest.FULL);
 
         
serviceRegistry.lookupServiceElseFail(MenuBarsService.class).menuBars();
 
         if(isFullIntrospect()) {
             var snapshot = snapshotSpecifications();
             log.info(" - introspecting all {} types eagerly 
(FullIntrospect=true)", snapshot.size());
-            introspect(snapshot.filter(x->x.getBeanSort().isMixin()), 
IntrospectionState.FULLY_INTROSPECTED);
-            introspect(snapshot.filter(x->!x.getBeanSort().isMixin()), 
IntrospectionState.FULLY_INTROSPECTED);
+            introspect(snapshot.filter(x->x.getBeanSort().isMixin()), 
IntrospectionRequest.FULL);
+            introspect(snapshot.filter(x->!x.getBeanSort().isMixin()), 
IntrospectionRequest.FULL);
         }
 
         log.info(" - running remaining validators");
@@ -353,7 +355,7 @@ private boolean isFullIntrospect() {
     @Override
     public void reloadSpecification(final Class<?> domainType) {
         invalidateCache(domainType);
-        loadSpecification(domainType, IntrospectionState.FULLY_INTROSPECTED);
+        loadSpecification(domainType, IntrospectionRequest.FULL);
     }
 
     @Override
@@ -370,7 +372,6 @@ public boolean loadSpecifications(final Class<?>... 
domainTypes) {
 
     /**
      * Return the specification for the specified class of object.
-     *
      * <p>
      * It is possible for this method to return <tt>null</tt>, for example if
      * any of the configured {@link ClassSubstitutor}s has filtered out the 
class.
@@ -380,12 +381,14 @@ public boolean loadSpecifications(final Class<?>... 
domainTypes) {
     @Override
     public ObjectSpecification loadSpecification(
             final @Nullable Class<?> type,
-            final @NonNull IntrospectionState upTo) {
-        return loadSpecificationNullable(type, this::classify, upTo);
+            final @NonNull IntrospectionRequest request) {
+        return loadSpecificationNullable(type, this::classify, request);
     }
 
     @Override
-    public void validateLater(final ObjectSpecification objectSpec) {
+    public void validateLater(
+            final ObjectSpecification objectSpec,
+            final Supplier<String> introspectionContextProvider) {
         if(!isMetamodelFullyIntrospected()) {
             // don't trigger validation during bootstrapping
             // getValidationResult() is lazily populated later on first 
request anyway
@@ -396,7 +399,9 @@ public void validateLater(final ObjectSpecification 
objectSpec) {
             return;
         }
 
-        log.info("re-validation triggered by {}", objectSpec);
+        if(log.isInfoEnabled()) {
+            log.info("re-validation triggered by {}", 
introspectionContextProvider.get());
+        }
 
         // validators might discover new specs
         // to prevent deadlocks, we queue up validation requests to be 
processed later
@@ -547,7 +552,7 @@ private CausewayBeanMetaData classify(final @Nullable 
Class<?> type) {
     private ObjectSpecificationMutable primeSpecification(
             final @NonNull CausewayBeanMetaData typeMeta) {
         return loadSpecificationNullable(
-                typeMeta.getCorrespondingClass(), type->typeMeta, 
IntrospectionState.NOT_INTROSPECTED);
+                typeMeta.getCorrespondingClass(), type->typeMeta, 
IntrospectionRequest.REGISTER);
 
     }
 
@@ -555,11 +560,9 @@ private ObjectSpecificationMutable primeSpecification(
     private ObjectSpecificationMutable loadSpecificationNullable(
             final @Nullable Class<?> type,
             final @NonNull Function<Class<?>, CausewayBeanMetaData> 
beanClassifier,
-            final @NonNull IntrospectionState upTo) {
+            final @NonNull IntrospectionRequest request) {
 
-        if(type==null) {
-            return null;
-        }
+        if(type==null) return null;
 
         var substitute = classSubstitutorRegistry.getSubstitution(type);
         if (substitute.isNeverIntrospect()) return null; // never inspect
@@ -571,11 +574,11 @@ private ObjectSpecificationMutable 
loadSpecificationNullable(
                 .register(
                         
createSpecification(beanClassifier.apply(substitutedType))));
 
-        spec.introspectUpTo(upTo);
+        spec.introspect(request);
 
         if(spec.getAliases().isNotEmpty()
             // this bool. expr. is an optimization, not strictly required ... 
a bit of hack though
-            && upTo == IntrospectionState.TYPE_INTROSPECTED) {
+            && request == IntrospectionRequest.TYPE_ONLY) {
 
             //XXX[3063] hitting this a couple of times
             //(~5 see 
org.apache.causeway.testdomain.domainmodel.DomainModelTest_usingGoodDomain.aliasesOnDomainServices_shouldBeHonored())
@@ -607,19 +610,19 @@ private ObjectSpecificationMutable 
createSpecification(final CausewayBeanMetaDat
 
     private void introspectSequential(
             final Can<ObjectSpecificationMutable> specs,
-            final IntrospectionState upTo) {
+            final IntrospectionRequest request) {
         for (var spec : specs) {
-            spec.introspectUpTo(upTo);
+            spec.introspect(request);
         }
     }
 
     private void introspectParallel(
             final Can<ObjectSpecificationMutable> specs,
-            final IntrospectionState upTo) {
+            final IntrospectionRequest request) {
         specs.parallelStream()
         .forEach(spec -> {
             try {
-                spec.introspectUpTo(upTo);
+                spec.introspect(request);
             } catch (Throwable ex) {
                 log.error("failure", ex);
                 throw ex;
@@ -630,37 +633,34 @@ private void introspectParallel(
     private void introspectAndLog(
             final String info,
             final Iterable<ObjectSpecificationMutable> specs,
-            final IntrospectionState upTo) {
+            final IntrospectionRequest request) {
         var stopWatch = _Timing.now();
-        introspect(Can.ofIterable(specs), upTo);
+        introspect(Can.ofIterable(specs), request);
         stopWatch.stop();
         log.info(" - introspecting {} {} took {}ms", 
_NullSafe.sizeAutodetect(specs), info, stopWatch.getMillis());
     }
 
     private void introspect(
             final Can<ObjectSpecificationMutable> specs,
-            final IntrospectionState upTo) {
+            final IntrospectionRequest request) {
         if(parallel) {
-            introspectParallel(specs, upTo);
+            introspectParallel(specs, request);
         } else {
-            introspectSequential(specs, upTo);
+            introspectSequential(specs, request);
         }
     }
 
     private void invalidateCache(final Class<?> cls) {
-
         var substitute = classSubstitutorRegistry.getSubstitution(cls);
-        if(substitute.isNeverIntrospect()) {
-            return;
-        }
+        if(substitute.isNeverIntrospect()) return;
 
-        ObjectSpecification spec =
-                loadSpecification(substitute.apply(cls), 
IntrospectionState.FULLY_INTROSPECTED);
+        var objSpec =
+                loadSpecification(substitute.apply(cls), 
IntrospectionRequest.FULL);
 
-        while(spec != null) {
-            var type = spec.getCorrespondingClass();
+        while(objSpec != null) {
+            var type = objSpec.getCorrespondingClass();
             cache.remove(type);
-            spec = spec.superclass();
+            objSpec = objSpec.superclass();
         }
     }
 
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderInternal.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderInternal.java
index c3ffbe5eea6..47235a2515e 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderInternal.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/SpecificationLoaderInternal.java
@@ -19,9 +19,11 @@
 package org.apache.causeway.core.metamodel.spec.impl;
 
 import java.util.Optional;
+import java.util.function.Supplier;
 
 import jakarta.inject.Named;
 
+import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.id.LogicalType;
@@ -31,15 +33,13 @@
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import 
org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutor;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
-import 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionState;
+import 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionRequest;
 import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
 
-import org.jspecify.annotations.NonNull;
-
 interface SpecificationLoaderInternal extends SpecificationLoader {
+
     /**
      * Return the specification for the specified class of object.
-     *
      * <p>
      * It is possible for this method to return <tt>null</tt>, for example if
      * any of the configured {@link ClassSubstitutor}s has filtered out the 
class.
@@ -47,8 +47,8 @@ interface SpecificationLoaderInternal extends 
SpecificationLoader {
      * @return {@code null} if {@code domainType==null}, or if the type should 
be ignored.
      */
     @Nullable
-    ObjectSpecification loadSpecification(@Nullable Class<?> domainType, 
@NonNull IntrospectionState upTo);
-    
+    ObjectSpecification loadSpecification(@Nullable Class<?> domainType, 
@NonNull IntrospectionRequest request);
+
     // -- SUPPORT FOR LOOKUP BY LOGICAL TYPE NAME
 
     /**
@@ -59,25 +59,27 @@ interface SpecificationLoaderInternal extends 
SpecificationLoader {
     @Nullable
     default ObjectSpecification loadSpecification(
             final @Nullable String logicalTypeName,
-            final @NonNull  IntrospectionState introspectionState) {
+            final @NonNull IntrospectionRequest request) {
 
         if(_Strings.isNullOrEmpty(logicalTypeName)) {
             return null;
         }
         return lookupLogicalType(logicalTypeName)
             .map(logicalType->
-                    loadSpecification(logicalType.correspondingClass(), 
introspectionState))
+                    loadSpecification(logicalType.correspondingClass(), 
request))
             .orElse(null);
     }
-    
+
     // -- SHORTCUTS - 1
 
+    @Override
     default Optional<ObjectSpecification> specForLogicalTypeName(
             final @Nullable String logicalTypeName) {
         return Optional.ofNullable(
-                loadSpecification(logicalTypeName, 
IntrospectionState.FULLY_INTROSPECTED));
+                loadSpecification(logicalTypeName, IntrospectionRequest.FULL));
     }
 
+    @Override
     default Optional<ObjectSpecification> specForLogicalType(
             final @Nullable LogicalType logicalType) {
         return Optional.ofNullable(logicalType)
@@ -85,12 +87,14 @@ default Optional<ObjectSpecification> specForLogicalType(
                 .flatMap(this::specForType);
     }
 
+    @Override
     default Optional<ObjectSpecification> specForType(
             final @Nullable Class<?> domainType) {
         return Optional.ofNullable(
-                loadSpecification(domainType, 
IntrospectionState.FULLY_INTROSPECTED));
+                loadSpecification(domainType, IntrospectionRequest.FULL));
     }
 
+    @Override
     default Optional<ObjectSpecification> specForBookmark(
             final @Nullable Bookmark bookmark) {
         return Optional.ofNullable(bookmark)
@@ -100,6 +104,7 @@ default Optional<ObjectSpecification> specForBookmark(
 
     // -- SHORTCUTS - 2
 
+    @Override
     default ObjectSpecification specForLogicalTypeNameElseFail(
             final @Nullable String logicalTypeName) {
         return specForLogicalTypeName(logicalTypeName)
@@ -108,6 +113,7 @@ default ObjectSpecification specForLogicalTypeNameElseFail(
                         _Strings.nullToEmpty(logicalTypeName)));
     }
 
+    @Override
     default ObjectSpecification specForLogicalTypeElseFail(
             final @Nullable LogicalType logicalType) {
         return specForLogicalType(logicalType)
@@ -116,6 +122,7 @@ default ObjectSpecification specForLogicalTypeElseFail(
                         logicalType));
     }
 
+    @Override
     default ObjectSpecification specForTypeElseFail(
             final @Nullable Class<?> domainType) {
         return specForType(domainType)
@@ -124,6 +131,7 @@ default ObjectSpecification specForTypeElseFail(
                         domainType));
     }
 
+    @Override
     default ObjectSpecification specForBookmarkElseFail(
             final @Nullable Bookmark bookmark) {
         return specForBookmark(bookmark)
@@ -134,17 +142,26 @@ default ObjectSpecification specForBookmarkElseFail(
 
     // -- CAUTION! (use only during meta-model initialization)
 
+    @Override
     default @Nullable ObjectSpecification loadSpecification(
             final @Nullable Class<?> domainType) {
-        return loadSpecification(domainType, 
IntrospectionState.TYPE_INTROSPECTED);
+        return loadSpecification(domainType, IntrospectionRequest.TYPE_ONLY);
     }
-    
+
     @Override
     default Optional<BeanSort> lookupBeanSort(final @Nullable LogicalType 
logicalType) {
         if(logicalType==null) return Optional.empty();
-        var spec = loadSpecification(logicalType.correspondingClass(), 
IntrospectionState.NOT_INTROSPECTED);
-        return spec != null 
+        var spec = loadSpecification(logicalType.correspondingClass(), 
IntrospectionRequest.REGISTER);
+        return spec != null
                 ? Optional.of(spec.getBeanSort())
                 : Optional.empty();
     }
+
+    /**
+     * queue {@code objectSpec} for later validation
+     * @param objectSpec
+     * @param introspectionContextProvider
+     */
+    void validateLater(ObjectSpecification objectSpec, Supplier<String> 
introspectionContextProvider);
+
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoader.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoader.java
index fc653675974..bd48f30d9af 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoader.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoader.java
@@ -21,6 +21,7 @@
 import java.util.Optional;
 import java.util.function.Consumer;
 
+import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.Identifier;
@@ -38,8 +39,6 @@
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailures;
 
-import org.jspecify.annotations.NonNull;
-
 /**
  * Builds the meta-model, utilizing an instance of {@link ProgrammingModel}
  */
@@ -124,12 +123,6 @@ default LogicalType lookupLogicalTypeElseFail(final 
@NonNull String logicalTypeN
         );
     }
 
-    /**
-     * queue {@code objectSpec} for later validation
-     * @param objectSpec
-     */
-    void validateLater(ObjectSpecification objectSpec);
-
     // -- LOOKUP API
 
     Optional<ObjectSpecification> specForLogicalTypeName(@Nullable String 
logicalTypeName);
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionState_comparable_Test.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionState_comparable_Test.java
index fb55239586c..2f2237bfa34 100644
--- 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionState_comparable_Test.java
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/spec/impl/IntrospectionState_comparable_Test.java
@@ -26,7 +26,7 @@
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 
-import 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationMutable.IntrospectionState;
+import 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationDefault.IntrospectionState;
 
 public class IntrospectionState_comparable_Test {
 


Reply via email to