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


The following commit(s) were added to refs/heads/main by this push:
     new 1ce1db080b5 CAUSEWAY-3859: Java record refactoring (part 22)
1ce1db080b5 is described below

commit 1ce1db080b5e1026fe707ce54cb1fcc523705452
Author: Andi Huber <[email protected]>
AuthorDate: Thu Feb 20 08:33:48 2025 +0100

    CAUSEWAY-3859: Java record refactoring (part 22)
---
 .../internal/binding/_BindableAbstract.java        |  12 +-
 .../interactions/managed/ManagedParameter.java     |  47 +---
 .../managed/ParameterNegotiationModel.java         | 261 ++++++++++++---------
 .../managed/PendingParamsSnapshot.java             |   2 +-
 .../interactions/managed/_BindingUtil.java         |  92 ++++----
 .../core/metamodel/object/MmDebugUtils.java        |   2 +-
 .../tabular/simple/DataTableSerializationTest.java |  59 +++--
 .../interaction/DomainObjectTesterFactory.java     |  22 +-
 8 files changed, 266 insertions(+), 231 deletions(-)

diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/binding/_BindableAbstract.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/binding/_BindableAbstract.java
index e0425df0e31..63f91fa7d48 100644
--- 
a/commons/src/main/java/org/apache/causeway/commons/internal/binding/_BindableAbstract.java
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/binding/_BindableAbstract.java
@@ -45,7 +45,7 @@
 public abstract class _BindableAbstract<T> implements Bindable<T> {
 
     private T value;
-    private Observable<? extends T> observable = null;;
+    private Observable<? extends T> observable = null;
     private InvalidationListener invalidationListener = null;
     private boolean valid = true;
     private InternalUtil<T> util = null;
@@ -178,9 +178,8 @@ public <R> Bindable<R> mapToBindable(
 
         var newBindable = 
_Bindables.<R>forValue(forwardMapper.apply(getValue()));
         addListener((e,o,n)->{
-            if(isReverseUpdating.get()) {
-                return;
-            }
+            if(isReverseUpdating.get()) return;
+
             try {
                 isForwardUpdating.set(true);
                 newBindable.setValue(forwardMapper.apply(n));
@@ -190,9 +189,8 @@ public <R> Bindable<R> mapToBindable(
         });
 
         newBindable.addListener((e,o,n)->{
-            if(isForwardUpdating.get()) {
-                return;
-            }
+            if(isForwardUpdating.get()) return;
+
             try {
                 isReverseUpdating.set(true);
                 setValue(reverseMapper.apply(n));
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 6c7e285fd9a..8e359d6e960 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
@@ -20,49 +20,26 @@
 
 import java.util.Optional;
 
+import org.jspecify.annotations.NonNull;
+
 import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
-import org.apache.causeway.core.metamodel.consent.Veto;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
 
-import org.jspecify.annotations.NonNull;
-import lombok.extern.log4j.Log4j2;
-
-@Log4j2
-public abstract class ManagedParameter
-implements
+public interface ManagedParameter
+extends
     ManagedValue,
     ManagedFeature {
-
-    public abstract int getParamNr();
-    @Override public abstract ObjectActionParameter getMetaModel();
-    public abstract ParameterNegotiationModel getNegotiationModel();
-
+    
+    ObjectActionParameter metaModel();
+    @Override default ObjectActionParameter getMetaModel() { return 
metaModel(); }
+    
+    int paramIndex();
+    ParameterNegotiationModel negotiationModel();
+    
     /**
-     * @param params
      * @return non-empty if not usable/editable (meaning if read-only)
      */
-    public final Optional<InteractionVeto> checkUsability(final @NonNull 
Can<ManagedObject> params) {
-
-        try {
-            var head = getNegotiationModel().getHead();
-
-            var usabilityConsent =
-                    getMetaModel()
-                    .isUsable(head, params, InteractionInitiatedBy.USER);
-
-            return usabilityConsent.isVetoed()
-                    ? Optional.of(InteractionVeto.readonly(usabilityConsent))
-                    : Optional.empty();
-
-        } catch (final Exception ex) {
-
-            log.warn(ex.getLocalizedMessage(), ex);
-            return Optional.of(InteractionVeto.readonly(new Veto("failure 
during usability evaluation")));
-
-        }
-
-    }
+    Optional<InteractionVeto> checkUsability(@NonNull Can<ManagedObject> 
params);
 
 }
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 5d1f3e81dcd..ea19b022518 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
@@ -24,12 +24,14 @@
 import java.util.function.UnaryOperator;
 import java.util.stream.IntStream;
 
+import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.Identifier;
 import org.apache.causeway.commons.binding.Bindable;
 import org.apache.causeway.commons.binding.Observable;
 import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.internal.base._Lazy;
 import org.apache.causeway.commons.internal.binding._BindableAbstract;
 import org.apache.causeway.commons.internal.binding._Bindables;
 import org.apache.causeway.commons.internal.binding._Bindables.BooleanBindable;
@@ -38,6 +40,7 @@
 import org.apache.causeway.core.metamodel.consent.Consent;
 import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.causeway.core.metamodel.consent.InteractionResult;
+import org.apache.causeway.core.metamodel.consent.Veto;
 import 
org.apache.causeway.core.metamodel.interactions.managed._BindingUtil.TargetFormat;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
@@ -45,8 +48,7 @@
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
 
-import lombok.Getter;
-import org.jspecify.annotations.NonNull;
+import lombok.extern.log4j.Log4j2;
 
 /**
  * Model used to negotiate the parameter values of an action by means of an UI 
dialog.
@@ -133,7 +135,7 @@ public void setParamValues(final @NonNull 
Can<ManagedObject> paramValues) {
         var valueIterator = paramValues.iterator();
         paramModels.forEach(paramModel->{
             if(!valueIterator.hasNext()) return;
-            paramModel.getBindableParamValue().setValue(valueIterator.next());
+            paramModel.bindableParamValue().setValue(valueIterator.next());
         });
     }
 
@@ -148,19 +150,19 @@ public void setParamValues(final @NonNull 
Can<ManagedObject> paramValues) {
     }
 
     @NonNull public Bindable<ManagedObject> getBindableParamValue(final int 
paramNr) {
-        return paramModels.getElseFail(paramNr).getBindableParamValue();
+        return paramModels.getElseFail(paramNr).bindableParamValue();
     }
 
     @NonNull public BooleanBindable getBindableParamValueDirtyFlag(final int 
paramNr) {
-        return 
paramModels.getElseFail(paramNr).getBindableParamValueDirtyFlag();
+        return paramModels.getElseFail(paramNr).bindableParamValueDirtyFlag();
     }
 
     @NonNull public Observable<Can<ManagedObject>> 
getObservableParamChoices(final int paramNr) {
-        return paramModels.getElseFail(paramNr).getObservableParamChoices();
+        return paramModels.getElseFail(paramNr).observableParamChoices();
     }
 
     @NonNull public Observable<String> getObservableParamValidation(final int 
paramNr) {
-        return paramModels.getElseFail(paramNr).getObservableParamValidation();
+        return paramModels.getElseFail(paramNr).observableParamValidation();
     }
 
     /**
@@ -168,20 +170,26 @@ public void setParamValues(final @NonNull 
Can<ManagedObject> paramValues) {
      * (Ignoring the {@link ParameterModel#isValidationFeedbackActive()} flag.)
      * @apiNote introduced for [CAUSEWAY-3753] - not sure why required.
      */
-    @NonNull public String validateImmediately(final int paramNr) {
-        return paramModels.getElseFail(paramNr).validateImmediately();
+    @NonNull public String validateImmediately(final int paramIndex) {
+        return getHead().getMetaModel().getParameterByIndex(paramIndex)
+                .isValid(
+                        getHead(),
+                        getParamValues(),
+                        InteractionInitiatedBy.USER)
+                .getReasonAsString()
+                .orElse(null);
     }
 
     @NonNull public Bindable<String> getBindableParamSearchArgument(final int 
paramNr) {
-        return 
paramModels.getElseFail(paramNr).getBindableParamSearchArgument();
+        return paramModels.getElseFail(paramNr).bindableParamSearchArgument();
     }
 
     @NonNull public Observable<Consent> getObservableVisibilityConsent(final 
int paramNr) {
-        return 
paramModels.getElseFail(paramNr).getObservableVisibilityConsent();
+        return paramModels.getElseFail(paramNr).observableVisibilityConsent();
     }
 
     @NonNull public Observable<Consent> getObservableUsabilityConsent(final 
int paramNr) {
-        return 
paramModels.getElseFail(paramNr).getObservableUsabilityConsent();
+        return paramModels.getElseFail(paramNr).observableUsabilityConsent();
     }
 
     @NonNull public Consent getVisibilityConsent(final int paramNr) {
@@ -232,13 +240,13 @@ public void setParamValue(final int paramIndex, final 
@Nullable ManagedObject ne
         if (ManagedObjects.isNullOrUnspecifiedOrEmpty(newParamValue)) {
             clearParamValue(paramIndex);
         } else {
-            
paramModels.getElseFail(paramIndex).getBindableParamValue().setValue(newParamValue);
+            
paramModels.getElseFail(paramIndex).bindableParamValue().setValue(newParamValue);
         }
     }
 
     public void clearParamValue(final int paramIndex) {
         var emptyValue = adaptParamValuePojo(paramIndex, null);
-        
paramModels.getElseFail(paramIndex).getBindableParamValue().setValue(emptyValue);
+        
paramModels.getElseFail(paramIndex).bindableParamValue().setValue(emptyValue);
     }
 
     /**
@@ -247,7 +255,7 @@ public void clearParamValue(final int paramIndex) {
      *      and returns the new parameter argument value also wrapped as 
{@link ManagedObject}
      */
     public void updateParamValue(final int paramIndex, final @NonNull 
UnaryOperator<ManagedObject> updater) {
-        var bindableParamValue = 
paramModels.getElseFail(paramIndex).getBindableParamValue();
+        var bindableParamValue = 
paramModels.getElseFail(paramIndex).bindableParamValue();
         var newParamValue = updater.apply(bindableParamValue.getValue());
         if (ManagedObjects.isNullOrUnspecifiedOrEmpty(newParamValue)) {
             clearParamValue(paramIndex);
@@ -320,92 +328,127 @@ public PendingParamsSnapshot createSnapshotModel() {
     }
 
     /**
-     * Returns a copy, but with a single param value replaced. 
+     * Returns a copy, but with a single param value replaced.
      */
-    public ParameterNegotiationModel withParamValue(int parameterIndex, 
@NonNull ManagedObject paramValue) {
+    public ParameterNegotiationModel withParamValue(final int parameterIndex, 
@NonNull final ManagedObject paramValue) {
         return ParameterNegotiationModel.of(managedAction, 
getParamValues().replace(parameterIndex, paramValue));
     }
-    
+
     // -- INTERNAL HOLDER OF PARAMETER BINDABLES
 
-    private static class ParameterModel extends ManagedParameter {
-
-        @Getter(onMethod_ = {@Override}) private final int paramNr;
-        @Getter(onMethod_ = {@Override}) @NonNull private final 
ObjectActionParameter metaModel;
-        @Getter(onMethod_ = {@Override}) @NonNull private final 
ParameterNegotiationModel negotiationModel;
-        @Getter @NonNull private final _BindableAbstract<ManagedObject> 
bindableParamValue;
-        @Getter @NonNull private final BooleanBindable 
bindableParamValueDirtyFlag;
-        @Getter @NonNull private final LazyObservable<String> 
observableParamValidation;
-        @Getter @NonNull private final _BindableAbstract<String> 
bindableParamSearchArgument;
-        @Getter @NonNull private final LazyObservable<Can<ManagedObject>> 
observableParamChoices;
-        @Getter @NonNull private final LazyObservable<Consent> 
observableVisibilityConsent;
-        @Getter @NonNull private final LazyObservable<Consent> 
observableUsabilityConsent;
-        private Observable<String> bindableParamAsTitle;
-        private Observable<String> bindableParamAsHtml;
-        private Bindable<String> bindableParamAsParsableText;
+    @Log4j2
+    private record ParameterModel(
+            int paramIndex,
+            @NonNull ObjectActionParameter metaModel,
+            @NonNull ParameterNegotiationModel negotiationModel,
+            @NonNull _BindableAbstract<ManagedObject> bindableParamValue,
+            @NonNull BooleanBindable bindableParamValueDirtyFlag,
+            @NonNull _BindableAbstract<String> bindableParamSearchArgument,
+            @NonNull LazyObservable<String> observableParamValidation,
+            @NonNull LazyObservable<Can<ManagedObject>> observableParamChoices,
+            @NonNull LazyObservable<Consent> observableVisibilityConsent,
+            @NonNull LazyObservable<Consent> observableUsabilityConsent,
+            @NonNull Observable<String> observableParamAsTitle,
+            @NonNull Observable<String> observableParamAsHtml,
+            @NonNull _Lazy<Bindable<String>> bindableParamAsParsableTextLazy
+        ) implements ManagedParameter {
 
         private ParameterModel(
-                final int paramNr,
-                final @NonNull ParameterNegotiationModel negotiationModel,
-                final @NonNull ManagedObject initialValue) {
-
-            var action = negotiationModel.getHead().getMetaModel();
+            final int paramIndex,
+            final @NonNull ParameterNegotiationModel negotiationModel,
+            final @NonNull ManagedObject initialValue) {
+            this(paramIndex, 
negotiationModel.getHead().getMetaModel().getParameterByIndex(paramIndex), 
negotiationModel,
+                // bindableParamValue
+                _Bindables.forValue(initialValue),
+                // bindableParamValueDirtyFlag
+                _Bindables.forBoolean(false),
+                // bindableParamSearchArgument
+                _Bindables.forValue(null),
+                // unused in canonical constructor  ..
+                null, null, null, null, null, null, null);
+        }
 
-            this.paramNr = paramNr;
-            this.metaModel = action.getParameters().getElseFail(paramNr);
+        // canonical constructor
+        ParameterModel(
+            final int paramIndex,
+            @NonNull final ObjectActionParameter metaModel,
+            @NonNull final ParameterNegotiationModel negotiationModel,
+            @NonNull final _BindableAbstract<ManagedObject> bindableParamValue,
+            @NonNull final BooleanBindable bindableParamValueDirtyFlag,
+            @NonNull final _BindableAbstract<String> 
bindableParamSearchArgument,
+            // unused ..
+            final LazyObservable<String> observableParamValidation,
+            final LazyObservable<Can<ManagedObject>> observableParamChoices,
+            final LazyObservable<Consent> observableVisibilityConsent,
+            final LazyObservable<Consent> observableUsabilityConsent,
+            final Observable<String> observableParamAsTitle,
+            final Observable<String> observableParamAsHtml,
+            final _Lazy<Bindable<String>> bindableParamAsParsableTextLazy
+        ) {
+            this.paramIndex = paramIndex;
+            this.metaModel = metaModel;
             this.negotiationModel = negotiationModel;
 
-            bindableParamValue = _Bindables.forValue(initialValue);
-            bindableParamValueDirtyFlag = _Bindables.forBoolean(false);
+            this.bindableParamValue = bindableParamValue;
+            this.bindableParamValueDirtyFlag = bindableParamValueDirtyFlag;
+            this.bindableParamSearchArgument = bindableParamSearchArgument;
 
-            //bindableParamValue.setValueRefiner(MmEntityUtil::refetch); no 
longer used
-            
bindableParamValue.setValueGuard(MmAssertionUtils.assertInstanceOf(metaModel.getElementType()));
-            bindableParamValue.addListener((event, oldValue, newValue)->{
+            
bindableParamValue().setValueGuard(MmAssertionUtils.assertInstanceOf(metaModel().getElementType()));
+            bindableParamValue().addListener((event, oldValue, newValue)->{
                 if(newValue==null) {
                     // lift null to empty ...
-                    bindableParamValue.setValue(metaModel.getEmpty()); // 
triggers this event again
+                    bindableParamValue().setValue(metaModel().getEmpty()); // 
triggers this event again
                     return;
                 }
-                getNegotiationModel().onNewParamValue();
-                bindableParamValueDirtyFlag.setValue(true); // set dirty 
whenever an update event happens
+                negotiationModel().onNewParamValue();
+                bindableParamValueDirtyFlag().setValue(true); // set dirty 
whenever an update event happens
             });
 
             // has either autoComplete, choices, or none
-            observableParamChoices = metaModel.hasAutoComplete()
-            ? _Observables.lazy(()->
-                getMetaModel().getAutoComplete(
-                        getNegotiationModel(),
-                        getBindableParamSearchArgument().getValue(),
-                        InteractionInitiatedBy.USER))
-            : metaModel.hasChoices()
+            this.observableParamChoices = metaModel().hasAutoComplete()
                 ? _Observables.lazy(()->
-                    getMetaModel().getChoices(getNegotiationModel(), 
InteractionInitiatedBy.USER))
-                : _Observables.lazy(Can::empty);
+                    metaModel().getAutoComplete(
+                            negotiationModel(),
+                            bindableParamSearchArgument().getValue(),
+                            InteractionInitiatedBy.USER))
+                : metaModel().hasChoices()
+                    ? _Observables.lazy(()->
+                        getMetaModel().getChoices(negotiationModel(), 
InteractionInitiatedBy.USER))
+                    : _Observables.lazy(Can::empty);
 
             // if has autoComplete, then activate the search argument
-            bindableParamSearchArgument = _Bindables.forValue(null);
-            if(metaModel.hasAutoComplete()) {
-                bindableParamSearchArgument.addListener((e,o,n)->{
-                    observableParamChoices.invalidate();
+            if(metaModel().hasAutoComplete()) {
+                this.bindableParamSearchArgument.addListener((e,o,n)->{
+                    observableParamChoices().invalidate();
                 });
             }
 
             // validate this parameter, but only when validationFeedback has 
been activated
-            observableParamValidation = _Observables.lazy(()->
+            this.observableParamValidation = _Observables.lazy(()->
                 isValidationFeedbackActive()
-                    ? validateImmediately()
+                    ? negotiationModel().validateImmediately(paramIndex)
                     : (String)null);
 
-            observableVisibilityConsent = _Observables.lazy(()->
-                metaModel.isVisible(
-                        negotiationModel.getHead(),
-                        negotiationModel.getParamValues(),
+            this.observableVisibilityConsent = _Observables.lazy(()->
+                metaModel().isVisible(
+                        negotiationModel().getHead(),
+                        negotiationModel().getParamValues(),
                         InteractionInitiatedBy.USER));
-            observableUsabilityConsent = _Observables.lazy(()->
-                metaModel.isUsable(
-                        negotiationModel.getHead(),
-                        negotiationModel.getParamValues(),
+            this.observableUsabilityConsent = _Observables.lazy(()->
+                metaModel().isUsable(
+                        negotiationModel().getHead(),
+                        negotiationModel().getParamValues(),
                         InteractionInitiatedBy.USER));
+
+            // value types should have associated rederers via value semantics
+            this.observableParamAsTitle = _BindingUtil
+                    .bindAsFormated(TargetFormat.TITLE, metaModel(), 
bindableParamValue());
+            this.observableParamAsHtml = _BindingUtil
+                    .bindAsFormated(TargetFormat.HTML, metaModel(), 
bindableParamValue());
+            // value types should have associated parsers/formatters via value 
semantics
+            // except for composite value types, which might have not
+            this.bindableParamAsParsableTextLazy = 
_Lazy.threadSafe(()->(Bindable<String>) _BindingUtil
+                    .bindAsFormated(TargetFormat.PARSABLE_TEXT, metaModel(), 
bindableParamValue()));
         }
 
         public void invalidateChoicesAndValidation() {
@@ -419,7 +462,7 @@ public void invalidateVisibilityAndUsability() {
         }
 
         private boolean isValidationFeedbackActive() {
-            return 
getNegotiationModel().getObservableValidationFeedbackActive().getValue();
+            return 
negotiationModel().getObservableValidationFeedbackActive().getValue();
         }
 
         // -- MANAGED PARAMETER
@@ -452,22 +495,12 @@ public Bindable<ManagedObject> getValue() {
 
         @Override
         public Observable<String> getValueAsTitle() {
-            if(bindableParamAsTitle==null) {
-                // value types should have associated rederers via value 
semantics
-                bindableParamAsTitle = _BindingUtil
-                        .bindAsFormated(TargetFormat.TITLE, metaModel, 
bindableParamValue);
-            }
-            return bindableParamAsTitle;
+            return observableParamAsTitle;
         }
 
         @Override
         public Observable<String> getValueAsHtml() {
-            if(bindableParamAsHtml==null) {
-                // value types should have associated rederers via value 
semantics
-                bindableParamAsHtml = _BindingUtil
-                        .bindAsFormated(TargetFormat.HTML, metaModel, 
bindableParamValue);
-            }
-            return bindableParamAsHtml;
+            return observableParamAsHtml;
         }
 
         @Override
@@ -477,13 +510,7 @@ public boolean isValueAsParsableTextSupported() {
 
         @Override
         public Bindable<String> getValueAsParsableText() {
-            if(bindableParamAsParsableText==null) {
-                // value types should have associated parsers/formatters via 
value semantics
-                // except for composite value types, which might have not
-                bindableParamAsParsableText = (Bindable<String>) _BindingUtil
-                        .bindAsFormated(TargetFormat.PARSABLE_TEXT, metaModel, 
bindableParamValue);
-            }
-            return bindableParamAsParsableText;
+            return bindableParamAsParsableTextLazy.get();
         }
 
         @Override
@@ -501,20 +528,44 @@ public Observable<Can<ManagedObject>> getChoices() {
             return observableParamChoices;
         }
 
-        // -- HELPER
-
-        /**
-         * Calls the underlying action parameter validation logic, for pending 
arguments.
-         * (Ignoring the {@link #isValidationFeedbackActive()} flag.)
-         */
-        private String validateImmediately() {
-            return metaModel
-                    .isValid(
-                            getNegotiationModel().getHead(),
-                            getNegotiationModel().getParamValues(),
-                            InteractionInitiatedBy.USER)
-                    .getReasonAsString()
-                    .orElse(null);
+        @Override
+        public Optional<InteractionVeto> checkUsability(@NonNull final 
Can<ManagedObject> params) {
+
+            try {
+                var head = negotiationModel().getHead();
+
+                var usabilityConsent =
+                    getMetaModel()
+                    .isUsable(head, params, InteractionInitiatedBy.USER);
+
+                return usabilityConsent.isVetoed()
+                    ? Optional.of(InteractionVeto.readonly(usabilityConsent))
+                    : Optional.empty();
+
+            } catch (final Exception ex) {
+                log.warn(ex.getLocalizedMessage(), ex);
+                return Optional.of(InteractionVeto.readonly(new Veto("failure 
during usability evaluation")));
+            }
+
+        }
+
+        // -- OBJECT CONTRACT
+
+        @Override
+        public final boolean equals(final Object obj) {
+            return obj instanceof ParameterModel other
+                ? Objects.equals(this.getIdentifier(), other.getIdentifier())
+                : false;
+        }
+
+        @Override
+        public final int hashCode() {
+            return Objects.hashCode(getIdentifier());
+        }
+
+        @Override
+        public final String toString() {
+            return "ParameterModel[id=%s]".formatted(getIdentifier());
         }
 
     }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/PendingParamsSnapshot.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/PendingParamsSnapshot.java
index e2fd4a0f8eb..dabe115db39 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/PendingParamsSnapshot.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/PendingParamsSnapshot.java
@@ -144,7 +144,7 @@ private Can<Bookmark> bookmark(
                         ()->String.format("Framework Bug: cardinality 
constraint mismatch on parameter %s",
                                 
paramModel.getMetaModel().getFeatureIdentifier()));
                 if(isPlural) {
-                    cardinalityConstraints[paramModel.getParamNr()] =
+                    cardinalityConstraints[paramModel.paramIndex()] =
                             
Objects.requireNonNull(((PackedManagedObject)paramValue).getLogicalType());
                 }
             }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/_BindingUtil.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/_BindingUtil.java
index f9a7b8b0e0b..f370462a077 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/_BindingUtil.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/_BindingUtil.java
@@ -18,22 +18,23 @@
  */
 package org.apache.causeway.core.metamodel.interactions.managed;
 
+import org.jspecify.annotations.NonNull;
+
 import org.apache.causeway.applib.value.semantics.Parser;
 import org.apache.causeway.applib.value.semantics.Renderer;
 import org.apache.causeway.applib.value.semantics.ValueSemanticsProvider;
+import org.apache.causeway.commons.binding.Bindable;
 import org.apache.causeway.commons.binding.Observable;
 import org.apache.causeway.commons.functional.Either;
 import org.apache.causeway.commons.internal.binding._BindableAbstract;
 import org.apache.causeway.commons.internal.binding._Bindables;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.core.metamodel.facetapi.FeatureType;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.MmUnwrapUtils;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
-import org.jspecify.annotations.NonNull;
 import lombok.experimental.UtilityClass;
 
 @UtilityClass
@@ -52,6 +53,10 @@ boolean requiresRenderer() {
         }
     }
 
+    /**
+     * For {@link TargetFormat#PARSABLE_TEXT} returns a {@link Bindable},
+     * otherwise just an {@link Observable}.
+     */
     @SuppressWarnings({ "rawtypes" })
     Observable<String> bindAsFormated(
             final @NonNull TargetFormat format,
@@ -62,50 +67,53 @@ Observable<String> bindAsFormated(
 
         // value types should have associated parsers/formatters via value 
semantics
         return spec.valueFacet()
-        .map(valueFacet->{
-            var eitherRendererOrParser = format.requiresRenderer()
-                ? Either.<Renderer, 
Parser>left(valueFacet.selectRendererForParamOrPropOrCollOrElseFallback(prop))
-                : Either.<Renderer, 
Parser>right(valueFacet.selectParserForAttributeOrElseFallback(prop));
-            var ctx = valueFacet.createValueSemanticsContext(prop);
-
-            return bindAsFormated(format, spec, bindablePropertyValue, 
eitherRendererOrParser, ctx);
-        })
-        .orElseGet(()->
-            // fallback Bindable that is floating free (unbound)
-            // writing to it has no effect on the domain
-            _Bindables.forValue(String.format("Could not find a ValueFacet for 
type %s",
-                    spec.logicalType()))
-        );
-
+            .map(valueFacet->{
+                var eitherRendererOrParser = format.requiresRenderer()
+                    ? Either.<Renderer, 
Parser>left(valueFacet.selectRendererForParamOrPropOrCollOrElseFallback(prop))
+                    : Either.<Renderer, 
Parser>right(valueFacet.selectParserForAttributeOrElseFallback(prop));
+                var ctx = valueFacet.createValueSemanticsContext(prop);
+
+                return bindAsFormated(format, spec, bindablePropertyValue, 
eitherRendererOrParser, ctx);
+            })
+            .orElseGet(()->
+                // fallback Bindable that is floating free (unbound)
+                // writing to it has no effect on the domain
+                _Bindables.forValue(String.format("Could not find a ValueFacet 
for type %s",
+                        spec.logicalType()))
+            );
     }
 
+    /**
+     * For {@link TargetFormat#PARSABLE_TEXT} returns a {@link Bindable},
+     * otherwise just an {@link Observable}.
+     */
     @SuppressWarnings({ "rawtypes" })
     Observable<String> bindAsFormated(
             final @NonNull TargetFormat format,
             final @NonNull ObjectActionParameter param,
             final @NonNull _BindableAbstract<ManagedObject> 
bindableParamValue) {
 
-        guardAgainstNonScalarParam(param);
+        // non-scalar action parameters are neither parseable nor renderable
+        if(param.isPlural()) return _Bindables.forValue("multiple parameters");
 
         var spec = param.getElementType();
 
         // value types should have associated parsers/formatters via value 
semantics
         return spec.valueFacet()
-        .map(valueFacet->{
-            var eitherRendererOrParser = format.requiresRenderer()
-                ? Either.<Renderer, 
Parser>left(valueFacet.selectRendererForParamOrPropOrCollOrElseFallback(param))
-                : Either.<Renderer, 
Parser>right(valueFacet.selectParserForAttributeOrElseFallback(param));
-            var ctx = valueFacet.createValueSemanticsContext(param);
-
-            return bindAsFormated(format, spec, bindableParamValue, 
eitherRendererOrParser, ctx);
-        })
-        .orElseGet(()->
-            // fallback Bindable that is floating free (unbound)
-            // writing to it has no effect on the domain
-            _Bindables.forValue(String.format("Could not find a ValueFacet for 
type %s",
-                    spec.logicalType()))
-        );
-
+            .map(valueFacet->{
+                var eitherRendererOrParser = format.requiresRenderer()
+                    ? Either.<Renderer, 
Parser>left(valueFacet.selectRendererForParamOrPropOrCollOrElseFallback(param))
+                    : Either.<Renderer, 
Parser>right(valueFacet.selectParserForAttributeOrElseFallback(param));
+                var ctx = valueFacet.createValueSemanticsContext(param);
+
+                return bindAsFormated(format, spec, bindableParamValue, 
eitherRendererOrParser, ctx);
+            })
+            .orElseGet(()->
+                // fallback Bindable that is floating free (unbound)
+                // writing to it has no effect on the domain
+                _Bindables.forValue(String.format("Could not find a ValueFacet 
for type %s",
+                        spec.logicalType()))
+            );
     }
 
     // -- PREDICATES
@@ -118,7 +126,7 @@ boolean hasParser(final @NonNull OneToOneAssociation prop) {
     }
 
     boolean hasParser(final @NonNull ObjectActionParameter param) {
-        return isNonScalarParam(param)
+        return param.isPlural()
                 ? false
                 : param.getElementType()
                     .valueFacet()
@@ -128,18 +136,10 @@ boolean hasParser(final @NonNull ObjectActionParameter 
param) {
 
     // -- HELPER
 
-    private boolean isNonScalarParam(final @NonNull ObjectActionParameter 
param) {
-        return param.getFeatureType() == FeatureType.ACTION_PARAMETER_PLURAL;
-    }
-
-    private void guardAgainstNonScalarParam(final @NonNull 
ObjectActionParameter param) {
-        if(isNonScalarParam(param)) {
-            throw _Exceptions.illegalArgument(
-                    "Non-scalar action parameters are neither parseable nor 
renderable: %s",
-                    param.getFeatureIdentifier());
-        }
-    }
-
+    /**
+     * For {@link TargetFormat#PARSABLE_TEXT} returns a {@link Bindable},
+     * otherwise just an {@link Observable}.
+     */
     @SuppressWarnings({ "unchecked", "rawtypes" })
     private Observable<String> bindAsFormated(
             final @NonNull TargetFormat format,
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmDebugUtils.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmDebugUtils.java
index c2a1a486f19..d9367698a51 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmDebugUtils.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmDebugUtils.java
@@ -50,7 +50,7 @@ public String formatted() {
         }
         String formatted(final ManagedParameter managedParam) {
             return String.format("- param[index=%d,name=%s]: %s",
-                    managedParam.getParamNr(),
+                    managedParam.paramIndex(),
                     managedParam.getFriendlyName(),
                     formatPendingValue(managedParam.getValue().getValue()));
         }
diff --git 
a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/tabular/simple/DataTableSerializationTest.java
 
b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/tabular/simple/DataTableSerializationTest.java
index 137a28c007e..11f0d68c840 100644
--- 
a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/tabular/simple/DataTableSerializationTest.java
+++ 
b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/tabular/simple/DataTableSerializationTest.java
@@ -21,7 +21,8 @@
 import jakarta.inject.Named;
 
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mockito;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -38,6 +39,7 @@
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.Setter;
+import lombok.SneakyThrows;
 
 class DataTableSerializationTest implements HasMetaModelContext {
 
@@ -49,52 +51,59 @@ final void setUp() throws Exception {
             .build();
     }
 
-    @Named("DataTableSerializationTest.Customer")
+    @Named("DataTableSerializationTest.CustomerClass")
     @AllArgsConstructor
-    public static class Customer implements ViewModel {
+    public static class CustomerClass implements ViewModel {
 
-        @Property
-        @Getter @Setter
-        private String memento;
+        @Property @Getter @Setter private String memento;
 
-        @Override
-        public String viewModelMemento() {
-            return memento;
-        }
+        @Override public String viewModelMemento() { return memento; }
+    }
+    
+    @Named("DataTableSerializationTest.CustomerRecord")
+    public record CustomerRecord(@Property String memento) implements 
ViewModel {
 
+        @Override public String viewModelMemento() { return memento; }
     }
 
-    @Test
-    void roundtripOnEmptyTable() {
-        var original = DataTable.forDomainType(Customer.class);
+
+    @ParameterizedTest
+    @ValueSource(classes = {CustomerClass.class, CustomerRecord.class})
+    void roundtripOnEmptyTable(Class<? extends ViewModel> viewmodelClass) {
+        var original = DataTable.forDomainType(viewmodelClass);
         var afterRoundtrip = _SerializationTester.roundtrip(original);
 
         assertNotNull(afterRoundtrip);
         assertEquals(
-                "DataTableSerializationTest.Customer",
+                "DataTableSerializationTest." + viewmodelClass.getSimpleName(),
                 afterRoundtrip.elementType().logicalTypeName());
         assertEquals(0, afterRoundtrip.getElementCount());
         assertEquals(1, afterRoundtrip.dataColumns().size());
         assertEquals(0, afterRoundtrip.dataRows().size());
     }
 
-    @Test
-    void roundtripOnPopulatedTable() {
-        var original = DataTable.forDomainType(Customer.class)
-            .withDataElementPojos(Can.of(
-                getObjectManager().adapt(new Customer("cus-1")),
-                getObjectManager().adapt(new Customer("cus-2"))
-                ));
+    @ParameterizedTest
+    @ValueSource(classes = {CustomerClass.class, CustomerRecord.class})
+    void roundtripOnPopulatedTable(Class<? extends ViewModel> viewmodelClass) {
+        var original = DataTable.forDomainType(viewmodelClass)
+            .withDataElementPojos(Can.of("cus-1", "cus-2")
+                .map(name->newInstance(viewmodelClass, name))
+                .map(getObjectManager()::adapt));
 
         var afterRoundtrip = _SerializationTester.roundtrip(original);
         assertNotNull(afterRoundtrip);
         assertEquals(2, afterRoundtrip.dataRows().size());
 
-        var cus1 = (Customer) 
afterRoundtrip.dataRows().getElseFail(0).rowElement().getPojo();
-        var cus2 = (Customer) 
afterRoundtrip.dataRows().getElseFail(1).rowElement().getPojo();
+        var cus1 = (ViewModel) 
afterRoundtrip.dataRows().getElseFail(0).rowElement().getPojo();
+        var cus2 = (ViewModel) 
afterRoundtrip.dataRows().getElseFail(1).rowElement().getPojo();
 
-        assertEquals("cus-1", cus1.getMemento());
-        assertEquals("cus-2", cus2.getMemento());
+        assertEquals("cus-1", cus1.viewModelMemento());
+        assertEquals("cus-2", cus2.viewModelMemento());
+    }
+    
+    @SneakyThrows
+    private static ViewModel newInstance(Class<? extends ViewModel> 
viewmodelClass, String memento) {
+        return viewmodelClass.getConstructor(new 
Class<?>[]{String.class}).newInstance(memento);
     }
 
 }
diff --git 
a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/util/interaction/DomainObjectTesterFactory.java
 
b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/util/interaction/DomainObjectTesterFactory.java
index 8caf6f371ae..2a76ec182e8 100644
--- 
a/regressiontests/base/src/main/java/org/apache/causeway/testdomain/util/interaction/DomainObjectTesterFactory.java
+++ 
b/regressiontests/base/src/main/java/org/apache/causeway/testdomain/util/interaction/DomainObjectTesterFactory.java
@@ -367,7 +367,7 @@ public void assertInvocationResult(
                 pendingArgs.getParamModels()
                         .forEach(param->{
                             pojoReplacers
-                                .get(param.getParamNr())
+                                .get(param.paramIndex())
                                 .ifPresent(replacer->updatePojo(param, 
replacer));
                         });
 
@@ -397,7 +397,7 @@ public Object invokeWithPojos(final List<Object> 
pojoArgList) {
                 pendingArgs.getParamModels()
                         .forEach(param->{
                             pojoVector
-                                .get(param.getParamNr())
+                                .get(param.paramIndex())
                                 .ifPresent(pojo->updatePojo(param, __->pojo));
                         });
 
@@ -431,7 +431,7 @@ public void assertInvocationResultNoRules(
                 pendingArgs.getParamModels()
                 .forEach(param->{
                     pojoReplacers
-                        .get(param.getParamNr())
+                        .get(param.paramIndex())
                         .ifPresent(replacer->updatePojo(param, replacer));
                 });
 
@@ -463,7 +463,7 @@ public void assertParameterValues(
                 pendingArgs.getParamModels()
                 .forEach(param->{
                     pojoTests
-                        .get(param.getParamNr())
+                        .get(param.paramIndex())
                         .ifPresent(pojoTest->
                             pojoTest.accept(
                                     
MmUnwrapUtils.single(param.getValue().getValue())
@@ -491,7 +491,7 @@ public <X> void assertParameterChoices(
                 startParameterNegotiation(checkRules).getParamModels()
                 .forEach(param->{
                     pojoTests
-                        .get(param.getParamNr())
+                        .get(param.paramIndex())
                         .ifPresent(pojoTest->
                             pojoTest.accept(
                                     (List<X>) choicesFor(param)
@@ -526,10 +526,10 @@ public void assertParameterVisibility(
                     pendingArgs.getParamModels()
                     .forEach(param->{
 
-                        var consent = 
pendingArgs.getVisibilityConsent(param.getParamNr());
+                        var consent = 
pendingArgs.getVisibilityConsent(param.paramIndex());
 
                         visibilityTests
-                            .get(param.getParamNr())
+                            .get(param.paramIndex())
                             .ifPresent(visibilityTest->
                                 visibilityTest.accept(consent.isAllowed()));
                     });
@@ -554,10 +554,10 @@ public void assertParameterUsability(
                     pendingArgs.getParamModels()
                     .forEach(param->{
 
-                        var consent = 
pendingArgs.getUsabilityConsent(param.getParamNr());
+                        var consent = 
pendingArgs.getUsabilityConsent(param.paramIndex());
 
                         usabilityTests
-                            .get(param.getParamNr())
+                            .get(param.paramIndex())
                             .ifPresent(usabilityTest->
                                 
usabilityTest.accept(consent.getReasonAsString().orElse(null)));
                     });
@@ -583,7 +583,7 @@ public void assertValidationMessage(
                         var objManager = 
param.getMetaModel().getObjectManager();
 
                         pojoArgMappers
-                            .get(param.getParamNr())
+                            .get(param.paramIndex())
                             .ifPresent(argMapper->
                                 param.getValue().setValue(
                                     objManager
@@ -640,7 +640,7 @@ public DataTableTester tableTester(
                 pendingArgs.getParamModels()
                         .forEach(param->{
                             pojoReplacers
-                                .get(param.getParamNr())
+                                .get(param.paramIndex())
                                 .ifPresent(replacer->updatePojo(param, 
replacer));
                         });
 


Reply via email to