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/02/08 14:16:19 UTC

[5/24] ISIS-323: lots more refactoring of RO

http://git-wip-us.apache.org/repos/asf/isis/blob/75f41b75/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
new file mode 100644
index 0000000..87e93d1
--- /dev/null
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/RestfulObjectsApplicationExceptionMapper.java
@@ -0,0 +1,120 @@
+/*
+ *  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.server;
+
+import java.util.List;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import com.google.common.collect.Lists;
+
+import org.apache.commons.lang.exception.ExceptionUtils;
+
+import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulMediaType;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulResponse;
+import org.apache.isis.viewer.restfulobjects.applib.util.JsonMapper;
+
+@Provider
+public class RestfulObjectsApplicationExceptionMapper implements ExceptionMapper<RestfulObjectsApplicationException> {
+
+    @Override
+    public Response toResponse(final RestfulObjectsApplicationException ex) {
+        final ResponseBuilder builder = Response.status(ex.getHttpStatusCode().getJaxrsStatusType()).type(RestfulMediaType.APPLICATION_JSON_ERROR).entity(jsonFor(ex));
+        final String message = ex.getMessage();
+        if (message != null) {
+            builder.header(RestfulResponse.Header.WARNING.getName(), message);
+        }
+        return builder.build();
+    }
+
+    private static class ExceptionPojo {
+
+        public static ExceptionPojo create(final Exception ex) {
+            return new ExceptionPojo(ex);
+        }
+
+        private static String format(final StackTraceElement stackTraceElement) {
+            return stackTraceElement.toString();
+        }
+
+        private final int httpStatusCode;
+        private final String message;
+        private final List<String> stackTrace = Lists.newArrayList();
+        private ExceptionPojo causedBy;
+
+        public ExceptionPojo(final Throwable ex) {
+            httpStatusCode = getHttpStatusCodeIfAny(ex);
+            this.message = ex.getMessage();
+            final StackTraceElement[] stackTraceElements = ex.getStackTrace();
+            for (final StackTraceElement stackTraceElement : stackTraceElements) {
+                this.stackTrace.add(format(stackTraceElement));
+            }
+            final Throwable cause = ex.getCause();
+            if (cause != null && cause != ex) {
+                this.causedBy = new ExceptionPojo(cause);
+            }
+        }
+
+        private int getHttpStatusCodeIfAny(final Throwable ex) {
+            if (!(ex instanceof HasHttpStatusCode)) {
+                return 0;
+            }
+            final HasHttpStatusCode hasHttpStatusCode = (HasHttpStatusCode) ex;
+            return hasHttpStatusCode.getHttpStatusCode().getStatusCode();
+        }
+
+        @SuppressWarnings("unused")
+        public int getHttpStatusCode() {
+            return httpStatusCode;
+        }
+
+        @SuppressWarnings("unused")
+        public String getMessage() {
+            return message;
+        }
+
+        @SuppressWarnings("unused")
+        public List<String> getStackTrace() {
+            return stackTrace;
+        }
+
+        @SuppressWarnings("unused")
+        public ExceptionPojo getCausedBy() {
+            return causedBy;
+        }
+
+    }
+
+    static String jsonFor(final RestfulObjectsApplicationException ex) {
+        final JsonRepresentation jsonRepresentation = ex.getJsonRepresentation();
+        if (jsonRepresentation != null) {
+            return jsonRepresentation.toString();
+        }
+        try {
+            return JsonMapper.instance().write(ExceptionPojo.create(ex));
+        } catch (final Exception e) {
+            // fallback
+            return "{ \"exception\": \"" + ExceptionUtils.getFullStackTrace(ex) + "\" }";
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/75f41b75/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/RuntimeExceptionMapper.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/RuntimeExceptionMapper.java b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/RuntimeExceptionMapper.java
new file mode 100644
index 0000000..994f184
--- /dev/null
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/RuntimeExceptionMapper.java
@@ -0,0 +1,100 @@
+/*
+ *  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.server;
+
+import java.util.List;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import com.google.common.collect.Lists;
+
+import org.apache.commons.lang.exception.ExceptionUtils;
+
+import org.apache.isis.viewer.restfulobjects.applib.RestfulMediaType;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulResponse.HttpStatusCode;
+import org.apache.isis.viewer.restfulobjects.applib.util.JsonMapper;
+
+@Provider
+public class RuntimeExceptionMapper implements ExceptionMapper<RuntimeException> {
+
+    @Override
+    public Response toResponse(final RuntimeException ex) {
+        final ResponseBuilder builder = Response.status(HttpStatusCode.INTERNAL_SERVER_ERROR.getJaxrsStatusType()).type(RestfulMediaType.APPLICATION_JSON_ERROR).entity(jsonFor(ex));
+        return builder.build();
+    }
+
+    private static class ExceptionPojo {
+
+        public static ExceptionPojo create(final Exception ex) {
+            return new ExceptionPojo(ex);
+        }
+
+        private static String format(final StackTraceElement stackTraceElement) {
+            return stackTraceElement.toString();
+        }
+
+        private final String message;
+        private final List<String> stackTrace = Lists.newArrayList();
+        private ExceptionPojo causedBy;
+
+        public ExceptionPojo(final Throwable ex) {
+            this.message = messageFor(ex);
+            final StackTraceElement[] stackTraceElements = ex.getStackTrace();
+            for (final StackTraceElement stackTraceElement : stackTraceElements) {
+                this.stackTrace.add(format(stackTraceElement));
+            }
+            final Throwable cause = ex.getCause();
+            if (cause != null && cause != ex) {
+                this.causedBy = new ExceptionPojo(cause);
+            }
+        }
+
+        private static String messageFor(final Throwable ex) {
+            final String message = ex.getMessage();
+            return message != null ? message : ex.getClass().getName();
+        }
+
+        @SuppressWarnings("unused")
+        public String getMessage() {
+            return message;
+        }
+
+        @SuppressWarnings("unused")
+        public List<String> getStackTrace() {
+            return stackTrace;
+        }
+
+        @SuppressWarnings("unused")
+        public ExceptionPojo getCausedBy() {
+            return causedBy;
+        }
+    }
+
+    static String jsonFor(final Exception ex) {
+        try {
+            return JsonMapper.instance().write(ExceptionPojo.create(ex));
+        } catch (final Exception e) {
+            // fallback
+            return "{ \"exception\": \"" + ExceptionUtils.getFullStackTrace(ex) + "\" }";
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/75f41b75/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyBasicAuth.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyBasicAuth.java b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyBasicAuth.java
new file mode 100644
index 0000000..54bd2b6
--- /dev/null
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyBasicAuth.java
@@ -0,0 +1,79 @@
+/*
+ *  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.server.authentication;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.codec.binary.Base64;
+
+import org.apache.isis.core.commons.authentication.AuthenticationSession;
+import org.apache.isis.core.runtime.authentication.AuthenticationManager;
+import org.apache.isis.core.runtime.authentication.AuthenticationRequestPassword;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+import org.apache.isis.core.webapp.auth.AuthenticationSessionStrategyAbstract;
+
+/**
+ * Implements the HTTP Basic Auth protocol; does not bind the
+ * {@link AuthenticationSession} onto the {@link HttpSession}.
+ */
+public class AuthenticationSessionStrategyBasicAuth extends AuthenticationSessionStrategyAbstract {
+
+    private static Pattern USER_AND_PASSWORD_REGEX = Pattern.compile("^(.+):(.+)$");
+
+    @Override
+    public AuthenticationSession lookupValid(final ServletRequest servletRequest, final ServletResponse servletResponse) {
+
+        final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
+        final String authStr = httpServletRequest.getHeader("Authorization");
+
+        // value should be in the form:
+        // Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+        if (authStr == null || !authStr.startsWith("Basic ")) {
+            return null;
+        }
+        final String digest = authStr.substring(6);
+
+        final String userAndPassword = new String(new Base64().decode(digest.getBytes()));
+        final Matcher matcher = USER_AND_PASSWORD_REGEX.matcher(userAndPassword);
+        if (!matcher.matches()) {
+            return null;
+        }
+
+        final String user = matcher.group(1);
+        final String password = matcher.group(2);
+
+        final AuthenticationSession authSession = getAuthenticationManager().authenticate(new AuthenticationRequestPassword(user, password));
+        return authSession;
+    }
+
+    // //////////////////////////////////////////////////////////
+    // Dependencies (from context)
+    // //////////////////////////////////////////////////////////
+
+    protected AuthenticationManager getAuthenticationManager() {
+        return IsisContext.getAuthenticationManager();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/75f41b75/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyHeader.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyHeader.java b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyHeader.java
new file mode 100644
index 0000000..84bfb73
--- /dev/null
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyHeader.java
@@ -0,0 +1,66 @@
+/*
+ *  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.server.authentication;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+
+import org.apache.isis.core.commons.authentication.AuthenticationSession;
+import org.apache.isis.core.runtime.authentication.standard.SimpleSession;
+import org.apache.isis.core.webapp.auth.AuthenticationSessionStrategyAbstract;
+
+/**
+ * Implements a home-grown protocol, whereby the user id and roles are passed
+ * using custom headers.
+ * 
+ * <p>
+ * Does not bind the {@link AuthenticationSession} onto the {@link HttpSession}.
+ */
+public class AuthenticationSessionStrategyHeader extends AuthenticationSessionStrategyAbstract {
+
+    @Override
+    public AuthenticationSession lookupValid(final ServletRequest servletRequest, final ServletResponse servletResponse) {
+
+        final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
+        final String user = httpServletRequest.getHeader("isis.user");
+        final List<String> roles = rolesFrom(httpServletRequest);
+
+        if (Strings.isNullOrEmpty(user)) {
+            return null;
+        }
+        return new SimpleSession(user, roles);
+    }
+
+    protected List<String> rolesFrom(final HttpServletRequest httpServletRequest) {
+        final String rolesStr = httpServletRequest.getHeader("isis.roles");
+        if (rolesStr == null) {
+            return Collections.emptyList();
+        }
+        return Lists.newArrayList(Splitter.on(",").split(rolesStr));
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/75f41b75/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyTrusted.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyTrusted.java b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyTrusted.java
new file mode 100644
index 0000000..666ccc3
--- /dev/null
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/authentication/AuthenticationSessionStrategyTrusted.java
@@ -0,0 +1,42 @@
+/*
+ *  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.server.authentication;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.apache.isis.core.commons.authentication.AuthenticationSession;
+import org.apache.isis.core.runtime.authentication.exploration.AuthenticationRequestExploration;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+import org.apache.isis.core.webapp.auth.AuthenticationSessionStrategyDefault;
+
+public class AuthenticationSessionStrategyTrusted extends AuthenticationSessionStrategyDefault {
+
+    @Override
+    public AuthenticationSession lookupValid(final ServletRequest servletRequest, final ServletResponse servletResponse) {
+        final AuthenticationSession session = super.lookupValid(servletRequest, servletResponse);
+        if (session != null) {
+            return session;
+        }
+
+        // will always succeed.
+        final AuthenticationRequestExploration request = new AuthenticationRequestExploration();
+        return IsisContext.getAuthenticationManager().authenticate(request);
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/75f41b75/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/embedded/EmbeddedWebViewerRestfulObjects.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/embedded/EmbeddedWebViewerRestfulObjects.java b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/embedded/EmbeddedWebViewerRestfulObjects.java
new file mode 100644
index 0000000..5331201
--- /dev/null
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/embedded/EmbeddedWebViewerRestfulObjects.java
@@ -0,0 +1,61 @@
+/*
+ *  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.server.embedded;
+
+import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher;
+import org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap;
+
+import org.apache.isis.core.commons.lang.MapUtils;
+import org.apache.isis.core.runtime.viewer.web.WebAppSpecification;
+import org.apache.isis.core.runtime.web.EmbeddedWebViewer;
+import org.apache.isis.core.webapp.IsisSessionFilter;
+import org.apache.isis.core.webapp.IsisWebAppBootstrapper;
+import org.apache.isis.core.webapp.content.ResourceCachingFilter;
+import org.apache.isis.core.webapp.content.ResourceServlet;
+import org.apache.isis.viewer.restfulobjects.server.RestfulObjectsApplication;
+import org.apache.isis.viewer.restfulobjects.server.authentication.AuthenticationSessionStrategyTrusted;
+
+final class EmbeddedWebViewerRestfulObjects extends EmbeddedWebViewer {
+    @Override
+    public WebAppSpecification getWebAppSpecification() {
+        final WebAppSpecification webAppSpec = new WebAppSpecification();
+
+        webAppSpec.addServletContextListener(IsisWebAppBootstrapper.class);
+        
+        webAppSpec.addContextParams("isis.viewers", "restfulobjects");
+
+        webAppSpec.addContextParams(RestfulObjectsViewerInstaller.JAVAX_WS_RS_APPLICATION, RestfulObjectsApplication.class.getName());
+
+        webAppSpec.addFilterSpecification(IsisSessionFilter.class, 
+                MapUtils.asMap(
+                        IsisSessionFilter.AUTHENTICATION_SESSION_STRATEGY_KEY, AuthenticationSessionStrategyTrusted.class.getName(),
+                        IsisSessionFilter.WHEN_NO_SESSION_KEY, IsisSessionFilter.WhenNoSession.CONTINUE.name().toLowerCase()), 
+                RestfulObjectsViewerInstaller.EVERYTHING);
+
+        webAppSpec.addFilterSpecification(ResourceCachingFilter.class, RestfulObjectsViewerInstaller.STATIC_CONTENT);
+        webAppSpec.addServletSpecification(ResourceServlet.class, RestfulObjectsViewerInstaller.STATIC_CONTENT);
+
+        
+        webAppSpec.addServletContextListener(ResteasyBootstrap.class);
+        webAppSpec.addServletSpecification(HttpServletDispatcher.class, RestfulObjectsViewerInstaller.ROOT);
+
+
+        return webAppSpec;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/75f41b75/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/embedded/RestfulObjectsViewerInstaller.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/embedded/RestfulObjectsViewerInstaller.java b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/embedded/RestfulObjectsViewerInstaller.java
new file mode 100644
index 0000000..da3f6a7
--- /dev/null
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/embedded/RestfulObjectsViewerInstaller.java
@@ -0,0 +1,52 @@
+/*
+ *  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.server.embedded;
+
+import org.apache.isis.Isis;
+import org.apache.isis.core.runtime.installerregistry.installerapi.IsisViewerInstallerAbstract;
+import org.apache.isis.core.runtime.viewer.IsisViewer;
+import org.apache.isis.core.runtime.viewer.web.WebAppSpecification;
+
+/**
+ * Convenience implementation of a {@link IsisViewer} providing the ability to
+ * configured for the JSON viewer from the {@link Isis command line} using
+ * <tt>--viewer restful</tt> command line option.
+ * 
+ * <p>
+ * In a production deployment the configuration represented by the
+ * {@link WebAppSpecification} would be specified in the <tt>web.xml<tt> file.
+ */
+public class RestfulObjectsViewerInstaller extends IsisViewerInstallerAbstract {
+
+    static final String JAVAX_WS_RS_APPLICATION = "javax.ws.rs.Application";
+
+    protected static final String EVERYTHING = "*";
+    protected static final String ROOT = "/";
+    protected static final String[] STATIC_CONTENT = new String[] { "*.js", "*.gif", "*.png", "*.html" };
+
+    public RestfulObjectsViewerInstaller() {
+        super("restfulobjects");
+    }
+
+    @Override
+    protected IsisViewer doCreateViewer() {
+        return new EmbeddedWebViewerRestfulObjects();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/75f41b75/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainObjectResourceServerside.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainObjectResourceServerside.java b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainObjectResourceServerside.java
new file mode 100644
index 0000000..6e0de9f
--- /dev/null
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainObjectResourceServerside.java
@@ -0,0 +1,409 @@
+/**
+ *  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.server.resources;
+
+import java.io.InputStream;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.ClientResponseType;
+
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.isis.core.runtime.system.transaction.IsisTransactionManager;
+import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulMediaType;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulResponse.HttpStatusCode;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.DomainObjectResource;
+import org.apache.isis.viewer.restfulobjects.applib.links.LinkRepresentation;
+import org.apache.isis.viewer.restfulobjects.rendering.util.UrlParserUtils;
+import org.apache.isis.viewer.restfulobjects.server.RestfulObjectsApplicationException;
+import org.apache.isis.viewer.restfulobjects.server.resources.DomainResourceHelper.Intent;
+import org.apache.isis.viewer.restfulobjects.server.resources.DomainResourceHelper.MemberMode;
+
+@Path("/objects")
+public class DomainObjectResourceServerside extends ResourceAbstract implements DomainObjectResource {
+
+    // //////////////////////////////////////////////////////////
+    // persist
+    // //////////////////////////////////////////////////////////
+
+    @Override
+    @POST
+    @Path("/")
+    @Consumes({ MediaType.WILDCARD })
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_DOMAIN_OBJECT, RestfulMediaType.APPLICATION_JSON_ERROR })
+    @ClientResponseType(entityType = String.class)
+    public Response persist(final InputStream object) {
+
+        init(RepresentationType.DOMAIN_OBJECT, Where.OBJECT_FORMS);
+
+        final String objectStr = DomainResourceHelper.asStringUtf8(object);
+        final JsonRepresentation objectRepr = DomainResourceHelper.readAsMap(objectStr);
+        if (!objectRepr.isMap()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Body is not a map; got %s", objectRepr);
+        }
+
+        final LinkRepresentation describedByLink = objectRepr.getLink("links[rel=describedby]");
+        if (!describedByLink.isLink()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Could not determine type of domain object to persist (no links[rel=describedby] link); got %s", objectRepr);
+        }
+
+        final String domainTypeStr = UrlParserUtils.domainTypeFrom(describedByLink);
+        if (domainTypeStr == null) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Could not determine type of domain object to persist (no href in links[rel=describedby]); got %s", describedByLink);
+        }
+        final ObjectSpecification domainTypeSpec = getSpecificationLoader().loadSpecification(domainTypeStr);
+        if (domainTypeSpec == null) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Could not determine type of domain object to persist (no such class '%s')", domainTypeStr);
+        }
+
+        final ObjectAdapter objectAdapter = getResourceContext().getPersistenceSession().createTransientInstance(domainTypeSpec);
+
+        final JsonRepresentation propertiesList = objectRepr.getArrayEnsured("members[memberType=property]");
+        if (propertiesList == null) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Could not find properties list (no members[memberType=property]); got %s", objectRepr);
+        }
+        if (!DomainResourceHelper.copyOverProperties(getResourceContext(), objectAdapter, propertiesList)) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, objectRepr, "Illegal property value");
+        }
+
+        final Consent validity = objectAdapter.getSpecification().isValid(objectAdapter);
+        if (validity.isVetoed()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, objectRepr, validity.getReason());
+        }
+        getResourceContext().getPersistenceSession().makePersistent(objectAdapter);
+
+        return new DomainResourceHelper(getResourceContext(), objectAdapter).objectRepresentation();
+    }
+
+    // //////////////////////////////////////////////////////////
+    // domain object
+    // //////////////////////////////////////////////////////////
+
+    @Override
+    @GET
+    @Path("/{oid}")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_DOMAIN_OBJECT, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response object(@PathParam("oid") final String oidStr) {
+        init(RepresentationType.DOMAIN_OBJECT, Where.OBJECT_FORMS);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+        return helper.objectRepresentation();
+    }
+
+    @Override
+    @PUT
+    @Path("/{oid}")
+    @Consumes({ MediaType.WILDCARD })
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_DOMAIN_OBJECT, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response object(@PathParam("oid") final String oidStr, final InputStream object) {
+
+        init(RepresentationType.DOMAIN_OBJECT, Where.OBJECT_FORMS);
+
+        final String objectStr = DomainResourceHelper.asStringUtf8(object);
+        final JsonRepresentation objectRepr = DomainResourceHelper.readAsMap(objectStr);
+        if (!objectRepr.isMap()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Body is not a map; got %s", objectRepr);
+        }
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+
+        final JsonRepresentation propertiesList = objectRepr.getArrayEnsured("members[memberType=property]");
+        if (propertiesList == null) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Could not find properties list (no members[memberType=property]); got %s", objectRepr);
+        }
+
+        final IsisTransactionManager transactionManager = getResourceContext().getPersistenceSession().getTransactionManager();
+        transactionManager.startTransaction();
+        try {
+            if (!DomainResourceHelper.copyOverProperties(getResourceContext(), objectAdapter, propertiesList)) {
+                transactionManager.abortTransaction();
+                throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, objectRepr, "Illegal property value");
+            }
+
+            final Consent validity = objectAdapter.getSpecification().isValid(objectAdapter);
+            if (validity.isVetoed()) {
+                transactionManager.abortTransaction();
+                throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, objectRepr, validity.getReason());
+            }
+
+            transactionManager.endTransaction();
+        } finally {
+            // in case an exception got thrown somewhere...
+            if (!transactionManager.getTransaction().getState().isComplete()) {
+                transactionManager.abortTransaction();
+            }
+        }
+
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+        return helper.objectRepresentation();
+    }
+
+    // //////////////////////////////////////////////////////////
+    // domain object property
+    // //////////////////////////////////////////////////////////
+
+    @Override
+    @GET
+    @Path("/{oid}/properties/{propertyId}")
+    @Consumes({ MediaType.WILDCARD })
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_PROPERTY, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response propertyDetails(@PathParam("oid") final String oidStr, @PathParam("propertyId") final String propertyId) {
+        init(RepresentationType.OBJECT_PROPERTY, Where.OBJECT_FORMS);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        return helper.propertyDetails(objectAdapter, propertyId, MemberMode.NOT_MUTATING, Caching.NONE, getResourceContext().getWhere());
+    }
+
+    @Override
+    @PUT
+    @Path("/{oid}/properties/{propertyId}")
+    @Consumes({ MediaType.WILDCARD })
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response modifyProperty(@PathParam("oid") final String oidStr, @PathParam("propertyId") final String propertyId, final InputStream body) {
+        init(Where.OBJECT_FORMS);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        final OneToOneAssociation property = helper.getPropertyThatIsVisibleAndUsable(propertyId, Intent.MUTATE, getResourceContext().getWhere());
+
+        final ObjectSpecification propertySpec = property.getSpecification();
+        final String bodyAsString = DomainResourceHelper.asStringUtf8(body);
+
+        final ObjectAdapter argAdapter = helper.parseAsMapWithSingleValue(propertySpec, bodyAsString);
+
+        final Consent consent = property.isAssociationValid(objectAdapter, argAdapter);
+        if (consent.isVetoed()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.UNAUTHORIZED, consent.getReason());
+        }
+
+        property.set(objectAdapter, argAdapter);
+
+        return helper.propertyDetails(objectAdapter, propertyId, MemberMode.MUTATING, Caching.NONE, getResourceContext().getWhere());
+    }
+
+    @Override
+    @DELETE
+    @Path("/{oid}/properties/{propertyId}")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response clearProperty(@PathParam("oid") final String oidStr, @PathParam("propertyId") final String propertyId) {
+        init(Where.OBJECT_FORMS);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        final OneToOneAssociation property = helper.getPropertyThatIsVisibleAndUsable(propertyId, Intent.MUTATE, getResourceContext().getWhere());
+
+        final Consent consent = property.isAssociationValid(objectAdapter, null);
+        if (consent.isVetoed()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.UNAUTHORIZED, consent.getReason());
+        }
+
+        property.set(objectAdapter, null);
+
+        return helper.propertyDetails(objectAdapter, propertyId, MemberMode.MUTATING, Caching.NONE, getResourceContext().getWhere());
+    }
+
+    // //////////////////////////////////////////////////////////
+    // domain object collection
+    // //////////////////////////////////////////////////////////
+
+    @Override
+    @GET
+    @Path("/{oid}/collections/{collectionId}")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_COLLECTION, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response accessCollection(@PathParam("oid") final String oidStr, @PathParam("collectionId") final String collectionId) {
+        init(RepresentationType.OBJECT_COLLECTION, Where.PARENTED_TABLES);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        return helper.collectionDetails(objectAdapter, collectionId, MemberMode.NOT_MUTATING, Caching.NONE, getResourceContext().getWhere());
+    }
+
+    @Override
+    @PUT
+    @Path("/{oid}/collections/{collectionId}")
+    @Consumes({ MediaType.WILDCARD })
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response addToSet(@PathParam("oid") final String oidStr, @PathParam("collectionId") final String collectionId, final InputStream body) {
+        init(Where.PARENTED_TABLES);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        final OneToManyAssociation collection = helper.getCollectionThatIsVisibleAndUsable(collectionId, Intent.MUTATE, getResourceContext().getWhere());
+
+        if (!collection.getCollectionSemantics().isSet()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Collection '%s' does not have set semantics", collectionId);
+        }
+
+        final ObjectSpecification collectionSpec = collection.getSpecification();
+        final String bodyAsString = DomainResourceHelper.asStringUtf8(body);
+        final ObjectAdapter argAdapter = helper.parseAsMapWithSingleValue(collectionSpec, bodyAsString);
+
+        final Consent consent = collection.isValidToAdd(objectAdapter, argAdapter);
+        if (consent.isVetoed()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.UNAUTHORIZED, consent.getReason());
+        }
+
+        collection.addElement(objectAdapter, argAdapter);
+
+        return helper.collectionDetails(objectAdapter, collectionId, MemberMode.MUTATING, Caching.NONE, getResourceContext().getWhere());
+    }
+
+    @Override
+    @POST
+    @Path("/{oid}/collections/{collectionId}")
+    @Consumes({ MediaType.WILDCARD })
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response addToList(@PathParam("oid") final String oidStr, @PathParam("collectionId") final String collectionId, final InputStream body) {
+        init(Where.PARENTED_TABLES);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        final OneToManyAssociation collection = helper.getCollectionThatIsVisibleAndUsable(collectionId, Intent.MUTATE, getResourceContext().getWhere());
+
+        if (!collection.getCollectionSemantics().isListOrArray()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.METHOD_NOT_ALLOWED, "Collection '%s' does not have list or array semantics", collectionId);
+        }
+
+        final ObjectSpecification collectionSpec = collection.getSpecification();
+        final String bodyAsString = DomainResourceHelper.asStringUtf8(body);
+        final ObjectAdapter argAdapter = helper.parseAsMapWithSingleValue(collectionSpec, bodyAsString);
+
+        final Consent consent = collection.isValidToAdd(objectAdapter, argAdapter);
+        if (consent.isVetoed()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.UNAUTHORIZED, consent.getReason());
+        }
+
+        collection.addElement(objectAdapter, argAdapter);
+
+        return helper.collectionDetails(objectAdapter, collectionId, MemberMode.MUTATING, Caching.NONE, getResourceContext().getWhere());
+    }
+
+    @Override
+    @DELETE
+    @Path("/{oid}/collections/{collectionId}")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response removeFromCollection(@PathParam("oid") final String oidStr, @PathParam("collectionId") final String collectionId) {
+        init(Where.PARENTED_TABLES);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        final OneToManyAssociation collection = helper.getCollectionThatIsVisibleAndUsable(collectionId, Intent.MUTATE, getResourceContext().getWhere());
+
+        final ObjectSpecification collectionSpec = collection.getSpecification();
+        final ObjectAdapter argAdapter = helper.parseAsMapWithSingleValue(collectionSpec, getResourceContext().getQueryString());
+
+        final Consent consent = collection.isValidToRemove(objectAdapter, argAdapter);
+        if (consent.isVetoed()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.UNAUTHORIZED, consent.getReason());
+        }
+
+        collection.removeElement(objectAdapter, argAdapter);
+
+        return helper.collectionDetails(objectAdapter, collectionId, MemberMode.MUTATING, Caching.NONE, getResourceContext().getWhere());
+    }
+
+    // //////////////////////////////////////////////////////////
+    // domain object action
+    // //////////////////////////////////////////////////////////
+
+    @Override
+    @GET
+    @Path("/{oid}/actions/{actionId}")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_ACTION, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response actionPrompt(@PathParam("oid") final String oidStr, @PathParam("actionId") final String actionId) {
+        init(RepresentationType.OBJECT_ACTION, Where.OBJECT_FORMS);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        return helper.actionPrompt(actionId, getResourceContext().getWhere());
+    }
+
+    // //////////////////////////////////////////////////////////
+    // domain object action invoke
+    // //////////////////////////////////////////////////////////
+
+    @Override
+    @GET
+    @Path("/{oid}/actions/{actionId}/invoke")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ACTION_RESULT, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response invokeActionQueryOnly(@PathParam("oid") final String oidStr, @PathParam("actionId") final String actionId) {
+        init(RepresentationType.ACTION_RESULT, Where.STANDALONE_TABLES);
+
+        final JsonRepresentation arguments = getResourceContext().getQueryStringAsJsonRepr();
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        return helper.invokeActionQueryOnly(actionId, arguments, getResourceContext().getWhere());
+    }
+
+    @Override
+    @PUT
+    @Path("/{oid}/actions/{actionId}/invoke")
+    @Consumes({ MediaType.WILDCARD })
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ACTION_RESULT, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response invokeActionIdempotent(@PathParam("oid") final String oidStr, @PathParam("actionId") final String actionId, final InputStream arguments) {
+        init(RepresentationType.ACTION_RESULT, Where.STANDALONE_TABLES);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        return helper.invokeActionIdempotent(actionId, arguments, getResourceContext().getWhere());
+    }
+
+    @Override
+    @POST
+    @Path("/{oid}/actions/{actionId}/invoke")
+    @Consumes({ MediaType.WILDCARD })
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ACTION_RESULT, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response invokeAction(@PathParam("oid") final String oidStr, @PathParam("actionId") final String actionId, final InputStream body) {
+        init(RepresentationType.ACTION_RESULT, Where.STANDALONE_TABLES);
+
+        final ObjectAdapter objectAdapter = getObjectAdapter(oidStr);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
+
+        return helper.invokeAction(actionId, body, getResourceContext().getWhere());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/75f41b75/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
new file mode 100644
index 0000000..88e4dd9
--- /dev/null
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainResourceHelper.java
@@ -0,0 +1,605 @@
+
+/**
+ *  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.server.resources;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+
+import org.apache.isis.applib.annotation.ActionSemantics;
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.core.commons.authentication.AuthenticationSession;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.adapter.version.Version;
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.core.metamodel.facets.object.value.ValueFacet;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
+import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAssociationFilters;
+import org.apache.isis.core.metamodel.spec.feature.ObjectMember;
+import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulResponse.HttpStatusCode;
+import org.apache.isis.viewer.restfulobjects.applib.util.JsonMapper;
+import org.apache.isis.viewer.restfulobjects.applib.util.UrlEncodingUtils;
+import org.apache.isis.viewer.restfulobjects.rendering.RendererContext;
+import org.apache.isis.viewer.restfulobjects.rendering.RendererFactory;
+import org.apache.isis.viewer.restfulobjects.rendering.RendererFactoryRegistry;
+import org.apache.isis.viewer.restfulobjects.rendering.ReprRendererException;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.AbstractObjectMemberReprRenderer;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ActionResultReprRenderer;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.DomainObjectLinkTo;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.DomainObjectReprRenderer;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.JsonValueEncoder;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.MemberType;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectActionReprRenderer;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAdapterLinkTo;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndAction;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndActionInvocation;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndCollection;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndProperty;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectCollectionReprRenderer;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectPropertyReprRenderer;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.JsonValueEncoder.ExpectedStringRepresentingValueException;
+import org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils;
+import org.apache.isis.viewer.restfulobjects.rendering.util.UrlDecoderUtils;
+import org.apache.isis.viewer.restfulobjects.rendering.util.UrlParserUtils;
+import org.apache.isis.viewer.restfulobjects.server.ResourceContext;
+import org.apache.isis.viewer.restfulobjects.server.RestfulObjectsApplicationException;
+import org.apache.isis.viewer.restfulobjects.server.resources.ResourceAbstract.Caching;
+import org.codehaus.jackson.JsonParseException;
+import org.codehaus.jackson.map.JsonMappingException;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.Lists;
+import com.google.common.io.ByteStreams;
+
+public final class DomainResourceHelper {
+
+    private static final DateFormat ETAG_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
+
+    private final RendererContext resourceContext;
+    private ObjectAdapterLinkTo adapterLinkTo;
+
+    private final ObjectAdapter objectAdapter;
+
+    public DomainResourceHelper(final RendererContext resourceContext, final ObjectAdapter objectAdapter) {
+        this.resourceContext = resourceContext;
+        this.objectAdapter = objectAdapter;
+        using(new DomainObjectLinkTo());
+    }
+
+    public DomainResourceHelper using(final ObjectAdapterLinkTo linkTo) {
+        adapterLinkTo = linkTo;
+        adapterLinkTo.usingUrlBase(resourceContext).with(objectAdapter);
+        return this;
+    }
+
+    // //////////////////////////////////////////////////////////////
+    // multiple properties (persist or multi-property update)
+    // //////////////////////////////////////////////////////////////
+
+    static boolean copyOverProperties(final RendererContext resourceContext, final ObjectAdapter objectAdapter, final JsonRepresentation propertiesList) {
+        final ObjectSpecification objectSpec = objectAdapter.getSpecification();
+        final List<ObjectAssociation> properties = objectSpec.getAssociations(ObjectAssociationFilters.PROPERTIES);
+        boolean allOk = true;
+
+        for (final ObjectAssociation association : properties) {
+            final OneToOneAssociation property = (OneToOneAssociation) association;
+            final ObjectSpecification propertySpec = property.getSpecification();
+            final String id = property.getId();
+            final JsonRepresentation propertyRepr = propertiesList.getRepresentation("[id=%s]", id);
+            if (propertyRepr == null) {
+                if (property.isMandatory()) {
+                    throw new IllegalArgumentException(String.format("Mandatory field %s missing", property.getName()));
+                }
+                continue;
+            }
+            final JsonRepresentation valueRepr = propertyRepr.getRepresentation("value");
+            final Consent usable = property.isUsable(resourceContext.getAuthenticationSession() , objectAdapter, resourceContext.getWhere());
+            if (usable.isVetoed()) {
+                propertyRepr.mapPut("invalidReason", usable.getReason());
+                allOk = false;
+                continue;
+            }
+            final ObjectAdapter valueAdapter = objectAdapterFor(resourceContext, propertySpec, valueRepr);
+            final Consent consent = property.isAssociationValid(objectAdapter, valueAdapter);
+            if (consent.isAllowed()) {
+                try {
+                    property.set(objectAdapter, valueAdapter);
+                } catch (final IllegalArgumentException ex) {
+                    propertyRepr.mapPut("invalidReason", ex.getMessage());
+                    allOk = false;
+                }
+            } else {
+                propertyRepr.mapPut("invalidReason", consent.getReason());
+                allOk = false;
+            }
+        }
+
+        return allOk;
+    }
+
+    // //////////////////////////////////////////////////////////////
+    // propertyDetails
+    // //////////////////////////////////////////////////////////////
+
+    public Response objectRepresentation() {
+        final RendererFactory rendererFactory = getRendererFactoryRegistry().find(RepresentationType.DOMAIN_OBJECT);
+
+        final DomainObjectReprRenderer renderer = (DomainObjectReprRenderer) rendererFactory.newRenderer(resourceContext, null, JsonRepresentation.newMap());
+        renderer.with(objectAdapter).includesSelf();
+
+        final ResponseBuilder respBuilder = ResourceAbstract.responseOfOk(renderer, Caching.NONE);
+
+        final Version version = objectAdapter.getVersion();
+        if (version != null && version.getTime() != null) {
+            respBuilder.tag(ETAG_FORMAT.format(version.getTime()));
+        }
+        return respBuilder.build();
+    }
+
+    // //////////////////////////////////////////////////////////////
+    // propertyDetails
+    // //////////////////////////////////////////////////////////////
+
+    public enum MemberMode {
+        NOT_MUTATING {
+            @Override
+            public void apply(final AbstractObjectMemberReprRenderer<?, ?> renderer) {
+                renderer.asStandalone();
+            }
+        },
+        MUTATING {
+            @Override
+            public void apply(final AbstractObjectMemberReprRenderer<?, ?> renderer) {
+                renderer.asMutated();
+            }
+        };
+
+        public abstract void apply(AbstractObjectMemberReprRenderer<?, ?> renderer);
+    }
+
+    Response propertyDetails(final ObjectAdapter objectAdapter, final String propertyId, final MemberMode memberMode, final Caching caching, Where where) {
+
+        final OneToOneAssociation property = getPropertyThatIsVisibleAndUsable(propertyId, Intent.ACCESS, where);
+
+        final RendererFactory factory = getRendererFactoryRegistry().find(RepresentationType.OBJECT_PROPERTY);
+        final ObjectPropertyReprRenderer renderer = (ObjectPropertyReprRenderer) factory.newRenderer(resourceContext, null, JsonRepresentation.newMap());
+
+        renderer.with(new ObjectAndProperty(objectAdapter, property)).usingLinkTo(adapterLinkTo);
+
+        memberMode.apply(renderer);
+
+        return ResourceAbstract.responseOfOk(renderer, caching).build();
+    }
+
+    // //////////////////////////////////////////////////////////////
+    // collectionDetails
+    // //////////////////////////////////////////////////////////////
+
+    Response collectionDetails(final ObjectAdapter objectAdapter, final String collectionId, final MemberMode memberMode, final Caching caching, Where where) {
+
+        final OneToManyAssociation collection = getCollectionThatIsVisibleAndUsable(collectionId, Intent.ACCESS, where);
+
+        final RendererFactory factory = RendererFactoryRegistry.instance.find(RepresentationType.OBJECT_COLLECTION);
+        final ObjectCollectionReprRenderer renderer = (ObjectCollectionReprRenderer) factory.newRenderer(resourceContext, null, JsonRepresentation.newMap());
+
+        renderer.with(new ObjectAndCollection(objectAdapter, collection)).usingLinkTo(adapterLinkTo);
+
+        memberMode.apply(renderer);
+
+        return ResourceAbstract.responseOfOk(renderer, caching).build();
+    }
+
+    // //////////////////////////////////////////////////////////////
+    // action Prompt
+    // //////////////////////////////////////////////////////////////
+
+    Response actionPrompt(final String actionId, Where where) {
+        final ObjectAction action = getObjectActionThatIsVisibleAndUsable(actionId, Intent.ACCESS, where);
+
+        final RendererFactory factory = getRendererFactoryRegistry().find(RepresentationType.OBJECT_ACTION);
+        final ObjectActionReprRenderer renderer = (ObjectActionReprRenderer) factory.newRenderer(resourceContext, null, JsonRepresentation.newMap());
+
+        renderer.with(new ObjectAndAction(objectAdapter, action)).usingLinkTo(adapterLinkTo).asStandalone();
+
+        return ResourceAbstract.responseOfOk(renderer, Caching.NONE).build();
+    }
+
+    // //////////////////////////////////////////////////////////////
+    // invoke action
+    // //////////////////////////////////////////////////////////////
+
+    enum Intent {
+        ACCESS, MUTATE;
+
+        public boolean isMutate() {
+            return this == MUTATE;
+        }
+    }
+
+    Response invokeActionQueryOnly(final String actionId, final JsonRepresentation arguments, Where where) {
+        final ObjectAction action = getObjectActionThatIsVisibleAndUsable(actionId, Intent.ACCESS, where);
+
+        final ActionSemantics.Of actionSemantics = action.getSemantics();
+        if (actionSemantics != ActionSemantics.Of.SAFE) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.METHOD_NOT_ALLOWED, "Method not allowed; action '%s' is not query only", action.getId());
+        }
+
+        return invokeActionUsingAdapters(action, arguments);
+    }
+
+    Response invokeActionIdempotent(final String actionId, final InputStream body, Where where) {
+
+        final ObjectAction action = getObjectActionThatIsVisibleAndUsable(actionId, Intent.MUTATE, where);
+
+        final ActionSemantics.Of actionSemantics = action.getSemantics();
+        if (!actionSemantics.isIdempotentInNature()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.METHOD_NOT_ALLOWED, "Method not allowed; action '%s' is not idempotent", action.getId());
+        }
+        final String bodyAsString = asStringUtf8(body);
+        final JsonRepresentation arguments = readAsMap(bodyAsString);
+
+        return invokeActionUsingAdapters(action, arguments);
+    }
+
+    Response invokeAction(final String actionId, final InputStream body, Where where) {
+        final ObjectAction action = getObjectActionThatIsVisibleAndUsable(actionId, Intent.MUTATE, where);
+
+        final String bodyAsString = asStringUtf8(body);
+        final JsonRepresentation arguments = readAsMap(bodyAsString);
+
+        return invokeActionUsingAdapters(action, arguments);
+    }
+
+    Response invokeActionUsingAdapters(final ObjectAction action, final JsonRepresentation arguments) {
+
+        final List<ObjectAdapter> argAdapters = parseArguments(action, arguments);
+
+        // validate individual args
+        final List<ObjectActionParameter> parameters = action.getParameters();
+        for (int i = 0; i < parameters.size(); i++) {
+            final ObjectActionParameter parameter = parameters.get(i);
+            final ObjectAdapter argAdapter = argAdapters.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 arg = argAdapter.getObject();
+                final String reasonNotValid = parameter.isValid(objectAdapter, arg, null);
+                if (reasonNotValid != null) {
+                    throw RestfulObjectsApplicationException.create(HttpStatusCode.NOT_ACCEPTABLE, reasonNotValid);
+                }
+            }
+        }
+
+        // validate all args
+        final ObjectAdapter[] argArray = argAdapters.toArray(new ObjectAdapter[0]);
+        final Consent consent = action.isProposedArgumentSetValid(objectAdapter, argArray);
+        if (consent.isVetoed()) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.NOT_ACCEPTABLE, consent.getReason());
+        }
+
+        // invoke
+        final ObjectAdapter returnedAdapter = action.execute(objectAdapter, argArray);
+
+        // response (void)
+        final RendererFactory factory = getRendererFactoryRegistry().find(RepresentationType.ACTION_RESULT);
+        final ActionResultReprRenderer renderer = (ActionResultReprRenderer) factory.newRenderer(resourceContext, null, JsonRepresentation.newMap());
+
+        renderer.with(new ObjectAndActionInvocation(objectAdapter, action, arguments, returnedAdapter)).using(adapterLinkTo);
+
+        final ResponseBuilder respBuilder = ResourceAbstract.responseOfOk(renderer, Caching.NONE);
+
+        final Version version = objectAdapter.getVersion();
+        ResourceAbstract.addLastModifiedAndETagIfAvailable(respBuilder, version);
+
+        return respBuilder.build();
+    }
+
+    /**
+     *
+     * @param resourceContext
+     * @param objectSpec
+     *            - the {@link ObjectSpecification} to interpret the object as.
+     * @param node
+     *            - expected to be either a String or a Map (ie from within a
+     *            List, built by parsing a JSON structure).
+     */
+    private static ObjectAdapter objectAdapterFor(final RendererContext resourceContext, final ObjectSpecification objectSpec, final JsonRepresentation representation) {
+
+        if (representation == null) {
+            return null;
+        }
+
+        // value (encodable)
+        if (objectSpec.isEncodeable()) {
+            return new JsonValueEncoder().asAdapter(objectSpec, representation);
+        }
+
+        // reference
+        if (!representation.isLink()) {
+            throw new ExpectedMapRepresentingLinkException();
+        }
+        final JsonRepresentation argLink = representation.asLink();
+        final String oidFromHref = UrlParserUtils.oidFromLink(argLink);
+        if (oidFromHref == null) {
+            throw new ExpectedMapRepresentingLinkException();
+        }
+
+        final ObjectAdapter objectAdapter = OidUtils.getObjectAdapter(resourceContext, oidFromHref);
+        if (objectAdapter == null) {
+            throw new UnknownOidException(oidFromHref);
+        }
+        return objectAdapter;
+    }
+
+    /**
+     * Similar to
+     * {@link #objectAdapterFor(ResourceContext, ObjectSpecification, Object)},
+     * however the object being interpreted is a String holding URL encoded JSON
+     * (rather than having already been parsed into a Map representation).
+     *
+     * @throws IOException
+     * @throws JsonMappingException
+     * @throws JsonParseException
+     */
+    ObjectAdapter objectAdapterFor(final ObjectSpecification spec, final String urlEncodedJson) throws JsonParseException, JsonMappingException, IOException {
+
+        final String json = UrlDecoderUtils.urlDecode(urlEncodedJson);
+        final JsonRepresentation representation = JsonMapper.instance().read(json);
+        return objectAdapterFor(resourceContext, spec, representation);
+    }
+
+    private static class ExpectedMapRepresentingLinkException extends IllegalArgumentException {
+        private static final long serialVersionUID = 1L;
+    }
+
+    private static class UnknownOidException extends IllegalArgumentException {
+        private static final long serialVersionUID = 1L;
+
+        public UnknownOidException(final String oid) {
+            super(UrlDecoderUtils.urlDecode(oid));
+        }
+    }
+
+    // ///////////////////////////////////////////////////////////////////
+    // get{MemberType}ThatIsVisibleAndUsable
+    // ///////////////////////////////////////////////////////////////////
+
+    protected OneToOneAssociation getPropertyThatIsVisibleAndUsable(final String propertyId, final Intent intent, Where where) {
+
+        final ObjectAssociation association = objectAdapter.getSpecification().getAssociation(propertyId);
+        if (association == null || !association.isOneToOneAssociation()) {
+            throwNotFoundException(propertyId, MemberType.PROPERTY);
+        }
+        final OneToOneAssociation property = (OneToOneAssociation) association;
+        return memberThatIsVisibleAndUsable(property, MemberType.PROPERTY, intent, where);
+    }
+
+    protected OneToManyAssociation getCollectionThatIsVisibleAndUsable(final String collectionId, final Intent intent, Where where) {
+
+        final ObjectAssociation association = objectAdapter.getSpecification().getAssociation(collectionId);
+        if (association == null || !association.isOneToManyAssociation()) {
+            throwNotFoundException(collectionId, MemberType.COLLECTION);
+        }
+        final OneToManyAssociation collection = (OneToManyAssociation) association;
+        return memberThatIsVisibleAndUsable(collection, MemberType.COLLECTION, intent, where);
+    }
+
+    protected ObjectAction getObjectActionThatIsVisibleAndUsable(final String actionId, final Intent intent, Where where) {
+
+        final ObjectAction action = objectAdapter.getSpecification().getObjectAction(actionId);
+        if (action == null) {
+            throwNotFoundException(actionId, MemberType.ACTION);
+        }
+
+        return memberThatIsVisibleAndUsable(action, MemberType.ACTION, intent, where);
+    }
+
+    protected <T extends ObjectMember> T memberThatIsVisibleAndUsable(final T objectMember, final MemberType memberType, final Intent intent, Where where) {
+        final String memberId = objectMember.getId();
+        final AuthenticationSession authenticationSession = resourceContext.getAuthenticationSession();
+        if (objectMember.isVisible(authenticationSession, objectAdapter, where).isVetoed()) {
+            throwNotFoundException(memberId, memberType);
+        }
+        if (intent.isMutate()) {
+            final Consent usable = objectMember.isUsable(authenticationSession, objectAdapter, where);
+            if (usable.isVetoed()) {
+                final String memberTypeStr = memberType.name().toLowerCase();
+                throw RestfulObjectsApplicationException.create(HttpStatusCode.NOT_ACCEPTABLE, "%s is not usable: '%s' (%s)", memberTypeStr, memberId, usable.getReason());
+            }
+        }
+        return objectMember;
+    }
+
+    protected static void throwNotFoundException(final String memberId, final MemberType memberType) {
+        final String memberTypeStr = memberType.name().toLowerCase();
+        throw RestfulObjectsApplicationException.create(HttpStatusCode.NOT_FOUND, "%s '%s' either does not exist or is not visible", memberTypeStr, memberId);
+    }
+
+    // ///////////////////////////////////////////////////////////////////
+    // parseBody
+    // ///////////////////////////////////////////////////////////////////
+
+    /**
+     *
+     * @param objectSpec
+     * @param bodyAsString
+     *            - as per {@link #asStringUtf8(InputStream)}
+     * @return
+     */
+    ObjectAdapter parseAsMapWithSingleValue(final ObjectSpecification objectSpec, final String bodyAsString) {
+        final JsonRepresentation arguments = readAsMap(bodyAsString);
+
+        return parseAsMapWithSingleValue(objectSpec, arguments);
+    }
+
+    ObjectAdapter parseAsMapWithSingleValue(final ObjectSpecification objectSpec, final JsonRepresentation arguments) {
+        final JsonRepresentation representation = arguments.getRepresentation("value");
+        if (arguments.size() != 1 || representation == null) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Body should be a map with a single key 'value' whose value represents an instance of type '%s'", resourceFor(objectSpec));
+        }
+
+        return objectAdapterFor(resourceContext, objectSpec, representation);
+    }
+
+    private List<ObjectAdapter> parseArguments(final ObjectAction action, final JsonRepresentation arguments) {
+        return parseArguments(resourceContext, action, arguments);
+    }
+
+    public static List<ObjectAdapter> parseArguments(final RendererContext resourceContext, final ObjectAction action, final JsonRepresentation arguments) {
+        final List<JsonRepresentation> argList = argListFor(action, arguments);
+
+        final List<ObjectAdapter> argAdapters = Lists.newArrayList();
+        final List<ObjectActionParameter> parameters = action.getParameters();
+        for (int i = 0; i < argList.size(); i++) {
+            final String paramName = parameters.get(i).getName();
+            final JsonRepresentation arg = argList.get(i);
+            final ObjectSpecification paramSpec = parameters.get(i).getSpecification();
+            try {
+                final ObjectAdapter objectAdapter = objectAdapterFor(resourceContext, paramSpec, arg);
+                argAdapters.add(objectAdapter);
+            } catch (final ExpectedStringRepresentingValueException e) {
+                throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Action '%s', argument %s should be a URL encoded string representing a value of type %s", action.getId(), paramName, resourceFor(paramSpec));
+            } catch (final ExpectedMapRepresentingLinkException e) {
+                throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Action '%s', argument %s should be a map representing a link to reference of type %s", action.getId(), paramName, resourceFor(paramSpec));
+            }
+        }
+        return argAdapters;
+    }
+
+    private static List<JsonRepresentation> argListFor(final ObjectAction action, final JsonRepresentation arguments) {
+        final List<JsonRepresentation> argList = Lists.newArrayList();
+
+        // ensure that we have no arguments that are not parameters
+        for (final Entry<String, JsonRepresentation> arg : arguments.mapIterable()) {
+            final String argName = arg.getKey();
+            if (action.getParameterById(argName) == null) {
+                throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Action '%s' does not have a parameter %s but an argument of that name was provided", action.getId(), argName);
+            }
+        }
+
+        // ensure that an argument value has been provided for all non-optional
+        // parameters
+        final List<ObjectActionParameter> parameters = action.getParameters();
+        for (final ObjectActionParameter param : parameters) {
+            final String paramId = param.getId();
+            final JsonRepresentation argRepr = arguments.getRepresentation(paramId);
+            if (argRepr == null && !param.isOptional()) {
+                throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "Action '%s', no argument found for (mandatory) parameter '%s'", action.getId(), paramId);
+            }
+            argList.add(argRepr);
+        }
+        return argList;
+    }
+
+    public static JsonRepresentation readParameterMapAsMap(final Map<String, String[]> parameterMap) {
+        final JsonRepresentation map = JsonRepresentation.newMap();
+        for (final Map.Entry<String, String[]> parameter : parameterMap.entrySet()) {
+            map.mapPut(parameter.getKey(), parameter.getValue()[0]);
+        }
+        return map;
+    }
+
+    public static JsonRepresentation readQueryStringAsMap(final String queryString) {
+        if (queryString == null) {
+            return JsonRepresentation.newMap();
+        }
+        final String queryStringTrimmed = queryString.trim();
+        if (queryStringTrimmed.isEmpty()) {
+            return JsonRepresentation.newMap();
+        }
+        final String queryStringUrlDecoded = UrlEncodingUtils.urlDecode(queryStringTrimmed);
+        if (queryStringUrlDecoded.isEmpty()) {
+            return JsonRepresentation.newMap();
+        }
+
+        return read(queryStringUrlDecoded, "query string");
+    }
+
+    public static JsonRepresentation readAsMap(final String body) {
+        if (body == null) {
+            return JsonRepresentation.newMap();
+        }
+        final String bodyTrimmed = body.trim();
+        if (bodyTrimmed.isEmpty()) {
+            return JsonRepresentation.newMap();
+        }
+        return read(bodyTrimmed, "body");
+    }
+
+    private static JsonRepresentation read(final String args, final String argsNature) {
+        try {
+            final JsonRepresentation jsonRepr = JsonMapper.instance().read(args);
+            if (!jsonRepr.isMap()) {
+                throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, "could not read %s as a JSON map", argsNature);
+            }
+            return jsonRepr;
+        } catch (final JsonParseException e) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, e, "could not parse %s", argsNature);
+        } catch (final JsonMappingException e) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, e, "could not read %s as JSON", argsNature);
+        } catch (final IOException e) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, e, "could not parse %s", argsNature);
+        }
+    }
+
+    public static String asStringUtf8(final InputStream body) {
+        try {
+            final byte[] byteArray = ByteStreams.toByteArray(body);
+            return new String(byteArray, Charsets.UTF_8);
+        } catch (final IOException e) {
+            throw RestfulObjectsApplicationException.create(HttpStatusCode.BAD_REQUEST, e, "could not read body");
+        }
+    }
+
+    // //////////////////////////////////////////////////////////////
+    // misc
+    // //////////////////////////////////////////////////////////////
+
+    private static String resourceFor(final ObjectSpecification objectSpec) {
+        // TODO: should return a string in the form
+        // http://localhost:8080/types/xxx
+        return objectSpec.getFullIdentifier();
+    }
+
+    // //////////////////////////////////////////////////////////////
+    // dependencies
+    // //////////////////////////////////////////////////////////////
+
+    protected RendererFactoryRegistry getRendererFactoryRegistry() {
+        // TODO: yuck
+        return RendererFactoryRegistry.instance;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/75f41b75/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainServiceResourceServerside.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainServiceResourceServerside.java b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainServiceResourceServerside.java
new file mode 100644
index 0000000..30f5fff
--- /dev/null
+++ b/component/viewer/restfulobjects/server/src/main/java/org/apache/isis/viewer/restfulobjects/server/resources/DomainServiceResourceServerside.java
@@ -0,0 +1,170 @@
+/**
+ *  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.server.resources;
+
+import java.io.InputStream;
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
+import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
+import org.apache.isis.viewer.restfulobjects.applib.RestfulMediaType;
+import org.apache.isis.viewer.restfulobjects.applib.domainobjects.DomainServiceResource;
+import org.apache.isis.viewer.restfulobjects.rendering.RendererFactory;
+import org.apache.isis.viewer.restfulobjects.rendering.RendererFactoryRegistry;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.DomainObjectReprRenderer;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.DomainServiceLinkTo;
+import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ListReprRenderer;
+import org.apache.isis.viewer.restfulobjects.server.resources.DomainResourceHelper.MemberMode;
+
+@Path("/services")
+public class DomainServiceResourceServerside extends ResourceAbstract implements DomainServiceResource {
+
+    @Override
+    @GET
+    @Path("/")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_LIST, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response services() {
+        final RepresentationType representationType = RepresentationType.LIST;
+        init(representationType, Where.STANDALONE_TABLES);
+
+        final List<ObjectAdapter> serviceAdapters = getResourceContext().getServiceAdapters();
+
+        final RendererFactory factory = RendererFactoryRegistry.instance.find(representationType);
+
+        final ListReprRenderer renderer = (ListReprRenderer) factory.newRenderer(getResourceContext(), null, JsonRepresentation.newMap());
+        renderer.usingLinkToBuilder(new DomainServiceLinkTo()).withSelf("services").with(serviceAdapters);
+
+        return responseOfOk(renderer, Caching.ONE_DAY).build();
+    }
+
+    // //////////////////////////////////////////////////////////
+    // domain service
+    // //////////////////////////////////////////////////////////
+
+    @Override
+    @GET
+    @Path("/{serviceId}")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_DOMAIN_OBJECT, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response service(@PathParam("serviceId") final String serviceId) {
+        init(RepresentationType.DOMAIN_OBJECT, Where.OBJECT_FORMS);
+
+        final ObjectAdapter serviceAdapter = getServiceAdapter(serviceId);
+
+        final RendererFactory rendererFactory = rendererFactoryRegistry.find(RepresentationType.DOMAIN_OBJECT);
+
+        final DomainObjectReprRenderer renderer = (DomainObjectReprRenderer) rendererFactory.newRenderer(getResourceContext(), null, JsonRepresentation.newMap());
+        renderer.usingLinkToBuilder(new DomainServiceLinkTo()).with(serviceAdapter).includesSelf();
+
+        return responseOfOk(renderer, Caching.ONE_DAY).build();
+    }
+
+    // //////////////////////////////////////////////////////////
+    // domain service property
+    // //////////////////////////////////////////////////////////
+
+    @Override
+    @GET
+    @Path("/{serviceId}/properties/{propertyId}")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_PROPERTY, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response propertyDetails(@PathParam("serviceId") final String serviceId, @PathParam("propertyId") final String propertyId) {
+        init(RepresentationType.OBJECT_PROPERTY, Where.OBJECT_FORMS);
+
+        final ObjectAdapter serviceAdapter = getServiceAdapter(serviceId);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), serviceAdapter).using(new DomainServiceLinkTo());
+
+        return helper.propertyDetails(serviceAdapter, propertyId, MemberMode.NOT_MUTATING, Caching.ONE_DAY, getResourceContext().getWhere());
+    }
+
+    // //////////////////////////////////////////////////////////
+    // domain service action
+    // //////////////////////////////////////////////////////////
+
+    @Override
+    @GET
+    @Path("/{serviceId}/actions/{actionId}")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_OBJECT_ACTION, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response actionPrompt(@PathParam("serviceId") final String serviceId, @PathParam("actionId") final String actionId) {
+        init(RepresentationType.OBJECT_ACTION, Where.OBJECT_FORMS);
+
+        final ObjectAdapter serviceAdapter = getServiceAdapter(serviceId);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), serviceAdapter).using(new DomainServiceLinkTo());
+
+        return helper.actionPrompt(actionId, getResourceContext().getWhere());
+    }
+
+    // //////////////////////////////////////////////////////////
+    // domain service action invoke
+    // //////////////////////////////////////////////////////////
+
+    @Override
+    @GET
+    @Path("/{serviceId}/actions/{actionId}/invoke")
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ACTION_RESULT, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response invokeActionQueryOnly(@PathParam("serviceId") final String serviceId, @PathParam("actionId") final String actionId) {
+        init(RepresentationType.ACTION_RESULT, Where.STANDALONE_TABLES);
+
+        final JsonRepresentation arguments = getResourceContext().getQueryStringAsJsonRepr();
+
+        final ObjectAdapter serviceAdapter = getServiceAdapter(serviceId);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), serviceAdapter).using(new DomainServiceLinkTo());
+
+        return helper.invokeActionQueryOnly(actionId, arguments, getResourceContext().getWhere());
+    }
+
+    @Override
+    @PUT
+    @Path("/{serviceId}/actions/{actionId}/invoke")
+    @Consumes({ MediaType.WILDCARD })
+    // to save the client having to specify a Content-Type: application/json
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ACTION_RESULT, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response invokeActionIdempotent(@PathParam("serviceId") final String serviceId, @PathParam("actionId") final String actionId, final InputStream arguments) {
+        init(RepresentationType.ACTION_RESULT, Where.STANDALONE_TABLES);
+
+        final ObjectAdapter serviceAdapter = getServiceAdapter(serviceId);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), serviceAdapter).using(new DomainServiceLinkTo());
+
+        return helper.invokeActionIdempotent(actionId, arguments, getResourceContext().getWhere());
+    }
+
+    @Override
+    @POST
+    @Path("/{serviceId}/actions/{actionId}/invoke")
+    @Consumes({ MediaType.WILDCARD })
+    // to save the client having to specify a Content-Type: application/json
+    @Produces({ MediaType.APPLICATION_JSON, RestfulMediaType.APPLICATION_JSON_ACTION_RESULT, RestfulMediaType.APPLICATION_JSON_ERROR })
+    public Response invokeAction(@PathParam("serviceId") final String serviceId, @PathParam("actionId") final String actionId, final InputStream arguments) {
+        init(RepresentationType.ACTION_RESULT, Where.STANDALONE_TABLES);
+
+        final ObjectAdapter serviceAdapter = getServiceAdapter(serviceId);
+        final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), serviceAdapter).using(new DomainServiceLinkTo());
+
+        return helper.invokeAction(actionId, arguments, getResourceContext().getWhere());
+    }
+
+}