You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2019/08/08 14:55:12 UTC

[isis] branch v2 updated: ISIS-2158 fixes some behavior inconsitences regarding domain object execution mode

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

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


The following commit(s) were added to refs/heads/v2 by this push:
     new 4d48551  ISIS-2158 fixes some behavior inconsitences regarding domain object execution mode
4d48551 is described below

commit 4d48551e822f48883b71d74ff9127e4470248330
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Aug 8 16:54:51 2019 +0200

    ISIS-2158 fixes some behavior inconsitences regarding domain object
    execution mode
---
 .../applib/services/wrapper/WrapperFactory.java    |  29 +-
 .../isis/commons/internal/collections/_Arrays.java |  21 +
 .../commons/internal/collections/_ArraysTest.java  |  74 +++
 .../isis/metamodel/facets/ImperativeFacet.java     |  36 +-
 ...FactoryDefaultTest_wrappedObject_transient.java |  39 +-
 .../handlers/DomainObjectInvocationHandler.java    | 574 ++++++++++-----------
 .../unittestsupport/jmocking/PostponedAction.java  |  46 ++
 7 files changed, 459 insertions(+), 360 deletions(-)

diff --git a/core/applib/src/main/java/org/apache/isis/applib/services/wrapper/WrapperFactory.java b/core/applib/src/main/java/org/apache/isis/applib/services/wrapper/WrapperFactory.java
index b0d6971..4fda1ce 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/services/wrapper/WrapperFactory.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/services/wrapper/WrapperFactory.java
@@ -25,6 +25,9 @@ import org.apache.isis.applib.services.factory.FactoryService;
 import org.apache.isis.applib.services.wrapper.events.InteractionEvent;
 import org.apache.isis.applib.services.wrapper.listeners.InteractionListener;
 
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+
 /**
  * Provides the ability to &quot;wrap&quot; of a domain object such that it can
  * be interacted with while enforcing the hide/disable/validate rules implies by
@@ -71,21 +74,27 @@ public interface WrapperFactory {
      *
      * @see WrapperFactory#wrap(Object, ExecutionMode)
      */
