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

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

commit b741654e834aad29c038bb152c48066410fc9bfc
Author: Andi Huber <[email protected]>
AuthorDate: Fri Jun 27 08:05:45 2025 +0200

    CAUSEWAY-2297: immutable InteractionResult (refactor)
---
 .../applib/events/InteractionEventTest.java        |  22 +--
 core/metamodel/src/main/java/module-info.java      |   1 +
 .../core/metamodel/consent/ConsentAbstract.java    |   2 +-
 .../core/metamodel/consent/InteractionResult.java  | 149 ++++++---------------
 .../metamodel/consent/InteractionResultSet.java    |  13 +-
 .../metamodel/interactions/InteractionUtils.java   |  82 ++++++------
 .../interactions/managed/ManagedFeature.java       |   3 +-
 .../interactions/managed/ManagedMember.java        |   8 +-
 .../interactions/managed/ManagedParameter.java     |  11 +-
 .../managed/ParameterNegotiationModel.java         |   7 +-
 .../core/metamodel/object/MmVisibilityUtils.java   |   2 +-
 .../metamodel/consent/InteractionResultTest.java   |  59 ++++----
 .../handlers/DomainObjectInvocationHandler.java    |  56 +++-----
 13 files changed, 164 insertions(+), 251 deletions(-)

diff --git 
a/api/applib/src/test/java/org/apache/causeway/applib/events/InteractionEventTest.java
 
