You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2013/04/27 20:31:44 UTC

[25/50] [abbrv] git commit: ISIS-233, ISIS-361: validate non-parseable parameters, more tests for RO

ISIS-233, ISIS-361: validate non-parseable parameters, more tests for RO

* validation of invalid arguments when invoking service actions.


Project: http://git-wip-us.apache.org/repos/asf/isis/repo
Commit: http://git-wip-us.apache.org/repos/asf/isis/commit/7bc86916
Tree: http://git-wip-us.apache.org/repos/asf/isis/tree/7bc86916
Diff: http://git-wip-us.apache.org/repos/asf/isis/diff/7bc86916

Branch: refs/heads/master
Commit: 7bc8691649c5f25960d6223d277a2121af34c56d
Parents: b4dd583
Author: Dan Haywood <da...@apache.org>
Authored: Thu Mar 14 09:35:58 2013 +0000
Committer: Dan Haywood <da...@apache.org>
Committed: Sat Apr 27 19:01:20 2013 +0100

----------------------------------------------------------------------
 .../applib/client/RestfulResponse.java             |    4 -
 .../rendering/domainobjects/JsonValueEncoder.java  |    7 +-
 .../RestfulObjectsApplicationExceptionMapper.java  |   34 ++--
 .../server/resources/DomainResourceHelper.java     |   36 +---
 ...stfulObjectsApplicationExceptionMapperTest.java |   36 ++--
 .../server/util/UrlParserUtilsTest.java            |    8 +-
 ...mainServiceTest_req_safe_arg_bad_malformed.java |  124 +++++++++++
 ...ServiceTest_req_safe_optionalarg_resp_list.java |  164 +++++++++++++++
 .../DomainServiceTest_req_safe_refarg_bad.java     |   36 ++--
 ..._req_safe_simplearg_fail_mandatory_missing.java |  137 ++++++++++++
 .../specloader/specimpl/ObjectActionImpl.java      |    3 -
 .../specimpl/ObjectActionParameterAbstract.java    |   65 ++++++
 .../specimpl/ObjectActionParameterParseable.java   |   69 ++-----
 .../specimpl/OneToOneActionParameterImpl.java      |   17 +-
 .../MustSatisfySpecificationOnParameterFacet.java  |    3 +
 .../tck/dom/actions/ActionsEntityRepository.java   |   22 ++
 16 files changed, 616 insertions(+), 149 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/client/RestfulResponse.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/client/RestfulResponse.java b/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/client/RestfulResponse.java
index 758179c..c1869ac 100644
--- a/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/client/RestfulResponse.java
+++ b/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/client/RestfulResponse.java
@@ -30,17 +30,13 @@ import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.Response.Status.Family;
 import javax.ws.rs.core.Response.StatusType;
 
-import org.apache.isis.core.commons.lang.StringUtils;
 import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
 import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
-import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation;
-import org.apache.isis.viewer.restfulobjects.applib.errors.ErrorRepresentation;
 import org.apache.isis.viewer.restfulobjects.applib.util.JsonMapper;
 import org.apache.isis.viewer.restfulobjects.applib.util.Parser;
 import org.codehaus.jackson.JsonParseException;
 import org.codehaus.jackson.map.JsonMappingException;
 