+    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
     public static enum ExecutionMode {
         /**
-         * Validate all business rules and then execute.
+         * Validate all business rules and then execute. May throw exceptions in order to fail fast. 
          */
-        EXECUTE(true,true,true),
+        EXECUTE(true, true, true),
+        
         /**
-         * Validate all business rules and then execute, but don't throw exception if fails.
+         * Validate all business rules and then execute, but don't throw an exception if validation 
+         * or execution fails.
          */
-        TRY(true,true,false),
+        TRY(true, true, false),
+        
         /**
-         * Skip all business rules and then execute.
+         * Skip all business rules and then execute, does throw an exception if execution fails.
          */
-        SKIP_RULES(false, true, false),
+        SKIP_RULES(false, true, true),
+        
         /**
-         * Validate all business rules but do not execute.
+         * Validate all business rules but do not execute, throw an exception if validation 
+         * fails. 
          */
         NO_EXECUTE(true, false, true);
 
@@ -93,12 +102,6 @@ public interface WrapperFactory {
         private final boolean execute;
         private final boolean failFast;
 
-        private ExecutionMode(final boolean enforceRules, final boolean execute, final boolean failFast) {
-            this.enforceRules = enforceRules;
-            this.execute = execute;
-            this.failFast = failFast;
-        }
-
         public boolean shouldEnforceRules() {
             return enforceRules;
         }
diff --git a/core/commons/src/main/java/org/apache/isis/commons/internal/collections/_Arrays.java b/core/commons/src/main/java/org/apache/isis/commons/internal/collections/_Arrays.java
index 8cd1eda..1c3adc7 100644
--- a/core/commons/src/main/java/org/apache/isis/commons/internal/collections/_Arrays.java
+++ b/core/commons/src/main/java/org/apache/isis/commons/internal/collections/_Arrays.java
@@ -20,6 +20,7 @@
 package org.apache.isis.commons.internal.collections;
 
 import java.lang.reflect.Array;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.function.BiPredicate;
 import java.util.stream.Collector;
@@ -32,6 +33,8 @@ import org.apache.isis.commons.internal.base._NullSafe;
 
 import static org.apache.isis.commons.internal.base._With.requires;
 
+import lombok.val;
+
 /**
  * <h1>- internal use only -</h1>
  * <p>
@@ -218,6 +221,24 @@ public final class _Arrays {
         return _NullSafe.stream(iterable)
                 .collect(toArray(componentType));
     }
+    
+    // -- MODIFICATION
+    
+    public static <T> T[] removeByIndex(T[] array, int index) {
+        if(array==null || array.length<=1) {
+            throw new IllegalArgumentException("Array must be of lenght 1 or larger.");
+        }
+        if(index>=array.length) {
+            val msg = String.format("Array index %d is out of bounds [0, %d]", index, array.length-1);
+            throw new IllegalArgumentException(msg);
+        }
+        final T[] result = Arrays.copyOf(array, array.length - 1);
+        // copy the elements from index + 1 till end 
+        // from original array to the new array 
+        val remaining = result.length - index;
+        System.arraycopy(array, index+1, result, index, remaining);
+        return result;
+    }
 
     // -- COMPONENT TYPE INFERENCE
 
diff --git a/core/commons/src/test/java/org/apache/isis/commons/internal/collections/_ArraysTest.java b/core/commons/src/test/java/org/apache/isis/commons/internal/collections/_ArraysTest.java
new file mode 100644
index 0000000..f4d83a9
--- /dev/null
+++ b/core/commons/src/test/java/org/apache/isis/commons/internal/collections/_ArraysTest.java
@@ -0,0 +1,74 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.commons.internal.collections;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import lombok.val;
+
+class _ArraysTest {
+
+    @Test
+    void removeByIndex_head() {
+        val input = new Integer[] {0, 1, 2, 3};
+        val output = _Arrays.removeByIndex(input, 0);
+        assertArrayEquals(new Integer[] {1, 2, 3}, output);
+    }
+
+    @Test
+    void removeByIndex_inBetween() {
+        val input = new Integer[] {0, 1, 2, 3};
+        val output = _Arrays.removeByIndex(input, 1);
+        assertArrayEquals(new Integer[] {0, 2, 3}, output);
+    }
+    
+    @Test
+    void removeByIndex_tail() {
+        val input = new Integer[] {0, 1, 2, 3};
+        val output = _Arrays.removeByIndex(input, 3);
+        assertArrayEquals(new Integer[] {0, 1, 2}, output);
+    }
+    
+    @Test
+    void removeByIndex_outOfBounds() {
+        val input = new Integer[] {0, 1, 2, 3};
+        assertThrows(IllegalArgumentException.class, 
+                ()->_Arrays.removeByIndex(input, 4));
+    }
+    
+    @Test
+    void removeByIndex_empty() {
+        val input = new Integer[] {};
+        assertThrows(IllegalArgumentException.class, 
+                ()->_Arrays.removeByIndex(input, 0));
+    }
+    
+    @Test
+    void removeByIndex_null() {
+        val input = (Integer[])null;
+        assertThrows(IllegalArgumentException.class, 
+                ()->_Arrays.removeByIndex(input, 0));
+    }
+    
+    
+    
+}
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/ImperativeFacet.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/ImperativeFacet.java
index dbda762..35bab07 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/ImperativeFacet.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/ImperativeFacet.java
@@ -23,16 +23,18 @@ import java.lang.reflect.Method;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Predicate;
-import java.util.stream.Stream;
+import java.util.stream.Collectors;
 
 import org.apache.isis.applib.services.wrapper.WrapperFactory;
 import org.apache.isis.commons.internal.base._Casts;
-import org.apache.isis.commons.internal.collections._Lists;
+import org.apache.isis.commons.internal.base._NullSafe;
 import org.apache.isis.metamodel.facetapi.DecoratingFacet;
 import org.apache.isis.metamodel.facetapi.Facet;
 import org.apache.isis.metamodel.spec.ObjectSpecification;
 import org.apache.isis.metamodel.spec.feature.ObjectMember;
 
+import lombok.val;
+
 /**
  * A {@link Facet} implementation that ultimately wraps a {@link Method} or
  * possibly several equivalent methods, for a Java implementation of a
@@ -133,20 +135,22 @@ public interface ImperativeFacet extends Facet {
         }
 
         public static Intent getIntent(final ObjectMember member, final Method method) {
-            final List<ImperativeFacet> imperativeFacets = _Lists.newArrayList();
-            final Stream<Facet> allFacets = member.streamFacets();
-            allFacets.forEach(facet->{
-                final ImperativeFacet imperativeFacet = ImperativeFacet.Util.getImperativeFacet(facet);
-                if (imperativeFacet == null) {
-                    return;
-                }
-                final List<Method> methods = imperativeFacet.getMethods();
-                if (!methods.contains(method)) {
-                    return;
-                }
-                imperativeFacets.add(imperativeFacet);
-            });
-
+//            val imperativeFacets = member.streamFacets()
+//                    .map(ImperativeFacet.Util::getImperativeFacet)
+//                    .filter(_NullSafe::isPresent)
+//                    .filter(imperativeFacet->imperativeFacet.getMethods().contains(method))
+//                    .collect(Collectors.toList());
+
+            val imperativeFacets1 = member.streamFacets()
+                    .map(ImperativeFacet.Util::getImperativeFacet)
+                    .filter(_NullSafe::isPresent)
+                    .collect(Collectors.toList());
+            
+            val imperativeFacets = imperativeFacets1.stream()
+                    .filter(imperativeFacet->imperativeFacet.getMethods().contains(method))
+                    .collect(Collectors.toList());
+            
+            
             switch(imperativeFacets.size()) {
             case 0:
                 break;
diff --git a/core/plugins/jdo-datanucleus-5/src/test/java/org/apache/isis/wrapper/WrapperFactoryDefaultTest_wrappedObject_transient.java b/core/plugins/jdo-datanucleus-5/src/test/java/org/apache/isis/wrapper/WrapperFactoryDefaultTest_wrappedObject_transient.java
index 708f075..a4fe9f5 100644
--- a/core/plugins/jdo-datanucleus-5/src/test/java/org/apache/isis/wrapper/WrapperFactoryDefaultTest_wrappedObject_transient.java
+++ b/core/plugins/jdo-datanucleus-5/src/test/java/org/apache/isis/wrapper/WrapperFactoryDefaultTest_wrappedObject_transient.java
@@ -22,12 +22,10 @@ package org.apache.isis.wrapper;
 import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.List;
 
 import org.jmock.Expectations;
 import org.jmock.auto.Mock;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 
@@ -63,9 +61,12 @@ import org.apache.isis.security.authentication.standard.SimpleSession;
 import org.apache.isis.unittestsupport.jmocking.JUnitRuleMockery2;
 import org.apache.isis.unittestsupport.jmocking.JUnitRuleMockery2.Mode;
 
+import static org.apache.isis.unittestsupport.jmocking.PostponedAction.returnValuePostponed;
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertThat;
 
+import lombok.val;
+
 /**
  * Contract test.
  */
@@ -110,7 +111,6 @@ public class WrapperFactoryDefaultTest_wrappedObject_transient {
 
     private final SimpleSession session = new SimpleSession("tester", Collections.<String>emptyList());
 
-    private List<Facet> facets;
     private Method getPasswordMethod;
     private Method setPasswordMethod;
 
@@ -143,6 +143,9 @@ public class WrapperFactoryDefaultTest_wrappedObject_transient {
 
                 allowing(mockPersistenceSessionServiceInternal).adapterFor(employeeDO);
                 will(returnValue(mockEmployeeAdapter));
+                
+                allowing(mockPersistenceSessionServiceInternal).adapterFor(passwordValue);
+                will(returnValue(mockPasswordAdapter));
 
                 allowing(mockAdapterManager).adapterFor(employeeDO);
                 will(returnValue(mockEmployeeAdapter));
@@ -162,6 +165,9 @@ public class WrapperFactoryDefaultTest_wrappedObject_transient {
                 allowing(mockPasswordMember).getIdentifier();
                 will(returnValue(mockPasswordIdentifier));
 
+                allowing(mockPasswordIdentifier).toClassAndNameIdentityString();
+                will(returnValue("mocked-class#member"));
+                
                 allowing(mockSpecificationLoader).loadSpecification(Employee.class);
                 will(returnValue(mockEmployeeSpec));
 
@@ -201,7 +207,7 @@ public class WrapperFactoryDefaultTest_wrappedObject_transient {
 
         // given
         final DisabledFacet disabledFacet = new DisabledFacetAbstractAlwaysEverywhere(mockPasswordMember){};
-        facets = Arrays.asList(disabledFacet, new PropertySetterFacetViaSetterMethod(setPasswordMethod, mockPasswordMember));
+        val facets = Arrays.asList(disabledFacet, new PropertySetterFacetViaSetterMethod(setPasswordMethod, mockPasswordMember));
 
         final Consent visibilityConsent = new Allow(new InteractionResult(new PropertyVisibilityEvent(employeeDO, null)));
 
@@ -212,13 +218,12 @@ public class WrapperFactoryDefaultTest_wrappedObject_transient {
         context.checking(new Expectations() {
             {
                 allowing(mockPasswordMember).streamFacets();
-                will(returnValue(facets.stream()));
+                will(returnValuePostponed(facets::stream));
 
                 allowing(mockPasswordMember).isVisible(mockEmployeeAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE);
                 will(returnValue(visibilityConsent));
 
-                allowing(mockPasswordMember).isUsable(mockEmployeeAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE
-                        );
+                allowing(mockPasswordMember).isUsable(mockEmployeeAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE);
                 will(returnValue(usabilityConsent));
             }
         });
@@ -229,7 +234,6 @@ public class WrapperFactoryDefaultTest_wrappedObject_transient {
         // then should throw exception
     }
 
-    @Ignore("TODO - reinstate or replace with integration tests")
     @Test
     public void canModifyProperty() {
         // given
@@ -243,8 +247,7 @@ public class WrapperFactoryDefaultTest_wrappedObject_transient {
                 allowing(mockPasswordMember).isVisible(mockEmployeeAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE);
                 will(returnValue(visibilityConsent));
 
-                allowing(mockPasswordMember).isUsable(mockEmployeeAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE
-                        );
+                allowing(mockPasswordMember).isUsable(mockEmployeeAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE);
                 will(returnValue(usabilityConsent));
 
                 allowing(mockPasswordMember).isAssociationValid(mockEmployeeAdapter, mockPasswordAdapter,
@@ -253,13 +256,16 @@ public class WrapperFactoryDefaultTest_wrappedObject_transient {
             }
         });
 
-        facets = Arrays.asList((Facet)new PropertySetterFacetViaSetterMethod(setPasswordMethod, mockPasswordMember));
+        val facets1 = Arrays.asList((Facet)new PropertySetterFacetViaSetterMethod(
+                setPasswordMethod, mockPasswordMember));
+        
         context.checking(new Expectations() {
             {
                 allowing(mockPasswordMember).streamFacets();
-                will(returnValue(facets.stream()));
+                will(returnValuePostponed(facets1::stream));
 
-                oneOf(mockPasswordMember).set(mockEmployeeAdapter, mockPasswordAdapter, InteractionInitiatedBy.USER);
+                oneOf(mockPasswordMember)
+                .set(mockEmployeeAdapter, mockPasswordAdapter, InteractionInitiatedBy.USER);
             }
         });
 
@@ -268,12 +274,13 @@ public class WrapperFactoryDefaultTest_wrappedObject_transient {
 
 
         // and given
-        facets = Arrays.asList((Facet)new PropertyAccessorFacetViaAccessor(mockOnType, getPasswordMethod, mockPasswordMember
-                ));
+        val facets2 = Arrays.asList((Facet)new PropertyAccessorFacetViaAccessor(
+                mockOnType, getPasswordMethod, mockPasswordMember));
+        
         context.checking(new Expectations() {
             {
                 allowing(mockPasswordMember).streamFacets();
-                will(returnValue(facets.stream()));
+                will(returnValuePostponed(facets2::stream));
 
                 oneOf(mockPasswordMember).get(mockEmployeeAdapter, InteractionInitiatedBy.USER);
                 will(returnValue(mockPasswordAdapter));
diff --git a/core/runtime-extensions/src/main/java/org/apache/isis/wrapper/handlers/DomainObjectInvocationHandler.java b/core/runtime-extensions/src/main/java/org/apache/isis/wrapper/handlers/DomainObjectInvocationHandler.java
index d06577b..5785240 100644
--- a/core/runtime-extensions/src/main/java/org/apache/isis/wrapper/handlers/DomainObjectInvocationHandler.java
+++ b/core/runtime-extensions/src/main/java/org/apache/isis/wrapper/handlers/DomainObjectInvocationHandler.java
@@ -22,10 +22,10 @@ package org.apache.isis.wrapper.handlers;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.Collection;
-import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Supplier;
 import java.util.stream.Stream;
 
 import org.apache.isis.applib.annotation.Where;
@@ -37,25 +37,22 @@ import org.apache.isis.applib.services.wrapper.WrapperFactory.ExecutionMode;
 import org.apache.isis.applib.services.wrapper.WrappingObject;
 import org.apache.isis.applib.services.wrapper.events.CollectionAccessEvent;
 import org.apache.isis.applib.services.wrapper.events.InteractionEvent;
-import org.apache.isis.applib.services.wrapper.events.ObjectTitleEvent;
 import org.apache.isis.applib.services.wrapper.events.PropertyAccessEvent;
 import org.apache.isis.applib.services.wrapper.events.UsabilityEvent;
 import org.apache.isis.applib.services.wrapper.events.ValidityEvent;
 import org.apache.isis.applib.services.wrapper.events.VisibilityEvent;
 import org.apache.isis.commons.internal.base._NullSafe;
-import org.apache.isis.commons.internal.collections._Lists;
+import org.apache.isis.commons.internal.collections._Arrays;
 import org.apache.isis.commons.internal.collections._Sets;
 import org.apache.isis.metamodel.IsisJdoMetamodelPlugin;
 import org.apache.isis.metamodel.MetaModelContext;
 import org.apache.isis.metamodel.adapter.ObjectAdapter;
 import org.apache.isis.metamodel.adapter.ObjectAdapterProvider;
-import org.apache.isis.metamodel.consent.Consent;
 import org.apache.isis.metamodel.consent.InteractionInitiatedBy;
 import org.apache.isis.metamodel.consent.InteractionResult;
 import org.apache.isis.metamodel.facets.ImperativeFacet;
 import org.apache.isis.metamodel.facets.ImperativeFacet.Intent;
 import org.apache.isis.metamodel.facets.object.mixin.MixinFacet;
-import org.apache.isis.metamodel.interactions.ObjectTitleContext;
 import org.apache.isis.metamodel.spec.ObjectSpecification;
 import org.apache.isis.metamodel.spec.feature.Contributed;
 import org.apache.isis.metamodel.spec.feature.ObjectAction;
@@ -236,12 +233,12 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
                 throw new UnsupportedOperationException(String.format("Cannot invoke supporting method '%s'; use only the 'invoke' method", memberName));
             }
 
-            final ObjectAction objectAction = (ObjectAction) objectMember;
+            val objectAction = (ObjectAction) objectMember;
 
             ObjectAction actualObjectAction;
             ObjectAdapter actualTargetAdapter;
 
-            final MixinFacet mixinFacet = targetAdapter.getSpecification().getFacet(MixinFacet.class);
+            val mixinFacet = targetAdapter.getSpecification().getFacet(MixinFacet.class);
             if(mixinFacet != null) {
 
                 // rather than invoke on a (transient) mixin, instead try to
@@ -268,11 +265,12 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
     private static ObjectAction determineMixinAction(
             final ObjectAdapter domainObjectAdapter,
             final ObjectAction objectAction) {
+        
         if(domainObjectAdapter == null) {
             return null;
         }
-        final ObjectSpecification specification = domainObjectAdapter.getSpecification();
-        final Stream<ObjectAction> objectActions = specification.streamObjectActions(Contributed.INCLUDED);
+        val specification = domainObjectAdapter.getSpecification();
+        val objectActions = specification.streamObjectActions(Contributed.INCLUDED);
 
         return objectActions
                 .filter(action->action instanceof ObjectActionMixedIn)
@@ -285,7 +283,9 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
     }
 
     public InteractionInitiatedBy getInteractionInitiatedBy() {
-        return getExecutionMode().shouldEnforceRules()? InteractionInitiatedBy.USER: InteractionInitiatedBy.FRAMEWORK;
+        return getExecutionMode().shouldEnforceRules()
+                ? InteractionInitiatedBy.USER
+                        : InteractionInitiatedBy.FRAMEWORK;
     }
 
     // see if this is a contributed property/collection/action
@@ -364,9 +364,10 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
 
         resolveIfRequired(targetAdapter);
 
-        final ObjectSpecification targetNoSpec = targetAdapter.getSpecification();
-        final ObjectTitleContext titleContext = targetNoSpec.createTitleInteractionContext(getAuthenticationSession(), InteractionInitiatedBy.FRAMEWORK, targetAdapter);
-        final ObjectTitleEvent titleEvent = titleContext.createInteractionEvent();
+        val targetNoSpec = targetAdapter.getSpecification();
+        val titleContext = targetNoSpec.createTitleInteractionContext(
+                getAuthenticationSession(), InteractionInitiatedBy.FRAMEWORK, targetAdapter);
+        val titleEvent = titleContext.createInteractionEvent();
         notifyListeners(titleEvent);
         return titleEvent.getTitle();
     }
@@ -378,38 +379,20 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
     private Object handleSaveMethod(
             final ObjectAdapter targetAdapter, final ObjectSpecification targetNoSpec) {
 
-        if(getExecutionMode().shouldEnforceRules()) {
-            if(getExecutionMode().shouldFailFast()) {
-                final InteractionResult interactionResult =
-                        targetNoSpec.isValidResult(targetAdapter, getInteractionInitiatedBy());
-                notifyListenersAndVetoIfRequired(interactionResult);
-            } else {
-                try {
-                    final InteractionResult interactionResult =
-                            targetNoSpec.isValidResult(targetAdapter, getInteractionInitiatedBy());
-                    notifyListenersAndVetoIfRequired(interactionResult);
-                } catch(Exception ex) {
-                    return null;
-                }
-            }
-
-        }
-
-        if (getExecutionMode().shouldExecute()) {
+        runValidationTask(()->{
+            val interactionResult =
+                    targetNoSpec.isValidResult(targetAdapter, getInteractionInitiatedBy());
+            notifyListenersAndVetoIfRequired(interactionResult);
+        });
+        
+        return runExecutionTask(()->{
             if (targetAdapter.isTransient()) {
                 val ps = IsisContext.getPersistenceSession().get();
-                if(getExecutionMode().shouldFailFast()) {
-                    ps.makePersistentInTransaction(targetAdapter);
-                } else {
-                    try {
-                        ps.makePersistentInTransaction(targetAdapter);
-                    } catch(Exception ignore) {
-                        // ignore
-                    }
-                }
+                ps.makePersistentInTransaction(targetAdapter);
             }
-        }
-        return null;
+            return null;
+        });
+        
     }
 
     // /////////////////////////////////////////////////////////////////
@@ -421,34 +404,28 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
             final Object[] args,
             final OneToOneAssociation property) {
 
-        if (args.length != 0) {
-            throw new IllegalArgumentException("Invoking a 'get' should have no arguments");
-        }
+        zeroArgsElseThrow(args, "get");
 
-        if(getExecutionMode().shouldEnforceRules()) {
-            if(getExecutionMode().shouldFailFast()) {
-                checkVisibility(targetAdapter, property);
-            } else {
-                try {
-                    checkVisibility(targetAdapter, property);
-                } catch(Exception ex) {
-                    return null;
-
-                }
-            }
-
-        }
+        runValidationTask(()->{
+            checkVisibility(targetAdapter, property);
+        });
 
         resolveIfRequired(targetAdapter);
+        
+        return runExecutionTask(()->{
+        
+            val interactionInitiatedBy = getInteractionInitiatedBy();
+            val currentReferencedAdapter = property.get(targetAdapter, interactionInitiatedBy);
 
-        final InteractionInitiatedBy interactionInitiatedBy = getInteractionInitiatedBy();
-        final ObjectAdapter currentReferencedAdapter = property.get(targetAdapter, interactionInitiatedBy);
-
-        final Object currentReferencedObj = ObjectAdapter.Util.unwrapPojo(currentReferencedAdapter);
+            val currentReferencedObj = ObjectAdapter.Util.unwrapPojo(currentReferencedAdapter);
 
-        final PropertyAccessEvent ev = new PropertyAccessEvent(getDelegate(), property.getIdentifier(), currentReferencedObj);
-        notifyListeners(ev);
-        return currentReferencedObj;
+            val propertyAccessEvent = new PropertyAccessEvent(
+                    getDelegate(), property.getIdentifier(), currentReferencedObj);
+            notifyListeners(propertyAccessEvent);
+            return currentReferencedObj;
+            
+        });
+        
     }
 
 
@@ -456,54 +433,36 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
     // property - modify
     // /////////////////////////////////////////////////////////////////
 
-    private Object handleSetterMethodOnProperty(
-            final ObjectAdapter targetAdapter, final Object[] args,
-            final OneToOneAssociation property) {
-        if (args.length != 1) {
-            throw new IllegalArgumentException("Invoking a setter should only have a single argument");
-        }
-
-        final Object argumentObj = underlying(args[0]);
 
-        if(getExecutionMode().shouldEnforceRules()) {
-            if(getExecutionMode().shouldFailFast()) {
-                checkVisibility(targetAdapter, property);
-                checkUsability(targetAdapter, property);
-            } else {
-                try {
-                    checkVisibility(targetAdapter, property);
-                    checkUsability(targetAdapter, property);
-                } catch(Exception ex) {
-                    return null;
-                }
-            }
-
-        }
-
-        final ObjectAdapter argumentAdapter = argumentObj != null ? adapterFor(argumentObj) : null;
 
+    private Object handleSetterMethodOnProperty(
+            final ObjectAdapter targetAdapter, 
+            final Object[] args,
+            final OneToOneAssociation property) {
+        
+        val singleArg = singleArgUnderlyingElseNull(args, "setter");
+        
+        runValidationTask(()->{
+            checkVisibility(targetAdapter, property);
+            checkUsability(targetAdapter, property);
+        });
+        
+        val argumentAdapter = adapterFor(singleArg);
+        
         resolveIfRequired(targetAdapter);
 
-
-        if(getExecutionMode().shouldEnforceRules()) {
-            final InteractionResult interactionResult = property.isAssociationValid(targetAdapter, argumentAdapter, getInteractionInitiatedBy()).getInteractionResult();
+        runValidationTask(()->{
+            val interactionResult = property.isAssociationValid(
+                    targetAdapter, argumentAdapter, getInteractionInitiatedBy())
+                    .getInteractionResult();
             notifyListenersAndVetoIfRequired(interactionResult);
-        }
-
-        if (getExecutionMode().shouldExecute()) {
-            if(getExecutionMode().shouldFailFast()) {
-                property.set(targetAdapter, argumentAdapter, getInteractionInitiatedBy());
-            } else {
-                try {
-                    property.set(targetAdapter, argumentAdapter, getInteractionInitiatedBy());
-                } catch(Exception ignore) {
-                    // ignore
-                }
-            }
-
-        }
-
-        return null;
+        });
+        
+        return runExecutionTask(()->{
+            property.set(targetAdapter, argumentAdapter, getInteractionInitiatedBy());
+            return null;
+        });
+        
     }
 
 
@@ -518,44 +477,40 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
             final Method method,
             final String memberName) {
 
-        if (args.length != 0) {
-            throw new IllegalArgumentException("Invoking a 'get' should have no arguments");
-        }
+        zeroArgsElseThrow(args, "get");
 
-        if(getExecutionMode().shouldEnforceRules()) {
-            if(getExecutionMode().shouldFailFast()) {
-                checkVisibility(targetAdapter, collection);
-            } else {
-                try {
-                    checkVisibility(targetAdapter, collection);
-                } catch(Exception ex) {
-                    return null;
-                }
-            }
-
-        }
+        runValidationTask(()->{
+            checkVisibility(targetAdapter, collection);
+        });
 
         resolveIfRequired(targetAdapter);
-
-        final InteractionInitiatedBy interactionInitiatedBy = getInteractionInitiatedBy();
-        final ObjectAdapter currentReferencedAdapter = collection.get(targetAdapter, interactionInitiatedBy);
-
-        final Object currentReferencedObj = ObjectAdapter.Util.unwrapPojo(currentReferencedAdapter);
-
-        final CollectionAccessEvent ev = new CollectionAccessEvent(getDelegate(), collection.getIdentifier());
-
-        if (currentReferencedObj instanceof Collection) {
-            final Collection<?> collectionViewObject = lookupWrappingObject(method, memberName,
-                    (Collection<?>) currentReferencedObj, collection);
-            notifyListeners(ev);
-            return collectionViewObject;
-        } else if (currentReferencedObj instanceof Map) {
-            final Map<?, ?> mapViewObject = lookupWrappingObject(memberName, (Map<?, ?>) currentReferencedObj,
-                    collection);
-            notifyListeners(ev);
-            return mapViewObject;
-        }
-        throw new IllegalArgumentException(String.format("Collection type '%s' not supported by framework", currentReferencedObj.getClass().getName()));
+        
+        return runExecutionTask(()->{
+        
+            val interactionInitiatedBy = getInteractionInitiatedBy();
+            val currentReferencedAdapter = collection.get(targetAdapter, interactionInitiatedBy);
+
+            val currentReferencedObj = ObjectAdapter.Util.unwrapPojo(currentReferencedAdapter);
+
+            val collectionAccessEvent = new CollectionAccessEvent(getDelegate(), collection.getIdentifier());
+
+            if (currentReferencedObj instanceof Collection) {
+                val collectionViewObject = lookupWrappingObject(method, memberName,
+                        (Collection<?>) currentReferencedObj, collection);
+                notifyListeners(collectionAccessEvent);
+                return collectionViewObject;
+            } else if (currentReferencedObj instanceof Map) {
+                val mapViewObject = lookupWrappingObject(memberName, (Map<?, ?>) currentReferencedObj,
+                        collection);
+                notifyListeners(collectionAccessEvent);
+                return mapViewObject;
+            }
+            
+            val msg = String.format("Collection type '%s' not supported by framework", currentReferencedObj.getClass().getName()); 
+            throw new IllegalArgumentException(msg);
+            
+        });
+        
     }
 
     private Collection<?> lookupWrappingObject(
@@ -586,109 +541,64 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
             final Object[] args,
             final OneToManyAssociation otma) {
 
-        if (args.length != 1) {
-            throw new IllegalArgumentException("Invoking a addTo should only have a single argument");
-        }
-
-        if(getExecutionMode().shouldEnforceRules()) {
-            if(getExecutionMode().shouldFailFast()) {
-                checkVisibility(targetAdapter, otma);
-                checkUsability(targetAdapter, otma);
-            } else {
-                try {
-                    checkVisibility(targetAdapter, otma);
-                    checkUsability(targetAdapter, otma);
-                } catch(Exception ex) {
-                    return null;
-                }
-            }
-        }
+        val singleArg = singleArgUnderlyingElseThrow(args, "addTo");
 
+        runValidationTask(()->{
+            checkVisibility(targetAdapter, otma);
+            checkUsability(targetAdapter, otma);
+        });
+        
         resolveIfRequired(targetAdapter);
-
-        final Object argumentObj = underlying(args[0]);
-        if (argumentObj == null) {
-            throw new IllegalArgumentException("Must provide a non-null object to add");
-        }
-        final ObjectAdapter argumentNO = adapterFor(argumentObj);
-
-        if(getExecutionMode().shouldEnforceRules()) {
-            final InteractionResult interactionResult = otma.isValidToAdd(targetAdapter, argumentNO,
+        val argumentAdapter = adapterFor(singleArg);
+        
+        runValidationTask(()->{
+            val interactionResult = otma.isValidToAdd(targetAdapter, argumentAdapter,
                     getInteractionInitiatedBy()).getInteractionResult();
             notifyListenersAndVetoIfRequired(interactionResult);
-        }
-
-        if (getExecutionMode().shouldExecute()) {
-            if(getExecutionMode().shouldFailFast()) {
-                otma.addElement(targetAdapter, argumentNO, getInteractionInitiatedBy());
-            } else {
-                try {
-                    otma.addElement(targetAdapter, argumentNO, getInteractionInitiatedBy());
-                } catch(Exception ignore) {
-                    // ignore
-                }
-            }
-        }
-
-        return null;
+        });
+        
+        return runExecutionTask(()->{
+            otma.addElement(targetAdapter, argumentAdapter, getInteractionInitiatedBy());
+            return null;
+        });
+        
     }
 
 
+    
     // /////////////////////////////////////////////////////////////////
     // collection - remove from
     // /////////////////////////////////////////////////////////////////
 
+
+
     private Object handleCollectionRemoveFromMethod(
             final ObjectAdapter targetAdapter,
             final Object[] args,
             final OneToManyAssociation collection) {
-        if (args.length != 1) {
-            throw new IllegalArgumentException("Invoking a removeFrom should only have a single argument");
-        }
-
-        if(getExecutionMode().shouldEnforceRules()) {
-            if(getExecutionMode().shouldFailFast()) {
-                checkVisibility(targetAdapter, collection);
-                checkUsability(targetAdapter, collection);
-            } else {
-                try {
-                    checkVisibility(targetAdapter, collection);
-                    checkUsability(targetAdapter, collection);
-                } catch(Exception ex) {
-                    return null;
-                }
-            }
-
-        }
+        
+        val singleArg = singleArgUnderlyingElseThrow(args, "removeFrom");
 
+        runValidationTask(()->{
+            checkVisibility(targetAdapter, collection);
+            checkUsability(targetAdapter, collection);
+        });
 
         resolveIfRequired(targetAdapter);
+        val argumentAdapter = adapterFor(singleArg);
 
-        final Object argumentObj = underlying(args[0]);
-        if (argumentObj == null) {
-            throw new IllegalArgumentException("Must provide a non-null object to remove");
-        }
-        final ObjectAdapter argumentAdapter = adapterFor(argumentObj);
-
-        if(getExecutionMode().shouldEnforceRules()) {
-            final InteractionResult interactionResult = collection.isValidToRemove(targetAdapter, argumentAdapter,
+        runValidationTask(()->{
+            val interactionResult = collection.isValidToRemove(targetAdapter, argumentAdapter,
                     getInteractionInitiatedBy()).getInteractionResult();
             notifyListenersAndVetoIfRequired(interactionResult);
-        }
-
-        if (getExecutionMode().shouldExecute()) {
-            if(getExecutionMode().shouldFailFast()) {
-                collection.removeElement(targetAdapter, argumentAdapter, getInteractionInitiatedBy());
-            } else {
-                try {
-                    collection.removeElement(targetAdapter, argumentAdapter, getInteractionInitiatedBy());
-                } catch(Exception ignore) {
-                    // ignore
-                }
-            }
-        }
+        });
+        
+        return runExecutionTask(()->{
+            collection.removeElement(targetAdapter, argumentAdapter, getInteractionInitiatedBy());
+            return null;
+        });
 
-        return null;
+        
     }
 
     // /////////////////////////////////////////////////////////////////
@@ -696,106 +606,71 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
     // /////////////////////////////////////////////////////////////////
 
     private Object handleActionMethod(
-            final ObjectAdapter targetAdapter, final Object[] args,
+            final ObjectAdapter targetAdapter, 
+            final Object[] args,
             final ObjectAction objectAction,
             final ContributeeMember contributeeMember) {
 
         final ObjectAdapter contributeeAdapter;
         final Object[] contributeeArgs;
         if(contributeeMember != null) {
-            final int contributeeParamPosition = contributeeMember.getContributeeParamPosition();
-            final Object contributee = args[contributeeParamPosition];
+            val contributeeParamPosition = contributeeMember.getContributeeParamPosition();
+            val contributee = args[contributeeParamPosition];
+            
             contributeeAdapter = adapterFor(contributee);
-
-            final List<Object> argCopy = _Lists.of(args);
-            argCopy.remove(contributeeParamPosition);
-            contributeeArgs = argCopy.toArray();
+            contributeeArgs = _Arrays.removeByIndex(args, contributeeParamPosition); 
         } else {
             contributeeAdapter = null;
             contributeeArgs = null;
         }
+        
+        val argAdapters = asObjectAdaptersUnderlying(args);
 
-        if(getExecutionMode().shouldEnforceRules()) {
-            if(getExecutionMode().shouldFailFast()) {
-                if(contributeeMember != null) {
-                    checkVisibility(contributeeAdapter, contributeeMember);
-                    checkUsability(contributeeAdapter, contributeeMember);
-                } else {
-                    checkVisibility(targetAdapter, objectAction);
-                    checkUsability(targetAdapter, objectAction);
-                }
-            } else {
-                try {
-                    if(contributeeMember != null) {
-                        checkVisibility(contributeeAdapter, contributeeMember);
-                        checkUsability(contributeeAdapter, contributeeMember);
-                    } else {
-                        checkVisibility(targetAdapter, objectAction);
-                        checkUsability(targetAdapter, objectAction);
-                    }
-                } catch(Exception ex) {
-                    return null;
-                }
-            }
-
-        }
-
-        final ObjectAdapter[] argAdapters = asObjectAdaptersUnderlying(args);
-
-        if(getExecutionMode().shouldEnforceRules()) {
+        runValidationTask(()->{
             if(contributeeMember != null) {
+                checkVisibility(contributeeAdapter, contributeeMember);
+                checkUsability(contributeeAdapter, contributeeMember);
+                
                 if(contributeeMember instanceof ObjectActionContributee) {
-                    final ObjectActionContributee objectActionContributee = (ObjectActionContributee) contributeeMember;
-                    final ObjectAdapter[] contributeeArgAdapters = asObjectAdaptersUnderlying(contributeeArgs);
-
+                    val objectActionContributee = (ObjectActionContributee) contributeeMember;
+                    val contributeeArgAdapters = asObjectAdaptersUnderlying(contributeeArgs);
                     checkValidity(contributeeAdapter, objectActionContributee, contributeeArgAdapters);
                 }
                 // nothing to do for contributed properties or collections
+                
             } else {
+                checkVisibility(targetAdapter, objectAction);
+                checkUsability(targetAdapter, objectAction);
                 checkValidity(targetAdapter, objectAction, argAdapters);
             }
-        }
-
-        if (getExecutionMode().shouldExecute()) {
-            final InteractionInitiatedBy interactionInitiatedBy = getInteractionInitiatedBy();
-
-            final ObjectAdapter mixedInAdapter = null; // if a mixin action, then it will automatically fill in.
-
-
-            ObjectAdapter returnedAdapter;
-
-            if(getExecutionMode().shouldFailFast()) {
-                returnedAdapter = objectAction.execute(
-                        targetAdapter, mixedInAdapter, argAdapters,
-                        interactionInitiatedBy);
-            } else {
-                try {
-                    returnedAdapter = objectAction.execute(
-                            targetAdapter, mixedInAdapter, argAdapters,
-                            interactionInitiatedBy);
-                } catch(Exception ignore) {
-                    // ignore
-                    returnedAdapter = null;
-                }
-
-            }
-
+        });
+        
+        return runExecutionTask(()->{
 
+            val interactionInitiatedBy = getInteractionInitiatedBy();
+            val mixedInAdapter = (ObjectAdapter)null; // if a mixin action, then it will automatically fill in.
+            val returnedAdapter = objectAction.execute(
+                    targetAdapter, mixedInAdapter, argAdapters,
+                    interactionInitiatedBy);
             return ObjectAdapter.Util.unwrapPojo(returnedAdapter);
-        }
-
-        return null;
+            
+        });
+        
     }
 
-    private void checkValidity(final ObjectAdapter targetAdapter, final ObjectAction objectAction, final ObjectAdapter[] argAdapters) {
-        final InteractionResult interactionResult = objectAction.isProposedArgumentSetValid(targetAdapter, argAdapters,
+    private void checkValidity(
+            final ObjectAdapter targetAdapter, 
+            final ObjectAction objectAction, 
+            final ObjectAdapter[] argAdapters) {
+        
+        val interactionResult = objectAction.isProposedArgumentSetValid(targetAdapter, argAdapters,
                 getInteractionInitiatedBy()).getInteractionResult();
         notifyListenersAndVetoIfRequired(interactionResult);
     }
 
     private ObjectAdapter[] asObjectAdaptersUnderlying(final Object[] args) {
 
-        final ObjectAdapter[] argAdapters = new ObjectAdapter[args.length];
+        val argAdapters = new ObjectAdapter[args.length];
         int i = 0;
         for (final Object arg : args) {
             argAdapters[i++] = adapterFor(underlying(arg));
@@ -810,7 +685,7 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
 
     private Object underlying(final Object arg) {
         if (arg instanceof WrappingObject) {
-            final WrappingObject argViewObject = (WrappingObject) arg;
+            val argViewObject = (WrappingObject) arg;
             return argViewObject.__isis_wrapped();
         } else {
             return arg;
@@ -830,26 +705,28 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
     private void checkVisibility(
             final ObjectAdapter targetObjectAdapter,
             final ObjectMember objectMember) {
-        final Consent visibleConsent = objectMember.isVisible(targetObjectAdapter, getInteractionInitiatedBy(), where);
-        final InteractionResult interactionResult = visibleConsent.getInteractionResult();
+        
+        val visibleConsent = objectMember.isVisible(targetObjectAdapter, getInteractionInitiatedBy(), where);
+        val interactionResult = visibleConsent.getInteractionResult();
         notifyListenersAndVetoIfRequired(interactionResult);
     }
 
     private void checkUsability(
             final ObjectAdapter targetObjectAdapter,
             final ObjectMember objectMember) {
-        final InteractionResult interactionResult = objectMember.isUsable(targetObjectAdapter,
-                getInteractionInitiatedBy(), where
-                ).getInteractionResult();
+        
+        val interactionResult = objectMember.isUsable(
+                targetObjectAdapter,
+                getInteractionInitiatedBy(), 
+                where)
+                .getInteractionResult();
         notifyListenersAndVetoIfRequired(interactionResult);
     }
 
-    // /////////////////////////////////////////////////////////////////
-    // notify listeners
-    // /////////////////////////////////////////////////////////////////
+    // -- NOTIFY LISTENERS
 
     private void notifyListenersAndVetoIfRequired(final InteractionResult interactionResult) {
-        final InteractionEvent interactionEvent = interactionResult.getInteractionEvent();
+        val interactionEvent = interactionResult.getInteractionEvent();
         notifyListeners(interactionEvent);
         if (interactionEvent.isVeto()) {
             throw toException(interactionEvent);
@@ -880,13 +757,11 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
         throw new IllegalArgumentException("Provided interactionEvent must be a VisibilityEvent, UsabilityEvent or a ValidityEvent");
     }
 
-    // /////////////////////////////////////////////////////////////////
-    // switching
-    // /////////////////////////////////////////////////////////////////
+    // -- SWITCHING
 
     private ObjectMember locateAndCheckMember(final Method method) {
-        final ObjectSpecificationDefault objectSpecificationDefault = getJavaSpecificationOfOwningClass(method);
-        final ObjectMember member = objectSpecificationDefault.getMember(method);
+        val objectSpecificationDefault = getJavaSpecificationOfOwningClass(method);
+        val member = objectSpecificationDefault.getMember(method);
 
         if (member == null) {
             final String methodName = method.getName();
@@ -911,9 +786,7 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
         return method.equals(__isis_executionMode);
     }
 
-    // /////////////////////////////////////////////////////////////////
-    // Specification lookup
-    // /////////////////////////////////////////////////////////////////
+    // -- SPECIFICATION LOOKUP
 
     private ObjectSpecificationDefault getJavaSpecificationOfOwningClass(final Method method) {
         return getJavaSpecification(method.getDeclaringClass());
@@ -931,9 +804,80 @@ public class DomainObjectInvocationHandler<T> extends DelegatingInvocationHandle
         return getSpecificationLoader().loadSpecification(type);
     }
 
-    // /////////////////////////////////////////////////////////////////
-    // Dependencies
-    // /////////////////////////////////////////////////////////////////
+    // -- HELPER
+    
+    private void runValidationTask(Runnable task) {
+        if(!getExecutionMode().shouldEnforceRules()) {
+            return;
+        }
+        if(getExecutionMode().shouldFailFast()) {
+            task.run();
+        } else {
+            try {
+                task.run();
+            } catch(Exception ex) {
+                // swallow
+            }
+        }
+    }
+    
+//    private void runExecutionTask(Runnable task) {
+//        if(!getExecutionMode().shouldExecute()) {
+//            return;
+//        }
+//        if(getExecutionMode().shouldFailFast()) {
+//            task.run();
+//        } else {
+//            try {
+//                task.run();
+//            } catch(Exception ex) {
+//                // swallow
+//            }
+//        }
+//    }
+    
+    private <X> X runExecutionTask(Supplier<X> task) {
+        if(!getExecutionMode().shouldExecute()) {
+            return null;
+        }
+        if(getExecutionMode().shouldFailFast()) {
+            return task.get();
+        } else {
+            try {
+                return task.get();
+            } catch(Exception ex) {
+                // swallow
+                return null;
+            }
+        }
+    }
+    
+    private Object singleArgUnderlyingElseThrow(Object[] args, String name) {
+        if (args.length != 1) {
+            throw new IllegalArgumentException("Invoking '" + name + "' should only have a single argument");
+        }
+        val argumentObj = underlying(args[0]);
+        if (argumentObj == null) {
+            throw new IllegalArgumentException("Must provide a non-null object to '" + name +"'");
+        }
+        return argumentObj;
+    }
+    
+    private Object singleArgUnderlyingElseNull(Object[] args, String name) {
+        if (args.length != 1) {
+            throw new IllegalArgumentException("Invoking '" + name + "' should only have a single argument");
+        }
+        val argumentObj = underlying(args[0]);
+        return argumentObj;
+    }
+    
+    private void zeroArgsElseThrow(Object[] args, String name) {
+        if (args.length != 0) {
+            throw new IllegalArgumentException("Invoking '" + name + "' should have no arguments");
+        }
+    }
+    
+    // -- DEPENDENCIES
 
     protected SpecificationLoader getSpecificationLoader() {
         return mmContext.getSpecificationLoader();
diff --git a/core/unittestsupport/src/main/java/org/apache/isis/unittestsupport/jmocking/PostponedAction.java b/core/unittestsupport/src/main/java/org/apache/isis/unittestsupport/jmocking/PostponedAction.java
new file mode 100644
index 0000000..b2fa43c
--- /dev/null
+++ b/core/unittestsupport/src/main/java/org/apache/isis/unittestsupport/jmocking/PostponedAction.java
@@ -0,0 +1,46 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.unittestsupport.jmocking;
+
+import java.util.function.Supplier;
+
+import org.hamcrest.Description;
+import org.jmock.api.Action;
+import org.jmock.api.Invocation;
+
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor(staticName = "returnValuePostponed")
+public class PostponedAction implements Action {
+    
+    @NonNull private Supplier<Object> resultSupplier;
+
+    public Object invoke(Invocation invocation) throws Throwable {
+        return resultSupplier.get();
+    }
+
+    public void describeTo(Description description) {
+        description.appendText("returns ");
+        description.appendValue(resultSupplier.get());
+    }
+    
+    
+
+}