b/api/applib/src/test/java/org/apache/causeway/applib/events/InteractionEventTest.java
index cca86b18d5b..c7d2dcf37b9 100644
--- 
a/api/applib/src/test/java/org/apache/causeway/applib/events/InteractionEventTest.java
+++ 
b/api/applib/src/test/java/org/apache/causeway/applib/events/InteractionEventTest.java
@@ -42,7 +42,7 @@ class InteractionEventTest {
     private static class CustomerOrder {}
 
     @BeforeEach
-    public void setUp() {
+    void setUp() {
         source = new Object();
         identifier = Identifier.actionIdentifier(
                 LogicalType.fqcn(CustomerOrder.class),
@@ -51,7 +51,7 @@ public void setUp() {
     }
 
     @Test
-    public void getIdentifier() {
+    void getIdentifier() {
         interactionEvent = new InteractionEvent(source, identifier) {
 
         };
@@ -59,7 +59,7 @@ public void getIdentifier() {
     }
 
     @Test
-    public void getSource() {
+    void getSource() {
         interactionEvent = new InteractionEvent(source, identifier) {
 
         };
@@ -67,7 +67,7 @@ public void getSource() {
     }
 
     @Test
-    public void getClassName() {
+    void getClassName() {
         interactionEvent = new InteractionEvent(source, identifier) {
 
         };
@@ -75,7 +75,7 @@ public void getClassName() {
     }
 
     @Test
-    public void getClassNaturalName() {
+    void getClassNaturalName() {
         interactionEvent = new InteractionEvent(source, identifier) {
 
         };
@@ -83,7 +83,7 @@ public void getClassNaturalName() {
     }
 
     @Test
-    public void getMember() {
+    void getMember() {
         interactionEvent = new InteractionEvent(source, identifier) {
 
         };
@@ -91,7 +91,7 @@ public void getMember() {
     }
 
     @Test
-    public void getMemberNaturalName() {
+    void getMemberNaturalName() {
         interactionEvent = new InteractionEvent(source, identifier) {
 
         };
@@ -99,7 +99,7 @@ public void getMemberNaturalName() {
     }
 
     @Test
-    public void shouldInitiallyNotVeto() {
+    void shouldInitiallyNotVeto() {
         interactionEvent = new InteractionEvent(source, identifier) {
 
         };
@@ -107,7 +107,7 @@ public void shouldInitiallyNotVeto() {
     }
 
     @Test
-    public void afterAdvisedShouldVeto() {
+    void afterAdvisedShouldVeto() {
         interactionEvent = new InteractionEvent(source, identifier) {
 
         };
@@ -116,7 +116,7 @@ public void afterAdvisedShouldVeto() {
     }
 
     @Test
-    public void afterAdvisedShouldReturnReason() {
+    void afterAdvisedShouldReturnReason() {
         interactionEvent = new InteractionEvent(source, identifier) {
 
         };
@@ -125,7 +125,7 @@ public void afterAdvisedShouldReturnReason() {
     }
 
     @Test
-    public void afterAdvisedShouldReturnAdvisorClass() {
+    void afterAdvisedShouldReturnAdvisorClass() {
         interactionEvent = new InteractionEvent(source, identifier) {
 
         };
diff --git a/core/metamodel/src/main/java/module-info.java 
b/core/metamodel/src/main/java/module-info.java
index aaae8756b6f..50a01928912 100644
--- a/core/metamodel/src/main/java/module-info.java
+++ b/core/metamodel/src/main/java/module-info.java
@@ -171,6 +171,7 @@
     requires spring.context;
     requires spring.core;
     requires spring.boot.autoconfigure;
+    requires org.jspecify;
 
 //JUnit testing stuff, not required as long this module is an 'open' one
 //    opens org.apache.causeway.core.metamodel.services to spring.core;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/ConsentAbstract.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/ConsentAbstract.java
index 733db18abbb..fa4abf02d24 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/ConsentAbstract.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/ConsentAbstract.java
@@ -53,7 +53,7 @@ public static Consent allowIf(final boolean allowed) {
     private static VetoReason determineReason(final InteractionResult 
interactionResult) {
         return interactionResult == null
             ? null
-            : interactionResult.getReason().orElse(null);
+            : interactionResult.vetoReason().orElse(null);
     }
 
     /**
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/InteractionResult.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/InteractionResult.java
index caa453b377f..6f2a155842b 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/InteractionResult.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/InteractionResult.java
@@ -19,131 +19,68 @@
 package org.apache.causeway.core.metamodel.consent;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 
 import org.apache.causeway.applib.services.wrapper.events.InteractionEvent;
+import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.core.metamodel.consent.Consent.VetoReason;
 
-public class InteractionResult {
-
-    /**
-     * Initially {@link #ADVISING}; when call
-     * {@link InteractionResult#getInteractionEvent()}, flips over into
-     * {@link #ADVISED}.
-     *
-     * <p>
-     * Subsequent attempts to
-     * {@link InteractionResult#advise(String, InteractionAdvisor)} will then 
be
-     * disallowed.
-     */
-    enum State {
-        ADVISING, ADVISED
-    }
-
-    private final InteractionEvent interactionEvent;
-    private final List<Consent.VetoReason> reasonBuf = new ArrayList<>();
-    private final List<InteractionAdvisor> advisors = new 
ArrayList<InteractionAdvisor>();
-
-    private State state = State.ADVISING;
-
-    public InteractionResult(final InteractionEvent interactionEvent) {
-        this.interactionEvent = interactionEvent;
-    }
-
-    /**
-     * Returns the contained {@link InteractionEvent}, if necessary updated 
with
-     * the {@link 
#advise(org.apache.causeway.core.metamodel.consent.Consent.VetoReason, 
InteractionAdvisor) advice} of the
-     * interactions.
-     *
-     * <p>
-     * That is, if still {@link State#ADVISING advising}, then copies over the
-     * details from this result into the contained {@link InteractionEvent}, 
and
-     * flips into {@link State#ADVISED advised (done)}.
-     */
-    public InteractionEvent getInteractionEvent() {
-        if (state == State.ADVISING) {
-            final String nullableReasonString = 
getReason().map(VetoReason::string).orElse(null);
-            interactionEvent.advised(nullableReasonString, getAdvisorClass());
-            state = State.ADVISED;
+public record InteractionResult(
+        InteractionEvent interactionEvent,
+        Optional<Consent.VetoReason> vetoReason,
+        /**
+         * Any {@link InteractionAdvisor} advisors that have appended veto 
reasons.
+         */
+        Can<InteractionAdvisor> advisors) {
+
+    public static Builder builder(final InteractionEvent interactionEvent) { 
return new Builder(interactionEvent); }
+    public record Builder(
+            InteractionEvent interactionEvent,
+            List<Consent.VetoReason> reasonBuf,
+            List<InteractionAdvisor> advisors) {
+        public Builder(InteractionEvent interactionEvent) {
+            this(interactionEvent, new ArrayList<>(), new ArrayList<>());
         }
-        return interactionEvent;
-    }
-
-    private Class<?> getAdvisorClass() {
-        final InteractionAdvisor advisor = getAdvisor();
-        return advisor != null ? advisor.getClass() : null;
-    }
-
-    public void advise(final Consent.VetoReason reason, final 
InteractionAdvisor facet) {
-        if (state == State.ADVISED) {
-            throw new IllegalStateException("Cannot append since have called 
getInteractionEvent");
+        public void addAdvise(final VetoReason reason, final 
InteractionAdvisor facet) {
+            reasonBuf.add(Objects.requireNonNull(reason));
+            advisors.add(Objects.requireNonNull(facet));
         }
-        if (reason == null) {
-            return;
+        public InteractionResult build() {
+            Optional<Consent.VetoReason> reason = 
reasonBuf.stream().reduce(Consent.VetoReason::reduce);
+            return new InteractionResult(interactionEvent, reason, 
Can.ofCollection(advisors));
         }
-        advisors.add(facet);
-        reasonBuf.add(reason);
     }
 
-    public boolean isVetoing() {
-        return !isNotVetoing();
+    // canonical constructor
+    public InteractionResult(
+            InteractionEvent interactionEvent,
+            Optional<Consent.VetoReason> vetoReason,
+            Can<InteractionAdvisor> advisors) {
+        this.interactionEvent = Objects.requireNonNull(interactionEvent);
+        this.vetoReason = Objects.requireNonNull(vetoReason);
+        this.advisors = Objects.requireNonNull(advisors);
+
+        vetoReason.ifPresent(reason->{
+            InteractionAdvisor advisor = 
advisors.stream().findFirst().orElseThrow();
+            interactionEvent.advised(reason.string(), advisor.getClass());
+        });
     }
 
-    public boolean isNotVetoing() {
-        return reasonBuf.size() == 0;
-    }
-
-    /**
-     * Returns the first of the {@link #getAdvisorFacets()} that has been
-     * {@link 
#advise(org.apache.causeway.core.metamodel.consent.Consent.VetoReason, 
InteractionAdvisor) advised} , or <tt>null</tt> if
-     * none yet.
-     *
-     * @see #getAdvisorFacets()
-     */
-    public InteractionAdvisor getAdvisor() {
-        return advisors.size() >= 1 ? advisors.get(0) : null;
-    }
-
-    /**
-     * Returns all {@link InteractionAdvisor advisor} (facet)s that have
-     * {@link 
#advise(org.apache.causeway.core.metamodel.consent.Consent.VetoReason, 
InteractionAdvisor) append}ed reasons to the
-     * buffer.
-     *
-     * @see #getAdvisor()
-     */
-    public List<InteractionAdvisor> getAdvisorFacets() {
-        return Collections.unmodifiableList(advisors);
-    }
+    public boolean isAllowing() { return vetoReason.isEmpty(); }
+    public boolean isVetoing() { return vetoReason.isPresent(); }
 
     public Consent createConsent() {
-        if (isNotVetoing()) {
-            return new Allow(this);
-        } else {
-            return new Veto(this);
-        }
-    }
-
-    /**
-     * Gets the reason as currently known, but does not change the state.
-     * <p>
-     * If {@link #isNotVetoing()}, then returns <tt>Optional.empty()</tt>.
-     */
-    public Optional<Consent.VetoReason> getReason() {
-        return reasonBuf.stream().reduce(Consent.VetoReason::reduce);
+        return isAllowing()
+            ? new Allow(this)
+            : new Veto(this);
     }
 
     @Override
     public String toString() {
-        return String.format("%s: %s: %s (%d facets advised)",
-                interactionEvent, state, toStringInterpret(), advisors.size());
-    }
-
-    private String toStringInterpret() {
-        return isNotVetoing()
-                ? "allowed"
-                : "vetoed";
+        return String.format("%s: %s (%d facets advised)",
+                interactionEvent, isAllowing() ? "allowed" : "vetoed", 
advisors.size());
     }
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/InteractionResultSet.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/InteractionResultSet.java
index c8716d4029f..60abb129980 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/InteractionResultSet.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/consent/InteractionResultSet.java
@@ -39,9 +39,8 @@ public InteractionResultSet add(final InteractionResult 
result) {
     }
 
     /**
-     * Empty only if all the {@link #add(InteractionResult) contained}
-     * {@link InteractionResult}s are also
-     * {@link InteractionResult#isNotVetoing() empty}.
+     * Allowing only if all the {@link #add(InteractionResult) contained}
+     * {@link InteractionResult}s are also {@link 
InteractionResult#isAllowing()}.
      */
     public boolean isAllowed() {
         return !isVetoed();
@@ -54,9 +53,7 @@ public boolean isAllowed() {
      */
     public boolean isVetoed() {
         for (final InteractionResult result : results) {
-            if (result.isVetoing()) {
-                return true;
-            }
+            if (result.isVetoing()) return true;
         }
         return false;
     }
@@ -84,9 +81,7 @@ public Consent createConsent() {
      */
     public InteractionResult getInteractionResult() {
         for (final InteractionResult result : results) {
-            if (!result.isNotVetoing()) {
-                return result;
-            }
+            if (!result.isAllowing()) return result;
         }
         return firstResult != null ? firstResult : null;
     }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/InteractionUtils.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/InteractionUtils.java
index 68d144dcab6..4c95dd4f0a3 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/InteractionUtils.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/InteractionUtils.java
@@ -24,6 +24,7 @@
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.config.environment.DeploymentType;
 import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
@@ -49,7 +50,7 @@ public final class InteractionUtils {
 
     public InteractionResult isVisibleResult(final FacetHolder facetHolder, 
final VisibilityContext context) {
 
-        var iaResult = new InteractionResult(context.createInteractionEvent());
+        var builder = 
InteractionResult.builder(context.createInteractionEvent());
 
         // depending on the ifHiddenPolicy, we may do no vetoing here 
(instead, it moves into the usability check).
         var ifHiddenPolicy = context.renderPolicy().ifHiddenPolicy();
@@ -58,12 +59,9 @@ public InteractionResult isVisibleResult(final FacetHolder 
facetHolder, final Vi
                 facetHolder.streamFacets(HidingInteractionAdvisor.class)
                 .filter(advisor->compatible(advisor, context))
                 .forEach(advisor->{
-                    var hidingReasonString = advisor.hides(context);
-                    var hidingReason = Optional.ofNullable(hidingReasonString)
-                            .map(Consent.VetoReason::explicit)
-                            .orElse(null);
-
-                    iaResult.advise(hidingReason, advisor);
+                    _Strings.nonEmpty(advisor.hides(context))
+                        .map(Consent.VetoReason::explicit)
+                        
.ifPresent(hidingReason->builder.addAdvise(hidingReason, advisor));
                 });
                 break;
             case SHOW_AS_DISABLED:
@@ -72,12 +70,12 @@ public InteractionResult isVisibleResult(final FacetHolder 
facetHolder, final Vi
                 break;
         }
 
-        return iaResult;
+        return builder.build();
     }
 
     public InteractionResult isUsableResult(final FacetHolder facetHolder, 
final UsabilityContext context) {
 
-        var isResult = new InteractionResult(context.createInteractionEvent());
+        var builder = 
InteractionResult.builder(context.createInteractionEvent());
 
         // depending on the ifHiddenPolicy, we additionally may disable using 
a hidden advisor
         var ifHiddenPolicy = context.renderPolicy().ifHiddenPolicy();
@@ -88,53 +86,49 @@ public InteractionResult isUsableResult(final FacetHolder 
facetHolder, final Usa
             case SHOW_AS_DISABLED_WITH_DIAGNOSTICS:
                 var visibilityContext = context.asVisibilityContext();
                 facetHolder.streamFacets(HidingInteractionAdvisor.class)
-                        .filter(advisor->compatible(advisor, context))
-                        .forEach(advisor->{
-                            String hidingReasonString = 
advisor.hides(visibilityContext);
-                            Consent.VetoReason hidingReason = 
Optional.ofNullable(hidingReasonString)
-                                    .map(Consent.VetoReason::explicit)
-                                    .orElse(null);
-                            if(hidingReason != null
-                                    && 
ifHiddenPolicy.isShowAsDisabledWithDiagnostics()) {
-                                hidingReason = 
VetoUtil.withAdvisorAsDiagnostic(hidingReason, advisor);
-                            }
-                            isResult.advise(hidingReason, advisor);
-                        });
+                    .filter(advisor->compatible(advisor, context))
+                    .forEach(advisor->{
+                        _Strings.nonEmpty(advisor.hides(visibilityContext))
+                            .map(Consent.VetoReason::explicit)
+                            .ifPresent(hidingReason->{
+                                
if(ifHiddenPolicy.isShowAsDisabledWithDiagnostics()) {
+                                    hidingReason = 
VetoUtil.withAdvisorAsDiagnostic(hidingReason, advisor);
+                                }
+                                builder.addAdvise(hidingReason, advisor);
+                            });
+                    });
                 break;
         }
 
         var ifDisabledPolicy = context.renderPolicy().ifDisabledPolicy();
         facetHolder.streamFacets(DisablingInteractionAdvisor.class)
-        .filter(advisor->compatible(advisor, context))
-        .forEach(advisor->{
-            Consent.VetoReason disablingReason = 
advisor.disables(context).orElse(null);
-            if(disablingReason != null
-                    && ifDisabledPolicy.isShowAsDisabledWithDiagnostics()) {
-                disablingReason = 
VetoUtil.withAdvisorAsDiagnostic(disablingReason, advisor);
-            }
-            isResult.advise(disablingReason, advisor);
-        });
-
-        return isResult;
+            .filter(advisor->compatible(advisor, context))
+            .forEach(advisor->{
+                advisor.disables(context)
+                    .ifPresent(disablingReason->{
+                        if(ifDisabledPolicy.isShowAsDisabledWithDiagnostics()) 
{
+                            disablingReason = 
VetoUtil.withAdvisorAsDiagnostic(disablingReason, advisor);
+                        }
+                        builder.addAdvise(disablingReason, advisor);
+                    });
+            });
+
+        return builder.build();
     }
 
     public InteractionResult isValidResult(final FacetHolder facetHolder, 
final ValidityContext context) {
 
-        var iaResult = new InteractionResult(context.createInteractionEvent());
+        var builder = 
InteractionResult.builder(context.createInteractionEvent());
 
         facetHolder.streamFacets(ValidatingInteractionAdvisor.class)
         .filter(advisor->compatible(advisor, context))
         .forEach(advisor->{
-            var invalidatingReasonString =
-                    
guardAgainstEmptyReasonString(advisor.invalidates(context), 
context.identifier());
-
-            var invalidatingReason = 
Optional.ofNullable(invalidatingReasonString)
-                    .map(Consent.VetoReason::explicit)
-                    .orElse(null);
-            iaResult.advise(invalidatingReason, advisor);
+            guardAgainstEmptyReasonString(advisor.invalidates(context), 
context.identifier())
+                .map(Consent.VetoReason::explicit)
+                
.ifPresent(invalidatingReason->builder.addAdvise(invalidatingReason, advisor));
         });
 
-        return iaResult;
+        return builder.build();
     }
 
     public InteractionResultSet isValidResultSet(
@@ -158,7 +152,7 @@ public RenderPolicy renderPolicy(final ManagedObject 
ownerAdapter) {
      * we should generate a message,
      * explaining what was going wrong and hinting developers at a possible 
resolution
      */
-    private String guardAgainstEmptyReasonString(
+    private Optional<String> guardAgainstEmptyReasonString(
             final @Nullable String reason, final @NonNull Identifier 
identifier) {
         if("".equals(reason)) {
             var msg = 
ProgrammingModelConstants.MessageTemplate.INVALID_USE_OF_VALIDATION_SUPPORT_METHOD.builder()
@@ -166,9 +160,9 @@ private String guardAgainstEmptyReasonString(
                 .addVariable("memberName", identifier.memberLogicalName())
                 .buildMessage();
             log.error(msg);
-            return msg;
+            return Optional.of(msg);
         }
-        return reason;
+        return Optional.ofNullable(reason);
     }
 
     private static boolean compatible(final InteractionAdvisor advisor, final 
InteractionContext ic) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedFeature.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedFeature.java
index 3f6cb957341..883af795091 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedFeature.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedFeature.java
@@ -28,7 +28,8 @@
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 
-public interface ManagedFeature {
+sealed public interface ManagedFeature
+permits ManagedMember, ManagedParameter {
 
     Identifier getIdentifier();
 
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedMember.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedMember.java
index 9c6d29da56b..b782d649867 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedMember.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedMember.java
@@ -20,6 +20,8 @@
 
 import java.util.Optional;
 
+import org.jspecify.annotations.NonNull;
+
 import org.apache.causeway.applib.Identifier;
 import org.apache.causeway.applib.annotation.Where;
 import org.apache.causeway.commons.internal.base._Casts;
@@ -30,15 +32,15 @@
 import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
 
 import lombok.Getter;
-import org.jspecify.annotations.NonNull;
 import lombok.RequiredArgsConstructor;
 import lombok.Setter;
 import lombok.extern.log4j.Log4j2;
 
 @Log4j2
 @RequiredArgsConstructor
-public abstract class ManagedMember
-implements ManagedFeature {
+public sealed abstract class ManagedMember
+implements ManagedFeature
+permits ManagedAction, ManagedCollection, ManagedProperty {
 
     /**
      * Some representations may vary according to whether the member is to be 
represented for read
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedParameter.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedParameter.java
index 8e359d6e960..f3478560709 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedParameter.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedParameter.java
@@ -26,17 +26,18 @@
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
 
-public interface ManagedParameter
+public sealed interface ManagedParameter
 extends
     ManagedValue,
-    ManagedFeature {
-    
+    ManagedFeature
+permits ParameterNegotiationModel.ParameterModel {
+
     ObjectActionParameter metaModel();
     @Override default ObjectActionParameter getMetaModel() { return 
metaModel(); }
-    
+
     int paramIndex();
     ParameterNegotiationModel negotiationModel();
-    
+
     /**
      * @return non-empty if not usable/editable (meaning if read-only)
      */
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ParameterNegotiationModel.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ParameterNegotiationModel.java
index ed4de33f9d3..6dd112dc317 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ParameterNegotiationModel.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ParameterNegotiationModel.java
@@ -97,7 +97,7 @@ private ParameterNegotiationModel(
     }
 
     // -- ACTION SPECIFIC
-    
+
     public ObjectAction act() {
         return managedAction.getAction();
     }
@@ -120,7 +120,7 @@ public Can<ManagedObject> getParamValues() {
     InteractionHead interactionHead() {
         return managedAction.interactionHead();
     }
-    
+
     public ActionInteractionHead actionInteractionHead() {
         return managedAction.actionInteractionHead();
     }
@@ -344,8 +344,7 @@ public ParameterNegotiationModel withParamValue(final int 
parameterIndex, @NonNu
 
     // -- INTERNAL HOLDER OF PARAMETER BINDABLES
 
-    @Log4j2
-    private record ParameterModel(
+    @Log4j2 record ParameterModel(
             int paramIndex,
             @NonNull ObjectActionParameter metaModel,
             @NonNull ParameterNegotiationModel negotiationModel,
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmVisibilityUtils.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmVisibilityUtils.java
index 8b01347329d..4a27cffc8c7 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmVisibilityUtils.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmVisibilityUtils.java
@@ -109,7 +109,7 @@ public static boolean isVisible(
                 Where.OBJECT_FORMS);
 
         return InteractionUtils.isVisibleResult(spec, visibilityContext)
-                .isNotVetoing();
+                .isAllowing();
     }
 
     private static VisibilityContext createVisibleInteractionContext(
diff --git 
a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/consent/InteractionResultTest.java
 
b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/consent/InteractionResultTest.java
index 23217ecdada..007b0290755 100644
--- 
a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/consent/InteractionResultTest.java
+++ 
b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/consent/InteractionResultTest.java
@@ -18,77 +18,76 @@
  */
 package org.apache.causeway.core.metamodel.consent;
 
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import org.apache.causeway.applib.services.wrapper.events.InteractionEvent;
 import org.apache.causeway.core.metamodel.consent.Consent.VetoReason;
 
 class InteractionResultTest {
 
-    private InteractionResult result;
-
-    @BeforeEach
-    public void setUp() throws Exception {
-        result = new InteractionResult(null);
-    }
-
-    @AfterEach
-    public void tearDown() throws Exception {
-        result = null;
-    }
+    private InteractionResult.Builder builder = 
InteractionResult.builder(Mockito.mock(InteractionEvent.class));
 
     @Test
-    public void shouldHaveNullReasonWhenJustInstantiated() {
+    void shouldHaveNullReasonWhenJustInstantiated() {
+        var result = builder.build();
         assertEquals(null, extractReason(result));
     }
 
     @Test
-    public void shouldBeEmptyWhenJustInstantiated() {
+    void shouldBeEmptyWhenJustInstantiated() {
+        var result = builder.build();
         assertFalse(result.isVetoing());
-        assertTrue(result.isNotVetoing());
+        assertTrue(result.isAllowing());
     }
 
     @Test
-    public void shouldHaveNonNullReasonWhenAdvisedWithNonNull() {
-        result.advise(vetoReason("foo"), InteractionAdvisor.forTesting());
+    void shouldHaveNonNullReasonWhenAdvisedWithNonNull() {
+        advise(vetoReason("foo"), InteractionAdvisor.forTesting());
+        var result = builder.build();
         assertEquals("foo", extractReason(result));
     }
 
     @Test
-    public void shouldConcatenateAdviseWhenAdvisedWithNonNull() {
-        result.advise(vetoReason("foo"), InteractionAdvisor.forTesting());
-        result.advise(vetoReason("bar"), InteractionAdvisor.forTesting());
+    void shouldConcatenateAdviseWhenAdvisedWithNonNull() {
+        advise(vetoReason("foo"), InteractionAdvisor.forTesting());
+        advise(vetoReason("bar"), InteractionAdvisor.forTesting());
+        var result = builder.build();
         assertEquals("foo; bar", extractReason(result));
     }
 
     @Test
-    public void shouldNotBeEmptyWhenAdvisedWithNonNull() {
-        result.advise(vetoReason("foo"), InteractionAdvisor.forTesting());
+    void shouldNotBeEmptyWhenAdvisedWithNonNull() {
+        advise(vetoReason("foo"), InteractionAdvisor.forTesting());
+        var result = builder.build();
         assertTrue(result.isVetoing());
-        assertFalse(result.isNotVetoing());
+        assertFalse(result.isAllowing());
     }
 
     @Test
-    public void shouldBeEmptyWhenAdvisedWithNull() {
-        result.advise(null, InteractionAdvisor.forTesting());
-        assertTrue(result.isNotVetoing());
-        assertFalse(result.isVetoing());
-        assertEquals(null, extractReason(result));
+    void shouldThrowWhenAdvisedWithNull() {
+        assertThrowsExactly(NullPointerException.class, ()->advise(null, 
InteractionAdvisor.forTesting()));
+        assertThrowsExactly(NullPointerException.class, 
()->advise(vetoReason("foo"), null));
     }
 
     // -- HELPER
 
+    private void advise(VetoReason vetoReason, InteractionAdvisor forTesting) {
+        builder.addAdvise(vetoReason, forTesting);
+    }
+
+
     static Consent.VetoReason vetoReason(final String reasonString) {
         return Consent.VetoReason.explicit(reasonString);
     }
 
     static String extractReason(final InteractionResult result) {
-        return result.getReason().map(VetoReason::string).orElse(null);
+        return result.vetoReason().map(VetoReason::string).orElse(null);
     }
 
 }
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
index 29af1c124fd..2b1f34deea9 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
@@ -100,7 +100,6 @@ public Object invoke(WrapperInvocation wrapperInvocation) 
throws Throwable {
 
         final Object target = wrapperInvocation.origin().pojo();
         final Method method = wrapperInvocation.method();
-        final ManagedObject managedMixee = 
wrapperInvocation.origin().managedMixee();
 
         if (classMetaData().isObjectMethod(method)
                 || isEnhancedEntityMethod(method)) {
@@ -133,52 +132,48 @@ public Object invoke(WrapperInvocation wrapperInvocation) 
throws Throwable {
         var objectMember = 
targetAdapter.objSpec().getMemberElseFail(resolvedMethod);
         var intent = ImperativeFacet.getIntent(objectMember, resolvedMethod);
         if(intent == Intent.CHECK_IF_HIDDEN || intent == 
Intent.CHECK_IF_DISABLED) {
-            throw new UnsupportedOperationException(String.format("Cannot 
invoke supporting method '%s'", objectMember.getId()));
+            throw _Exceptions.unsupportedOperation("Cannot invoke supporting 
method '%s'", objectMember.getId());
         }
 
         if (intent == Intent.DEFAULTS || intent == 
Intent.CHOICES_OR_AUTOCOMPLETE) {
             return method.invoke(target, wrapperInvocation.args());
         }
 
-        if (objectMember.isOneToOneAssociation()) {
+        if (objectMember instanceof OneToOneAssociation prop) {
 
             if (intent == Intent.CHECK_IF_VALID || intent == 
Intent.MODIFY_PROPERTY_SUPPORTING) {
-                throw new UnsupportedOperationException(String.format("Cannot 
invoke supporting method for '%s'; use only property accessor/mutator", 
objectMember.getId()));
+                throw _Exceptions.unsupportedOperation("Cannot invoke 
supporting method for '%s'; use only property accessor/mutator", 
objectMember.getId());
             }
 
-            final OneToOneAssociation otoa = (OneToOneAssociation) 
objectMember;
-
             if (intent == Intent.ACCESSOR) {
-                return handleGetterMethodOnProperty(wrapperInvocation, 
targetAdapter, otoa);
+                return handleGetterMethodOnProperty(wrapperInvocation, 
targetAdapter, prop);
             }
 
             if (intent == Intent.MODIFY_PROPERTY || intent == 
Intent.INITIALIZATION) {
-                return handleSetterMethodOnProperty(wrapperInvocation, 
targetAdapter, otoa);
+                return handleSetterMethodOnProperty(wrapperInvocation, 
targetAdapter, prop);
             }
         }
-        if (objectMember.isOneToManyAssociation()) {
+        if (objectMember instanceof OneToManyAssociation coll) {
 
             if (intent == Intent.CHECK_IF_VALID) {
-                throw new UnsupportedOperationException(String.format("Cannot 
invoke supporting method '%s'; use only collection accessor/mutator", 
objectMember.getId()));
+                throw _Exceptions.unsupportedOperation("Cannot invoke 
supporting method '%s'; use only collection accessor/mutator", 
objectMember.getId());
             }
 
-            final OneToManyAssociation otma = (OneToManyAssociation) 
objectMember;
             if (intent == Intent.ACCESSOR) {
-                return handleGetterMethodOnCollection(wrapperInvocation, 
targetAdapter, otma, objectMember.getId());
+                return handleGetterMethodOnCollection(wrapperInvocation, 
targetAdapter, coll, objectMember.getId());
             }
         }
 
         if (objectMember instanceof ObjectAction objectAction) {
 
             if (intent == Intent.CHECK_IF_VALID) {
-                throw new UnsupportedOperationException(String.format("Cannot 
invoke supporting method '%s'; use only the 'invoke' method", 
objectMember.getId()));
+                throw _Exceptions.unsupportedOperation("Cannot invoke 
supporting method '%s'; use only the 'invoke' method", objectMember.getId());
             }
 
             if(targetAdapter.objSpec().isMixin()) {
+                final ManagedObject managedMixee = 
wrapperInvocation.origin().managedMixee();
                 if (managedMixee == null) {
-                    throw _Exceptions.illegalState(
-                            "Missing the required managedMixee for action 
'%s'",
-                            objectAction.getId());
+                    throw _Exceptions.illegalState("Missing the required 
managedMixee for action '%s'", objectAction.getId());
                 }
                 MmAssertionUtils.assertIsBookmarkSupported(managedMixee);
 
@@ -197,8 +192,7 @@ public Object invoke(WrapperInvocation wrapperInvocation) 
throws Throwable {
                         return 
handleGetterMethodOnCollection(wrapperInvocation, managedMixee, 
(OneToManyAssociation)mixinMember, objectMember.getId());
                     }
                 } else {
-                    throw _Exceptions.illegalState(String.format(
-                            "Could not locate mixin member for action '%s' on 
spec '%s'", objectAction.getId(), targetAdapter.objSpec()));
+                    throw _Exceptions.illegalState("Could not locate mixin 
member for action '%s' on spec '%s'", objectAction.getId(), 
targetAdapter.objSpec());
                 }
             }
 
@@ -206,16 +200,15 @@ public Object invoke(WrapperInvocation wrapperInvocation) 
throws Throwable {
             return handleActionMethod(wrapperInvocation, targetAdapter, 
objectAction);
         }
 
-        throw new UnsupportedOperationException(String.format("Unknown member 
type '%s'", objectMember));
+        throw _Exceptions.unsupportedOperation("Unknown member type '%s'", 
objectMember);
     }
 
     private static ObjectMember determineMixinMember(
             final ManagedObject domainObjectAdapter,
             final ObjectAction objectAction) {
 
-        if(domainObjectAdapter == null) {
-            return null;
-        }
+        if(domainObjectAdapter == null) return null;
+
         var specification = domainObjectAdapter.objSpec();
         var objectActions = specification.streamAnyActions(MixedIn.INCLUDED);
         var objectAssociations = 
specification.streamAssociations(MixedIn.INCLUDED);
@@ -368,8 +361,7 @@ private Object handleGetterMethodOnCollection(
                 return mapViewObject;
             }
 
-            var msg = String.format("Collection type '%s' not supported by 
framework", currentReferencedObj.getClass().getName());
-            throw new IllegalArgumentException(msg);
+            throw _Exceptions.illegalArgument("Collection type '%s' not 
supported by framework", currentReferencedObj.getClass().getName());
         }, ()->new ExceptionLogger("getter " + collection.getId(), 
targetAdapter));
     }
 
@@ -481,7 +473,7 @@ private void checkUsability(
     // -- NOTIFY LISTENERS
 
     private void notifyListenersAndVetoIfRequired(final InteractionResult 
interactionResult) {
-        var interactionEvent = interactionResult.getInteractionEvent();
+        var interactionEvent = interactionResult.interactionEvent();
 
         mmc().getWrapperFactory().notifyListeners(interactionEvent);
         if (interactionEvent.isVeto()) {
@@ -578,8 +570,7 @@ private Object handleException(
 
     private Object singleArgUnderlyingElseNull(final Object[] args, final 
String name) {
         if (args.length != 1) {
-            throw new IllegalArgumentException(String.format(
-                    "Invoking '%s' should only have a single argument", name));
+            throw _Exceptions.illegalArgument("Invoking '%s' should only have 
a single argument", name);
         }
         var argumentObj = underlying(args[0]);
         return argumentObj;
@@ -587,8 +578,7 @@ private Object singleArgUnderlyingElseNull(final Object[] 
args, final String nam
 
     private void zeroArgsElseThrow(final Object[] args, final String name) {
         if (!_NullSafe.isEmpty(args)) {
-            throw new IllegalArgumentException(String.format(
-                    "Invoking '%s' should have no arguments", name));
+            throw _Exceptions.illegalArgument("Invoking '%s' should have no 
arguments", name);
         }
     }
 
@@ -598,13 +588,7 @@ String msg(Exception ex) {
             String id = mo.isBookmarkMemoized()
                     ? mo.getBookmarkElseFail().identifier()
                     : "<bookmark not memoized>";
-            var buf = new StringBuilder("Failed to execute ").append(" 
").append(what).append(" ");
-            buf.append(" on '")
-                    .append(logicalType.logicalName())
-                    .append(":")
-                    .append(id)
-                    .append("'");
-            return buf.toString();
+            return "Failed to execute %s on '%s:%s'".formatted(what, 
logicalType.logicalName(), id);
         }
     }
 


Reply via email to