-import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
 
 public class RestfulResponse<T> {

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/component/viewer/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/domainobjects/JsonValueEncoder.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/domainobjects/JsonValueEncoder.java b/component/viewer/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/domainobjects/JsonValueEncoder.java
index 6628dfc..1165c40 100644
--- a/component/viewer/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/domainobjects/JsonValueEncoder.java
+++ b/component/viewer/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/domainobjects/JsonValueEncoder.java
@@ -48,12 +48,15 @@ public final class JsonValueEncoder {
             argRepr.mapPut("invalidReason", reason);
             throw new IllegalArgumentException(reason);
         }
-        final JsonRepresentation argValueRepr = argRepr.getRepresentation("value");
-        if(argValueRepr == null) {
+        if(!argRepr.mapHas("value")) {
             String reason = "No 'value' key";
             argRepr.mapPut("invalidReason", reason);
             throw new IllegalArgumentException(reason);
         }
+        final JsonRepresentation argValueRepr = argRepr.getRepresentation("value");
+        if(argValueRepr == null) {
+            return null;
+        }
         if (!argValueRepr.isValue()) {
             String reason = "Representation must be of a value";
             argRepr.mapPut("invalidReason", reason);

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapper.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapper.java b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapper.java
index 524c43f..a2b7587 100644
--- a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapper.java
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapper.java
@@ -56,6 +56,23 @@ public class RestfulObjectsApplicationExceptionMapper implements ExceptionMapper
         return builder.build();
     }
 
+    static String bodyFor(final RestfulObjectsApplicationException ex) {
+        final JsonRepresentation jsonRepresentation = ex.getBody();
+        if (jsonRepresentation != null) {
+            return jsonRepresentation.toString();
+        }
+        Throwable cause = ex.getCause();
+        if(cause == null) {
+            return null;
+        }
+        try {
+            return JsonMapper.instance().write(ExceptionPojo.create(cause));
+        } catch (final Exception e) {
+            // fallback
+            return "{ \"exception\": \"" + ExceptionUtils.getFullStackTrace(cause) + "\" }";
+        }
+    }
+
     private static class ExceptionPojo {
 
         public static ExceptionPojo create(final Throwable ex) {
@@ -114,20 +131,5 @@ public class RestfulObjectsApplicationExceptionMapper implements ExceptionMapper
 
     }
 
-    static String bodyFor(final RestfulObjectsApplicationException ex) {
-        final JsonRepresentation jsonRepresentation = ex.getBody();
-        if (jsonRepresentation != null) {
-            return jsonRepresentation.toString();
-        }
-        Throwable cause = ex.getCause();
-        if(cause == null) {
-            return null;
-        }
-        try {
-            return JsonMapper.instance().write(ExceptionPojo.create(cause));
-        } catch (final Exception e) {
-            // fallback
-            return "{ \"exception\": \"" + ExceptionUtils.getFullStackTrace(cause) + "\" }";
-        }
-    }
+
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainResourceHelper.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainResourceHelper.java b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainResourceHelper.java
index 67d546d..74d7cdf 100644
--- a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainResourceHelper.java
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainResourceHelper.java
@@ -445,7 +445,7 @@ public final class DomainResourceHelper {
 
         final List<ObjectAdapter> argAdapters = Lists.newArrayList();
         final List<ObjectActionParameter> parameters = action.getParameters();
-        final StringBuilder invalidReasonBuf = new StringBuilder();
+        boolean valid = true;
         for (int i = 0; i < argList.size(); i++) {
             final JsonRepresentation argRepr = argList.get(i);
             final ObjectSpecification paramSpec = parameters.get(i).getSpecification();
@@ -455,22 +455,15 @@ public final class DomainResourceHelper {
 
                 // validate individual arg
                 final ObjectActionParameter parameter = parameters.get(i);
-                if (argAdapter == null) {
-                    // can only happen if this is an optional parameter; nothing to
-                    // do
-                    continue;
-                }
-                if (argAdapter.getSpecification().containsFacet(ValueFacet.class)) {
-                    final Object argPojo = argAdapter.getObject();
-                    final String reasonNotValid = parameter.isValid(objectAdapter, argPojo, null);
-                    if (reasonNotValid != null) {
-                        argRepr.mapPut("invalidReason", reasonNotValid);
-                        appendReasonTo(invalidReasonBuf, "Validation failed, see body for details");
-                    }
+                final Object argPojo = argAdapter!=null?argAdapter.getObject():null;
+                final String reasonNotValid = parameter.isValid(objectAdapter, argPojo, null);
+                if (reasonNotValid != null) {
+                    argRepr.mapPut("invalidReason", reasonNotValid);
+                    valid = false;
                 }
             } catch (final IllegalArgumentException e) {
-                String reason = e.getMessage();
-                appendReasonTo(invalidReasonBuf, reason);
+                argAdapters.add(null);
+                valid = false;
             }
         }
         
@@ -479,23 +472,16 @@ public final class DomainResourceHelper {
         final Consent consent = action.isProposedArgumentSetValid(objectAdapter, argArray);
         if (consent.isVetoed()) {
             arguments.mapPut("x-ro-invalidReason", consent.getReason());
-            appendReasonTo(invalidReasonBuf, "Validation failed, see body for details");
+            valid = false;
         }
 
-        if(invalidReasonBuf.length()>0) {
-            throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.VALIDATION_FAILED, arguments, invalidReasonBuf.toString());
+        if(!valid) {
+            throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.VALIDATION_FAILED, arguments, "Validation failed, see body for details");
         }
         
         return argAdapters;
     }
 
-    private void appendReasonTo(final StringBuilder buf, String reason) {
-        if(buf.length()>0) {
-            buf.append("; ");
-        }
-        buf.append(reason);
-    }
-
     private static List<JsonRepresentation> argListFor(final ObjectAction action, final JsonRepresentation arguments) {
         final List<JsonRepresentation> argList = Lists.newArrayList();
 

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/component/viewer/restfulobjects/server/src/test/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapperTest.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/test/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapperTest.java b/component/viewer/restfulobjects/server/src/test/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapperTest.java
index 6aa9834..3f68110 100644
--- a/component/viewer/restfulobjects/server/src/test/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapperTest.java
+++ b/component/viewer/restfulobjects/server/src/test/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapperTest.java
@@ -57,14 +57,7 @@ public class RestfulObjectsApplicationExceptionMapperTest {
 
         // and then
         final String entity = (String) response.getEntity();
-        assertThat(entity, is(not(nullValue())));
-        final JsonRepresentation jsonRepr = JsonMapper.instance().read(entity, JsonRepresentation.class);
-
-        // then
-        assertThat(jsonRepr.getString("message"), is(nullValue()));
-        assertThat(jsonRepr.getArray("stackTrace"), is(not(nullValue())));
-        assertThat(jsonRepr.getArray("stackTrace").size(), is(greaterThan(0)));
-        assertThat(jsonRepr.getRepresentation("causedBy"), is(nullValue()));
+        assertThat(entity, is(nullValue()));
     }
 
     @Test
@@ -81,18 +74,34 @@ public class RestfulObjectsApplicationExceptionMapperTest {
 
         // and then
         final String entity = (String) response.getEntity();
+        assertThat(entity, is(nullValue()));
+    }
+
+    @Test
+    public void entity_forException() throws Exception {
+        // given
+        final Exception exception = new Exception("barfoo");
+        final RestfulObjectsApplicationException ex = RestfulObjectsApplicationException.createWithCauseAndMessage(HttpStatusCode.BAD_REQUEST, exception, "foobar");
+
+        // when
+        final Response response = exceptionMapper.toResponse(ex);
+        final String entity = (String) response.getEntity();
         assertThat(entity, is(not(nullValue())));
         final JsonRepresentation jsonRepr = JsonMapper.instance().read(entity, JsonRepresentation.class);
 
         // then
-        assertThat(jsonRepr.getString("message"), is(ex.getMessage()));
+        assertThat((String) response.getMetadata().get("Warning").get(0), is("199 RestfulObjects foobar"));
+        assertThat(jsonRepr.getString("message"), is("barfoo"));
+        final JsonRepresentation causedByRepr = jsonRepr.getRepresentation("causedBy");
+        assertThat(causedByRepr, is(nullValue()));
     }
 
     @Test
-    public void entity_withCause() throws Exception {
+    public void entity_forExceptionWithCause() throws Exception {
         // given
-        final Exception cause = new Exception("barfoo");
-        final RestfulObjectsApplicationException ex = RestfulObjectsApplicationException.createWithCauseAndMessage(HttpStatusCode.BAD_REQUEST, cause, "foobar");
+        final Exception cause = new Exception("bozfoz");
+        final Exception exception = new Exception("barfoo", cause);
+        final RestfulObjectsApplicationException ex = RestfulObjectsApplicationException.createWithCauseAndMessage(HttpStatusCode.BAD_REQUEST, exception, "foobar");
 
         // when
         final Response response = exceptionMapper.toResponse(ex);
@@ -101,7 +110,8 @@ public class RestfulObjectsApplicationExceptionMapperTest {
         final JsonRepresentation jsonRepr = JsonMapper.instance().read(entity, JsonRepresentation.class);
 
         // then
-        assertThat(jsonRepr.getString("message"), is(ex.getMessage()));
+        assertThat((String) response.getMetadata().get("Warning").get(0), is("199 RestfulObjects foobar"));
+        assertThat(jsonRepr.getString("message"), is("barfoo"));
         final JsonRepresentation causedByRepr = jsonRepr.getRepresentation("causedBy");
         assertThat(causedByRepr, is(not(nullValue())));
         assertThat(causedByRepr.getString("message"), is(cause.getMessage()));

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/component/viewer/restfulobjects/server/src/test/java/org/apache/isis/viewer/restfulobjects/server/util/UrlParserUtilsTest.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/test/java/org/apache/isis/viewer/restfulobjects/server/util/UrlParserUtilsTest.java b/component/viewer/restfulobjects/server/src/test/java/org/apache/isis/viewer/restfulobjects/server/util/UrlParserUtilsTest.java
index f564834..47c5686 100644
--- a/component/viewer/restfulobjects/server/src/test/java/org/apache/isis/viewer/restfulobjects/server/util/UrlParserUtilsTest.java
+++ b/component/viewer/restfulobjects/server/src/test/java/org/apache/isis/viewer/restfulobjects/server/util/UrlParserUtilsTest.java
@@ -28,7 +28,7 @@ public class UrlParserUtilsTest {
     @Test
     public void oidFromLink() throws Exception {
         final JsonRepresentation link = JsonRepresentation.newMap();
-        link.mapPut("href", "http://localhost/objects/OID:1");
+        link.mapPut("href", "http://localhost/objects/OID/1");
         final String oidFromHref = UrlParserUtils.encodedOidFromLink(link);
         assertEquals("OID:1", oidFromHref);
     }
@@ -36,7 +36,7 @@ public class UrlParserUtilsTest {
     @Test
     public void domainTypeFromLink() throws Exception {
         final JsonRepresentation link = JsonRepresentation.newMap();
-        link.mapPut("href", "http://localhost/domainTypes/com.mycompany.myapp.Customer");
+        link.mapPut("href", "http://localhost/domain-types/com.mycompany.myapp.Customer");
         final String oidFromHref = UrlParserUtils.domainTypeFrom(link);
         assertEquals("com.mycompany.myapp.Customer", oidFromHref);
     }
@@ -44,7 +44,7 @@ public class UrlParserUtilsTest {
     @Test
     public void domainTypeFromLinkTrailingSlash() throws Exception {
         final JsonRepresentation link = JsonRepresentation.newMap();
-        link.mapPut("href", "http://localhost/domainTypes/com.mycompany.myapp.Customer/");
+        link.mapPut("href", "http://localhost/domain-types/com.mycompany.myapp.Customer/");
         final String oidFromHref = UrlParserUtils.domainTypeFrom(link);
         assertEquals("com.mycompany.myapp.Customer", oidFromHref);
     }
@@ -52,7 +52,7 @@ public class UrlParserUtilsTest {
     @Test
     public void domainTypeFromLinkFollowingStuff() throws Exception {
         final JsonRepresentation link = JsonRepresentation.newMap();
-        link.mapPut("href", "http://localhost/domainTypes/com.mycompany.myapp.Customer/otherStuffHere");
+        link.mapPut("href", "http://localhost/domain-types/com.mycompany.myapp.Customer/otherStuffHere");
         final String oidFromHref = UrlParserUtils.domainTypeFrom(link);
         assertEquals("com.mycompany.myapp.Customer", oidFromHref);
     }

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_arg_bad_malformed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_arg_bad_malformed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_arg_bad_malformed.java
new file mode 100644
index 0000000..6196072
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_arg_bad_malformed.java
@@ -0,0 +1,124 @@
+/*
+ *  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.viewer.restfulobjects.tck.domainservice.serviceId.action.invoke;
+
+import static org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers.hasProfile;
+import static org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers.isLink;
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import java.io.IOException;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.LinkRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.Rel;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulHttpMethod;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulMediaType;
+import org.apache.isis.viewer.restfulobjects.applib.client.RestfulClient;
+import org.apache.isis.viewer.restfulobjects.applib.client.RestfulResponse;
+import org.apache.isis.viewer.restfulobjects.applib.client.RestfulResponse.Header;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation.ResultType;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.DomainServiceResource;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ListRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ObjectActionRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.errors.ErrorRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.util.UrlEncodingUtils;
+import org.apache.isis.viewer.restfulobjects.tck.IsisWebServerRule;
+import org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers;
+import org.apache.isis.viewer.restfulobjects.tck.Util;
+import org.codehaus.jackson.JsonParseException;
+import org.codehaus.jackson.map.JsonMappingException;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class DomainServiceTest_req_safe_arg_bad_malformed {
+
+    @Rule
+    public IsisWebServerRule webServerRule = new IsisWebServerRule();
+
+    private RestfulClient client;
+
+    private DomainServiceResource serviceResource;
+
+    @Before
+    public void setUp() throws Exception {
+        client = webServerRule.getClient();
+        serviceResource = client.getDomainServiceResource();
+    }
+
+
+    @Test
+    public void usingClientFollow_whenImplicitlySetToNull() throws Exception {
+
+        // given
+        final JsonRepresentation givenAction = Util.givenAction(client, "ActionsEntities", "subListWithOptionalRange");
+        final ObjectActionRepresentation actionRepr = givenAction.as(ObjectActionRepresentation.class);
+
+        final LinkRepresentation invokeLink = actionRepr.getInvoke();
+
+        assertThat(invokeLink, isLink(client)
+                                    .rel(Rel.INVOKE)
+                                    .httpMethod(RestfulHttpMethod.GET)
+                                    .href(Matchers.endsWith(":39393/services/ActionsEntities/actions/subListWithOptionalRange/invoke"))
+                                    .build());
+        
+        // when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args.mapPut("from", JsonRepresentation.newMap());
+        args.mapPut("to.value", (Integer)null);
+
+        final RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
+        
+        // then
+        then(restfulResponse);
+    }
+
+    
+    @Test
+    public void usingResourceProxy_whenExplicitSetToNull() throws Exception {
+
+        // given, when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args.mapPut("from", JsonRepresentation.newMap());
+        args.mapPut("to.value", (Integer)null);
+
+        Response response = serviceResource.invokeActionQueryOnly("ActionsEntities", "subListWithOptionalRange", UrlEncodingUtils.urlEncode(args));
+        RestfulResponse<ActionResultRepresentation> restfulResponse = RestfulResponse.ofT(response);
+        
+        // then
+        then(restfulResponse);
+    }
+
+    
+    private static void then(RestfulResponse<ActionResultRepresentation> restfulResponse) throws JsonParseException, JsonMappingException, IOException {
+        assertThat(restfulResponse.getHeader(Header.CONTENT_TYPE), hasProfile(MediaType.APPLICATION_JSON));
+        final JsonRepresentation errorRepr = restfulResponse.wraps(JsonRepresentation.class).getEntity();
+
+        assertThat(errorRepr.getString("from.invalidReason"), is("No 'value' key"));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_optionalarg_resp_list.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_optionalarg_resp_list.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_optionalarg_resp_list.java
new file mode 100644
index 0000000..56195dc
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_optionalarg_resp_list.java
@@ -0,0 +1,164 @@
+/*
+ *  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.viewer.restfulobjects.tck.domainservice.serviceId.action.invoke;
+
+import static org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers.hasProfile;
+import static org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers.isLink;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import java.io.IOException;
+
+import javax.ws.rs.core.Response;
+
+import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.LinkRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.Rel;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulHttpMethod;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulMediaType;
+import org.apache.isis.viewer.restfulobjects.applib.client.RestfulClient;
+import org.apache.isis.viewer.restfulobjects.applib.client.RestfulResponse;
+import org.apache.isis.viewer.restfulobjects.applib.client.RestfulResponse.Header;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation.ResultType;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.DomainServiceResource;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ListRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ObjectActionRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.util.UrlEncodingUtils;
+import org.apache.isis.viewer.restfulobjects.tck.IsisWebServerRule;
+import org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers;
+import org.apache.isis.viewer.restfulobjects.tck.Util;
+import org.codehaus.jackson.JsonParseException;
+import org.codehaus.jackson.map.JsonMappingException;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class DomainServiceTest_req_safe_optionalarg_resp_list {
+
+    @Rule
+    public IsisWebServerRule webServerRule = new IsisWebServerRule();
+
+    private RestfulClient client;
+
+    private DomainServiceResource serviceResource;
+
+    @Before
+    public void setUp() throws Exception {
+        client = webServerRule.getClient();
+
+        serviceResource = client.getDomainServiceResource();
+    }
+
+    @Test
+    public void usingClientFollow_whenExplicitlySetToNull() throws Exception {
+
+        // given
+        final JsonRepresentation givenAction = Util.givenAction(client, "ActionsEntities", "subListWithOptionalRange");
+        final ObjectActionRepresentation actionRepr = givenAction.as(ObjectActionRepresentation.class);
+
+        final LinkRepresentation invokeLink = actionRepr.getInvoke();
+
+        assertThat(invokeLink, isLink(client)
+                                    .rel(Rel.INVOKE)
+                                    .httpMethod(RestfulHttpMethod.GET)
+                                    .href(Matchers.endsWith(":39393/services/ActionsEntities/actions/subListWithOptionalRange/invoke"))
+                                    .build());
+        
+        JsonRepresentation args =invokeLink.getArguments();
+        assertThat(args.size(), is(2));
+        assertThat(args, RestfulMatchers.mapHas("from"));
+        assertThat(args, RestfulMatchers.mapHas("to"));
+        
+        // when
+        args.mapPut("from.value", (Integer)null);
+        args.mapPut("to.value", (Integer)null);
+
+        final RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
+        
+        then(restfulResponse);
+    }
+
+
+    @Test
+    public void usingResourceProxy_whenExplicitlySetToNull() throws Exception {
+
+        // given, when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args.mapPut("from.value", (Integer)null);
+        args.mapPut("to.value", (Integer)null);
+        Response response = serviceResource.invokeActionQueryOnly("ActionsEntities", "subListWithOptionalRange", UrlEncodingUtils.urlEncode(args));
+        RestfulResponse<ActionResultRepresentation> restfulResponse = RestfulResponse.ofT(response);
+        
+        // then
+        then(restfulResponse);
+    }
+
+    
+    @Test
+    public void usingClientFollow_whenImplicitlySetToNull() throws Exception {
+
+        // given
+        final JsonRepresentation givenAction = Util.givenAction(client, "ActionsEntities", "subListWithOptionalRange");
+        final ObjectActionRepresentation actionRepr = givenAction.as(ObjectActionRepresentation.class);
+
+        final LinkRepresentation invokeLink = actionRepr.getInvoke();
+
+        assertThat(invokeLink, isLink(client)
+                                    .rel(Rel.INVOKE)
+                                    .httpMethod(RestfulHttpMethod.GET)
+                                    .href(Matchers.endsWith(":39393/services/ActionsEntities/actions/subListWithOptionalRange/invoke"))
+                                    .build());
+        
+        // when
+        JsonRepresentation args = JsonRepresentation.newMap();
+
+        final RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
+        
+        then(restfulResponse);
+    }
+
+    
+    @Test
+    public void usingResourceProxy_whenImplicitlySetToNull() throws Exception {
+
+        // given, when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        Response response = serviceResource.invokeActionQueryOnly("ActionsEntities", "subListWithOptionalRange", UrlEncodingUtils.urlEncode(args));
+        RestfulResponse<ActionResultRepresentation> restfulResponse = RestfulResponse.ofT(response);
+        
+        // then
+        then(restfulResponse);
+    }
+
+    
+    private static void then(RestfulResponse<ActionResultRepresentation> restfulResponse) throws JsonParseException, JsonMappingException, IOException {
+        assertThat(restfulResponse.getHeader(Header.CONTENT_TYPE), hasProfile(RestfulMediaType.APPLICATION_JSON_ACTION_RESULT));
+        final ActionResultRepresentation actionResultRepr = restfulResponse.getEntity();
+        
+        assertThat(actionResultRepr.getResultType(), is(ResultType.LIST));
+        
+        final ListRepresentation listRepr = actionResultRepr.getResult().as(ListRepresentation.class);
+
+        assertThat(listRepr.getValue().size(), is(5));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_refarg_bad.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_refarg_bad.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_refarg_bad.java
index d31ca0e..c93b407 100644
--- a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_refarg_bad.java
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_refarg_bad.java
@@ -20,6 +20,7 @@ package org.apache.isis.viewer.restfulobjects.tck.domainservice.serviceId.action
 
 import static org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers.hasProfile;
 import static org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers.hasStatus;
+import static org.hamcrest.CoreMatchers.*;
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertThat;
 
@@ -82,26 +83,10 @@ public class DomainServiceTest_req_safe_refarg_bad {
         
         RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
 
+        // then
         then(args, restfulResponse);
     }
 
-    private static void then(final JsonRepresentation args, RestfulResponse<ActionResultRepresentation> restfulResponse) throws JsonParseException, JsonMappingException, IOException {
-        // then the response is an error
-        assertThat(restfulResponse, hasStatus(HttpStatusCode.VALIDATION_FAILED));
-
-        assertThat(restfulResponse.getHeader(Header.WARNING), is("'href' does not reference a known entity"));
-
-        // hmmm... what is the media type, though?  the spec doesn't say.  testing for a generic one.
-        assertThat(restfulResponse.getHeader(Header.CONTENT_TYPE), hasProfile(MediaType.APPLICATION_JSON));
-
-        RestfulResponse<JsonRepresentation> restfulResponseOfError = restfulResponse.wraps(JsonRepresentation.class);
-        JsonRepresentation repr = restfulResponseOfError.getEntity();
-        
-        assertThat(repr.getString("searchFor.value.href"), is(args.getString("searchFor.value.href")));
-        assertThat(repr.getString("searchFor.invalidReason"), is("'href' does not reference a known entity"));
-    }
-
-
     @Test
     public void usingResourceProxy() throws Exception {
 
@@ -117,8 +102,25 @@ public class DomainServiceTest_req_safe_refarg_bad {
         Response response = serviceResource.invokeActionQueryOnly("ActionsEntities", "contains", UrlEncodingUtils.urlEncode(args));
         RestfulResponse<ActionResultRepresentation> restfulResponse = RestfulResponse.ofT(response);
         
+        // then
         then(args, restfulResponse);
     }
 
+    private static void then(final JsonRepresentation args, RestfulResponse<ActionResultRepresentation> restfulResponse) throws JsonParseException, JsonMappingException, IOException {
+        // then the response is an error
+        assertThat(restfulResponse, hasStatus(HttpStatusCode.VALIDATION_FAILED));
+        assertThat(restfulResponse.getHeader(Header.WARNING), is("Validation failed, see body for details"));
+
+        // hmmm... what is the media type, though?  the spec doesn't say.  testing for a generic one.
+        assertThat(restfulResponse.getHeader(Header.CONTENT_TYPE), hasProfile(MediaType.APPLICATION_JSON));
+
+        RestfulResponse<JsonRepresentation> restfulResponseOfError = restfulResponse.wraps(JsonRepresentation.class);
+        JsonRepresentation repr = restfulResponseOfError.getEntity();
+        
+        assertThat(repr.getString("searchFor.value.href"), is(args.getString("searchFor.value.href")));
+        assertThat(repr.getString("searchFor.invalidReason"), is("'href' does not reference a known entity"));
+    }
+
+
     
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_simplearg_fail_mandatory_missing.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_simplearg_fail_mandatory_missing.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_simplearg_fail_mandatory_missing.java
new file mode 100644
index 0000000..2116f54
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_simplearg_fail_mandatory_missing.java
@@ -0,0 +1,137 @@
+/*
+ *  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.viewer.restfulobjects.tck.domainservice.serviceId.action.invoke;
+
+import static org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers.hasProfile;
+import static org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers.hasStatus;
+import static org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers.isLink;
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import java.io.IOException;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.LinkRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.Rel;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulHttpMethod;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulMediaType;
+import org.apache.isis.viewer.restfulobjects.applib.client.RestfulClient;
+import org.apache.isis.viewer.restfulobjects.applib.client.RestfulResponse;
+import org.apache.isis.viewer.restfulobjects.applib.client.RestfulResponse.Header;
+import org.apache.isis.viewer.restfulobjects.applib.client.RestfulResponse.HttpStatusCode;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ActionResultRepresentation.ResultType;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.DomainServiceResource;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ListRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.ObjectActionRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.util.UrlEncodingUtils;
+import org.apache.isis.viewer.restfulobjects.tck.IsisWebServerRule;
+import org.apache.isis.viewer.restfulobjects.tck.RestfulMatchers;
+import org.apache.isis.viewer.restfulobjects.tck.Util;
+import org.codehaus.jackson.JsonParseException;
+import org.codehaus.jackson.map.JsonMappingException;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class DomainServiceTest_req_safe_simplearg_fail_mandatory_missing {
+
+    @Rule
+    public IsisWebServerRule webServerRule = new IsisWebServerRule();
+
+    private RestfulClient client;
+
+    private DomainServiceResource serviceResource;
+
+    @Before
+    public void setUp() throws Exception {
+        client = webServerRule.getClient();
+
+        serviceResource = client.getDomainServiceResource();
+    }
+    
+    @Test
+    public void usingClientFollow() throws Exception {
+
+        // given
+        final JsonRepresentation givenAction = Util.givenAction(client, "ActionsEntities", "subList");
+        final ObjectActionRepresentation actionRepr = givenAction.as(ObjectActionRepresentation.class);
+
+        final LinkRepresentation invokeLink = actionRepr.getInvoke();
+
+        assertThat(invokeLink, isLink(client)
+                                    .rel(Rel.INVOKE)
+                                    .httpMethod(RestfulHttpMethod.GET)
+                                    .href(Matchers.endsWith(":39393/services/ActionsEntities/actions/subList/invoke"))
+                                    .build());
+        
+        JsonRepresentation args =invokeLink.getArguments();
+        assertThat(args.size(), is(2));
+        assertThat(args, RestfulMatchers.mapHas("from"));
+        assertThat(args, RestfulMatchers.mapHas("to"));
+        
+        // when
+        args.mapPut("from.value", (Integer)null);
+        args.mapPut("to.value", 0);
+
+        final RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
+        
+        // then
+        thenResponseIsErrorWithInvalidReason(restfulResponse);
+    }
+
+    @Ignore("to write")
+    @Test
+    public void usingResourceProxy() throws Exception {
+
+        // given, when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args.mapPut("from.value", 1);
+        args.mapPut("to.value", 0);
+        Response response = serviceResource.invokeActionQueryOnly("ActionsEntities", "subList", UrlEncodingUtils.urlEncode(args));
+        RestfulResponse<ActionResultRepresentation> restfulResponse = RestfulResponse.ofT(response);
+        
+        // then
+        thenResponseIsErrorWithInvalidReason(restfulResponse);
+    }
+
+    private static void thenResponseIsErrorWithInvalidReason(final RestfulResponse<ActionResultRepresentation> restfulResponse) throws JsonParseException, JsonMappingException, IOException {
+        assertThat(restfulResponse, hasStatus(HttpStatusCode.VALIDATION_FAILED));
+        assertThat(restfulResponse.getHeader(Header.WARNING), is("Validation failed, see body for details"));
+
+        // hmmm... what is the media type, though?  the spec doesn't say.  testing for a generic one.
+        assertThat(restfulResponse.getHeader(Header.CONTENT_TYPE), hasProfile(MediaType.APPLICATION_JSON));
+
+        RestfulResponse<JsonRepresentation> restfulResponseOfError = restfulResponse.wraps(JsonRepresentation.class);
+        JsonRepresentation repr = restfulResponseOfError.getEntity();
+        
+        assertThat(repr.getString("from.invalidReason"), is("Mandatory"));
+        // TODO: really ought to be null, but ObjectActionImpl.isProposedArgumentSetValidResultSet also checks that each argument is valid
+        assertThat(repr.getString("x-ro-invalidReason"), is("Mandatory")); 
+    }
+
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionImpl.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionImpl.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionImpl.java
index cc5e5b7..4bb5fd1 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionImpl.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionImpl.java
@@ -379,9 +379,6 @@ public class ObjectActionImpl extends ObjectMemberAbstract implements ObjectActi
         final InteractionResultSet resultSet = new InteractionResultSet();
         final List<ObjectActionParameter> actionParameters = getParameters();
         if (proposedArguments != null) {
-            // TODO: doesn't seem to be used...
-            // ObjectAdapter[] params = realParameters(object,
-            // proposedArguments);
             for (int i = 0; i < proposedArguments.length; i++) {
                 final ValidityContext<?> ic = actionParameters.get(i).createProposedArgumentInteractionContext(getAuthenticationSession(), invocationMethod, object, proposedArguments, i);
                 InteractionUtils.isValidResultSet(getParameter(i), ic, resultSet);

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionParameterAbstract.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionParameterAbstract.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionParameterAbstract.java
index ea86b46..75951be 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionParameterAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionParameterAbstract.java
@@ -25,6 +25,7 @@ import com.google.common.collect.Lists;
 
 import org.apache.isis.applib.Identifier;
 import org.apache.isis.applib.filter.Filter;
+import org.apache.isis.applib.profiles.Localization;
 import org.apache.isis.applib.query.Query;
 import org.apache.isis.applib.query.QueryFindAllInstances;
 import org.apache.isis.core.commons.authentication.AuthenticationSession;
@@ -36,6 +37,7 @@ import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
 import org.apache.isis.core.metamodel.consent.Allow;
 import org.apache.isis.core.metamodel.consent.Consent;
 import org.apache.isis.core.metamodel.consent.InteractionInvocationMethod;
+import org.apache.isis.core.metamodel.consent.InteractionResultSet;
 import org.apache.isis.core.metamodel.deployment.DeploymentCategory;
 import org.apache.isis.core.metamodel.facetapi.Facet;
 import org.apache.isis.core.metamodel.facetapi.MultiTypedFacet;
@@ -44,9 +46,12 @@ import org.apache.isis.core.metamodel.facets.describedas.DescribedAsFacet;
 import org.apache.isis.core.metamodel.facets.mandatory.MandatoryFacet;
 import org.apache.isis.core.metamodel.facets.named.NamedFacet;
 import org.apache.isis.core.metamodel.facets.object.bounded.BoundedFacetUtils;
+import org.apache.isis.core.metamodel.facets.object.parseable.ParseableFacet;
 import org.apache.isis.core.metamodel.facets.param.choices.ActionParameterChoicesFacet;
 import org.apache.isis.core.metamodel.facets.param.defaults.ActionParameterDefaultsFacet;
 import org.apache.isis.core.metamodel.interactions.ActionArgumentContext;
+import org.apache.isis.core.metamodel.interactions.InteractionUtils;
+import org.apache.isis.core.metamodel.interactions.ValidityContext;
 import org.apache.isis.core.metamodel.spec.DomainModelException;
 import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 import org.apache.isis.core.metamodel.spec.SpecificationLoader;
@@ -307,6 +312,66 @@ public abstract class ObjectActionParameterAbstract implements ObjectActionParam
         return getAuthenticationSessionProvider().getAuthenticationSession();
     }
 
+    
+    
+    // /////////////////////////////////////////////////////////////
+    // Validation
+    // /////////////////////////////////////////////////////////////
+
+    @Override
+    public String isValid(final ObjectAdapter adapter, final Object proposedValue, final Localization localization) {
+        
+        ObjectAdapter proposedValueAdapter = null;
+        ObjectSpecification proposedValueSpec;
+        if(proposedValue != null) {
+            proposedValueAdapter = getAdapterMap().adapterFor(proposedValue);
+            proposedValueSpec = proposedValueAdapter.getSpecification();
+            if(!proposedValueSpec.isOfType(proposedValueSpec)) {
+                proposedValueAdapter = doCoerceProposedValue(adapter, proposedValue, localization);
+            }
+            
+            // check has been coerced into correct type; otherwise give up
+            if(proposedValueAdapter == null) {
+                return null;
+            }
+            proposedValueSpec = proposedValueAdapter.getSpecification();
+            if(!proposedValueSpec.isOfType(proposedValueSpec)) {
+                return null;
+            }
+        }
+        
+
+        final ValidityContext<?> ic = createProposedArgumentInteractionContext(getAuthenticationSession(), InteractionInvocationMethod.BY_USER, adapter, arguments(proposedValueAdapter), getNumber());
+
+        final InteractionResultSet buf = new InteractionResultSet();
+        InteractionUtils.isValidResultSet(this, ic, buf);
+        if (buf.isVetoed()) {
+            return buf.getInteractionResult().getReason();
+        }
+        return null;
+
+    }
+
+    /**
+     * Optional hook for parsing.
+     */
+    protected ObjectAdapter doCoerceProposedValue(ObjectAdapter adapter, Object proposedValue, final Localization localization) {
+        return null;
+    }
+
+    /**
+     * TODO: this is not ideal, because we can only populate the array for
+     * single argument, rather than the entire argument set. Instead, we ought
+     * to do this in two passes, one to build up the argument set as a single
+     * unit, and then validate each in turn.
+     */
+    private ObjectAdapter[] arguments(final ObjectAdapter proposedValue) {
+        final int parameterCount = getAction().getParameterCount();
+        final ObjectAdapter[] arguments = new ObjectAdapter[parameterCount];
+        arguments[getNumber()] = proposedValue;
+        return arguments;
+    }
+
     // /////////////////////////////////////////////////////////////
     // Dependencies (from parent)
     // /////////////////////////////////////////////////////////////

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionParameterParseable.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionParameterParseable.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionParameterParseable.java
index 44de1c9..b976b9c 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionParameterParseable.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/ObjectActionParameterParseable.java
@@ -67,67 +67,24 @@ public class ObjectActionParameterParseable extends ObjectActionParameterAbstrac
         final TypicalLengthFacet facet = getFacet(TypicalLengthFacet.class);
         return facet.value();
     }
-
-    /**
-     * Invoked when tab away, disables the OK button.
-     * 
-     * <p>
-     * Assumed to be invoked {@link InteractionInvocationMethod#BY_USER by user}.
-     */
-    @Override
-    public String isValid(final ObjectAdapter adapter, final Object proposedValue, final Localization localization) {
-
-
-        final ObjectActionParameter parameter = getAction().getParameters().get(getNumber());
-        final ObjectSpecification parameterSpecification = parameter.getSpecification();
-        if(proposedValue == null) {
+    
+    protected ObjectAdapter doCoerceProposedValue(ObjectAdapter adapter, Object proposedValue, final Localization localization) {
+        // try to parse
+        if (!(proposedValue instanceof String)) {
             return null;
         }
-        
-        ObjectAdapter proposedValueAdapter = getAdapterMap().adapterFor(proposedValue);
-        final ObjectSpecification proposedValueSpec = proposedValueAdapter.getSpecification();
-        if(proposedValueSpec.isOfType(proposedValueSpec)) {
-            // nothing to do
-        } else {
-            // try to parse
-            if (!(parameter instanceof ParseableEntryActionParameter)) {
-                return null;
-            }
-            if (!(proposedValue instanceof String)) {
-                return null;
-            }
-            final String proposedString = (String) proposedValue;
-
-            final ParseableFacet p = parameterSpecification.getFacet(ParseableFacet.class);
-            proposedValueAdapter = p.parseTextEntry(null, proposedString, localization);
-        }
-
-        final ValidityContext<?> ic = parameter.createProposedArgumentInteractionContext(getAuthenticationSession(), InteractionInvocationMethod.BY_USER, adapter, arguments(proposedValueAdapter), getNumber());
-
-        final InteractionResultSet buf = new InteractionResultSet();
-        InteractionUtils.isValidResultSet(parameter, ic, buf);
-        if (buf.isVetoed()) {
-            return buf.getInteractionResult().getReason();
+        final String proposedString = (String) proposedValue;
+
+        final ObjectSpecification parameterSpecification = getSpecification();
+        final ParseableFacet p = parameterSpecification.getFacet(ParseableFacet.class);
+        try {
+            final ObjectAdapter parsedAdapter = p.parseTextEntry(null, proposedString, localization);
+            return parsedAdapter;
+        } catch(Exception ex) {
+            return null;
         }
-        return null;
-
     }
 
-    /**
-     * TODO: this is not ideal, because we can only populate the array for
-     * single argument, rather than the entire argument set. Instead, we ought
-     * to do this in two passes, one to build up the argument set as a single
-     * unit, and then validate each in turn.
-     * 
-     * @param proposedValue
-     * @return
-     */
-    private ObjectAdapter[] arguments(final ObjectAdapter proposedValue) {
-        final int parameterCount = getAction().getParameterCount();
-        final ObjectAdapter[] arguments = new ObjectAdapter[parameterCount];
-        arguments[getNumber()] = proposedValue;
-        return arguments;
-    }
 
     // /////////////////////////////////////////////////////////////
     // getInstance

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/OneToOneActionParameterImpl.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/OneToOneActionParameterImpl.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/OneToOneActionParameterImpl.java
index 325e2f6..88e69d7 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/OneToOneActionParameterImpl.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/specloader/specimpl/OneToOneActionParameterImpl.java
@@ -22,10 +22,18 @@ package org.apache.isis.core.metamodel.specloader.specimpl;
 import org.apache.isis.applib.profiles.Localization;
 import org.apache.isis.core.metamodel.adapter.MutableProposedHolder;
 import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.consent.InteractionInvocationMethod;
+import org.apache.isis.core.metamodel.consent.InteractionResultSet;
 import org.apache.isis.core.metamodel.facetapi.FeatureType;
 import org.apache.isis.core.metamodel.facets.TypedHolder;
+import org.apache.isis.core.metamodel.facets.object.parseable.ParseableFacet;
+import org.apache.isis.core.metamodel.interactions.InteractionUtils;
+import org.apache.isis.core.metamodel.interactions.ValidityContext;
 import org.apache.isis.core.metamodel.spec.Instance;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter;
 import org.apache.isis.core.metamodel.spec.feature.OneToOneActionParameter;
+import org.apache.isis.core.metamodel.spec.feature.ParseableEntryActionParameter;
 
 public class OneToOneActionParameterImpl extends ObjectActionParameterAbstract implements OneToOneActionParameter {
 
@@ -43,15 +51,6 @@ public class OneToOneActionParameterImpl extends ObjectActionParameterAbstract i
         return true;
     }
 
-    /**
-     * TODO: need to be able to validate parameters individually, eg if have
-     * <tt>RegEx</tt> annotation; should delegate to the Check framework
-     * instead.
-     */
-    @Override
-    public String isValid(final ObjectAdapter adapter, final Object proposedValue, final Localization localization) {
-        return null;
-    }
 
     // /////////////////////////////////////////////////////////////
     // getInstance

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/param/validate/perspec/MustSatisfySpecificationOnParameterFacet.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/param/validate/perspec/MustSatisfySpecificationOnParameterFacet.java b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/param/validate/perspec/MustSatisfySpecificationOnParameterFacet.java
index 6b7e667..e09b2ca 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/param/validate/perspec/MustSatisfySpecificationOnParameterFacet.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/param/validate/perspec/MustSatisfySpecificationOnParameterFacet.java
@@ -52,6 +52,9 @@ public class MustSatisfySpecificationOnParameterFacet extends FacetAbstract impl
         }
         final ProposedHolder proposedHolder = (ProposedHolder) validityContext;
         final ObjectAdapter targetNO = proposedHolder.getProposed();
+        if(targetNO == null) {
+            return null;
+        }
         final Object targetObject = targetNO.getObject();
         final ReasonBuffer buf = new ReasonBuffer();
         for (final Specification specification : specifications) {

http://git-wip-us.apache.org/repos/asf/isis/blob/7bc86916/core/tck/tck-dom/src/main/java/org/apache/isis/core/tck/dom/actions/ActionsEntityRepository.java
----------------------------------------------------------------------
diff --git a/core/tck/tck-dom/src/main/java/org/apache/isis/core/tck/dom/actions/ActionsEntityRepository.java b/core/tck/tck-dom/src/main/java/org/apache/isis/core/tck/dom/actions/ActionsEntityRepository.java
index 1c89a73..a517b9a 100644
--- a/core/tck/tck-dom/src/main/java/org/apache/isis/core/tck/dom/actions/ActionsEntityRepository.java
+++ b/core/tck/tck-dom/src/main/java/org/apache/isis/core/tck/dom/actions/ActionsEntityRepository.java
@@ -27,6 +27,7 @@ import org.apache.isis.applib.annotation.MemberOrder;
 import org.apache.isis.applib.annotation.MustSatisfy;
 import org.apache.isis.applib.annotation.Named;
 import org.apache.isis.applib.annotation.ObjectType;
+import org.apache.isis.applib.annotation.Optional;
 import org.apache.isis.applib.query.Query;
 import org.apache.isis.applib.query.QueryDefault;
 import org.apache.isis.applib.spec.Specification;
@@ -84,4 +85,25 @@ public class ActionsEntityRepository extends AbstractEntityRepository<ActionsEnt
             return integer.intValue() < 0? "Cannot be less than zero": null;
         }
     }
+
+
+    @ActionSemantics(Of.SAFE)
+    @MemberOrder(sequence = "1")
+    public List<ActionsEntity> subListWithOptionalRange(
+            @Optional
+            @MustSatisfy(IntegerCannotBeNegative.class)
+            @Named("from") Integer from, 
+            @Optional
+            @MustSatisfy(IntegerCannotBeNegative.class)
+            @Named("to") Integer to) {
+        return subList(valueElseDefault(from, 0), valueElseDefault(to, Integer.MAX_VALUE));
+    }
+
+    public String validateSubListWithOptionalRange(final Integer from, final Integer to) {
+        return validateSubList(valueElseDefault(from, 0), valueElseDefault(to, Integer.MAX_VALUE));
+    }
+
+    private static int valueElseDefault(Integer value, int i) {
+        return value != null? value: i;
+    }
 }