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/03/21 09:52:55 UTC

[1/3] ISIS-233: invoking action with missing args or surplus args

Updated Branches:
  refs/heads/dan/ISIS-233-ro 25ebaa9ab -> 8210a58d7
Updated Tags:  refs/tags/isis-0.1.2-RC2-incubating [created] 073e279a0
  refs/tags/isis-0.1.2-RC3-incubating [created] 0b09a4c2e
  refs/tags/isis-0.1.2-RC4-incubating [created] 234a55fb5
  refs/tags/isis-0.1.2-incubating [created] 40c529018
  refs/tags/isis-0.2.0-incubating [created] 6dce716dd
  refs/tags/isis-1.0.0-RC1 [created] 771f7e393
  refs/tags/isis-1.0.0-RC2 [created] 53dddebcb
  refs/tags/isis-objectstore-jdo-1.0.0-RC1 [created] 1a2b468b2
  refs/tags/isis-objectstore-jdo-1.0.0-RC2 [created] 423f7a110
  refs/tags/isis-security-file-1.0.0-RC1 [created] ad852d3ef
  refs/tags/isis-security-file-1.0.0-RC2 [created] b71ec3d05
  refs/tags/isis-security-shiro-1.0.0-RC1 [created] d7afb7606
  refs/tags/isis-viewer-restfulobjects-1.0.0-RC1 [created] 6916f21e3
  refs/tags/isis-viewer-restfulobjects-1.0.0-RC2 [created] e21e1f19f
  refs/tags/isis-viewer-wicket-1.0.0-RC1 [created] 8f8a549a0
  refs/tags/isis-viewer-wicket-1.0.0-RC2 [created] f12423d73
  refs/tags/origin/isis-0.1.2-RC2-incubating [created] 073e279a0
  refs/tags/origin/isis-0.1.2-RC3-incubating [created] 0b09a4c2e
  refs/tags/origin/isis-0.1.2-RC4-incubating [created] 234a55fb5
  refs/tags/origin/isis-0.1.2-incubating [created] 40c529018
  refs/tags/origin/isis-0.2.0-incubating [created] 6dce716dd
  refs/tags/origin/isis-1.0.0 [created] 53dddebcb
  refs/tags/origin/isis-1.1.0 [created] 4859004d0
  refs/tags/origin/isis-objectstore-jdo-1.0.0 [created] 423f7a110
  refs/tags/origin/isis-security-file-1.0.0 [created] b71ec3d05
  refs/tags/origin/isis-security-shiro-1.0.0 [created] d7afb7606
  refs/tags/origin/isis-security-shiro-1.1.0 [created] 340f5709d
  refs/tags/origin/isis-viewer-restfulobjects-1.0.0 [created] e21e1f19f
  refs/tags/origin/isis-viewer-wicket-1.0.0 [created] f12423d73
  refs/tags/origin/isis-viewer-wicket-1.1.0 [created] e11c74411
  refs/tags/origin/quickstart_wicket_restful_jdo-archetype-1.0.0 [created] cdb171f80
  refs/tags/origin/quickstart_wicket_restful_jdo-archetype-1.0.1 [created] 86878f4c3
  refs/tags/origin/quickstart_wicket_restful_jdo-archetype-1.0.2 [created] 87c2b682c
  refs/tags/quickstart_wicket_restful_jdo-archetype-1.0.0-RC1 [created] cdb171f80
  refs/tags/quickstart_wicket_restful_jdo-archetype-1.0.1-RC1 [created] e1c22b22a
  refs/tags/quickstart_wicket_restful_jdo-archetype-1.0.1-RC2 [created] 86878f4c3


http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/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 a517b9a..676f04b 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
@@ -43,7 +43,7 @@ public class ActionsEntityRepository extends AbstractEntityRepository<ActionsEnt
 
     @ActionSemantics(Of.SAFE)
     @MemberOrder(sequence = "1")
-    public ActionsEntity findById(int id) {
+    public ActionsEntity findById(@Named("id") int id) {
         final Query<ActionsEntity> query = 
                 new QueryDefault<ActionsEntity>(ActionsEntity.class, ActionsEntity.class.getName() + "#pk", "id", id);
         return this.firstMatch(query);

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/core/tck/tck-dom/src/main/java/org/apache/isis/core/tck/dom/busrules/BusRulesEntityRepository.java
----------------------------------------------------------------------
diff --git a/core/tck/tck-dom/src/main/java/org/apache/isis/core/tck/dom/busrules/BusRulesEntityRepository.java b/core/tck/tck-dom/src/main/java/org/apache/isis/core/tck/dom/busrules/BusRulesEntityRepository.java
index 456f514..fd7d8a7 100644
--- a/core/tck/tck-dom/src/main/java/org/apache/isis/core/tck/dom/busrules/BusRulesEntityRepository.java
+++ b/core/tck/tck-dom/src/main/java/org/apache/isis/core/tck/dom/busrules/BusRulesEntityRepository.java
@@ -49,14 +49,14 @@ public class BusRulesEntityRepository extends AbstractEntityRepository<BusRulesE
 
     @ActionSemantics(Of.SAFE)
     @MemberOrder(sequence = "1")
-    public BusRulesEntity visibleAndInvocableAction(int id) {
+    public BusRulesEntity visibleAndInvocableAction(@Named("id") int id) {
         return this.findById(id);
     }
 
     @Disabled
     @ActionSemantics(Of.SAFE)
     @MemberOrder(sequence = "1")
-    public BusRulesEntity visibleButNotInvocableAction(int id) {
+    public BusRulesEntity visibleButNotInvocableAction(@Named("id") int id) {
         return this.findById(id);
     }
 


[2/3] ISIS-233: invoking action with missing args or surplus args

Posted by da...@apache.org.
http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/wicket/src/docbkx/guide/isis-wicket-viewer.md
----------------------------------------------------------------------
diff --git a/component/viewer/wicket/src/docbkx/guide/isis-wicket-viewer.md b/component/viewer/wicket/src/docbkx/guide/isis-wicket-viewer.md
new file mode 100644
index 0000000..98210f8
--- /dev/null
+++ b/component/viewer/wicket/src/docbkx/guide/isis-wicket-viewer.md
@@ -0,0 +1,2958 @@
+Preface
+=======
+
+The *Apache Isis Wicket Viewer* provides a customizable web-based viewer
+for *Apache Isis* implemented using the [Apache
+Wicket](http://wicket.apache.org) framework. This user guide is written
+for developers looking to write *Apache Isis* applications and deploy
+them using the *Wicket Viewer*. It assumes familiarity with writing
+*Isis* applications, but that is all that is needed to get an
+application up-and-running with the generic OOUI.
+
+Familiarity with *Apache Wicket* is useful to understand how the wicket
+viewer fits together, but is essential unless you want to start
+customizing the UI. If you're interested in learning more about *Wicket*
+itself, then [Wicket In Action](http://www.manning.com/dashorst/)
+(Martijn Dashort & Eelco Hillenius) is definitely worth reading.
+
+Introduction
+============
+
+> This introductory chapter describes the objective of the *Wicket
+> Viewer*, and the target audience for this guide.
+
+Introducing the Apache Isis Wicket Viewer
+-----------------------------------------
+
+The *Wicket Viewer* provides a generic OOUI web-based viewer for *Apache
+Isis* domain models, using [Apache Wicket](http://wicket.apache.org) as
+the underlying web framework.
+
+What this means for you, the developer, is that you can build a web
+application solely by developing the domain model as pojos. The *Wicket
+Viewer* will then render your domain objects in a web UI using ready
+provided *Wicket* Components.
+
+This generic UI provides a lot of functionality out-of-the-box:
+
+-   Each domain entity is rendered as a form, with the appropriate
+    individual component type for its values. This entity form also
+    allows editable references to other objects, too.
+
+-   Moreover, any collections of a domain entity (eg Order /
+    OrderDetail) are also automatically rendered as a list, a table, or
+    in any other appropriate representation. Both the scalar and
+    collection references let the end-user easily "walk the graph"
+    around your domain model.
+
+-   Finally, the vuewer also renders actions (commands/behaviors) for
+    your domain objects, and lets these actions to be invoked. This is
+    what makes *Apache Isis*-based applications that much more than
+    simple CRUD applications.
+
+You can see examples of the UI in ?.
+
+Over and above this generic interface, you are then free to customize
+this interface in various ways. Your customizations can be general (eg
+providing a way to render a collection of Locatable objects in a
+google-maps mashup) or targetted (eg a particular way to render a
+ShoppingCartWizard). Generally these customizations take the form of
+bespoke *Wicket* Components, though simple customizations can be
+performed just by modifying the CSS. Again you can see some examples of
+these customizations in ?.
+
+*Apache Isis* webapps that use the *Wicket Viewer* are bootstrapped in
+the same as vanilla *Wicket* applications, with the *Wicket Viewer*
+providing an implementation of *Wicket*'s `WebApplication` class that
+additionally bootstraps the *Apache Isis* runtime. Authentication is
+performed using *Apache Isis*' own security mechanism (which itself is
+pluggable).
+
+The Wicket Viewer compared to other Wicket RAD Frameworks
+---------------------------------------------------------
+
+### Differences in Capability
+
+There are several frameworks that aim either to provide a back-end to
+*Wicket* and/or that aim to eliminate some of the boilerplate; notable
+examples are [Wicketopia](http://wicketopia.sourceforge.net), [Wicket
+RAD](http://sites.google.com/site/wicketrad) and
+[DataBinder](http://databinder.net/wicket/show/overview). There are also
+precanned archetypes, such as jWeekEnd's
+[LegUp](http://jweekend.co.uk/dev/LegUp) archetypes and
+[IoLite](http://wicketstuff.org/confluence/display/STUFFWIKI/Wicket-Iolite),
+that do some of the wiring for back-end technologies like
+[Spring](http://springframework.org) and
+[Hibernate](http://hibernate.org). So, how does *Wicket Viewer* for
+*Apache Isis* compare to them?
+
+Well, compared to *Wicketopia* for example, *Wicket Viewer* shares the
+following ideas:
+
+-   both frameworks support pluggable editors for Javabean properties
+
+-   both frameworks allow the metamodel to be enhanced declaratively
+    using annotations (eg @Length); these are picked up by the editors
+
+-   both frameworks support (or aspire to support) the metamodel being
+    built from various sources, including for example *Hibernate* or
+    JSR-303 annotations etc. In the case of the *Wicket Viewer* this is
+    done by using *Apache Isis*' own metamodel.
+
+But there are some differences too; through *Wicket Viewer*'s use of the
+*Apache Isis* it:
+
+-   allows the metamodel to be enhanced imperatively as well as
+    declaratively; naming conventions are used to associate supporting
+    methods with the properties
+
+-   supports user-defined value types (using *Apache Isis*' @Value
+    annotation); important for domain-driven applications
+
+-   supports three level of business rule (both declaratively and
+    imperatively): is the entity class member visible, is it usable, is
+    the value proposed valid. For most other frameworks, only the last
+    of these (validation of proposed values) is typically supported
+
+-   supports properties which are references to other entities, not just
+    simple value types. In particular, the *Wicket Viewer*'s property
+    editor for references allows instances to be found by invoking
+    repositories in-situ
+
+-   renders entity collections as well as entity properties
+
+-   renders entity actions (commands), allowing more than simple CRUD
+    behavior to be exposed. Indeed, it's possible for the domain entity
+    to be immutable except through the invocation of actions
+
+The flip side of the *Wicket Viewer*'s more extensive metamodel support
+is that it uses *Apache Isis*' own metamodel API. In contrast,
+*Wicketopia* (only supporting properties and not collections or actions)
+can get by using java.bean.PropertyDescriptor from the JDK.
+
+Compared to frameworks and archetypes that use *Hibernate* for the
+backend, the *Wicket Viewer* (again by leveraging the rest of the
+*Apache Isis* framework) is more general here too. So, it has a Session
+concept that is analogous to a *Hibernate* session (and if using the
+*JPA ObjectStore* as a backend, the *Apache Isis*' Session just wraps
+the JPA provider's PersistenceContext *e*'s). But it also supports other
+back-end persistence stores too. Indeed, *Apache Isis*' default to
+support rapid development we usually use an in-memory object store.
+
+The *Wicket Viewer* can also handle non-persisted objects as well as
+persisted objects; with every managed object is stored in an identity
+map. This support for non-persisted objects is important because it is
+central to supporting specialized use cases, including managing
+workflows for entering data, and dashboards for analyzing data.
+
+A few other points worthy of mention:
+
+-   The *Wicket Viewer* will automatically serialize both persisted and
+    non-persisted objects, irrespective of whether the pojo itself is
+    serializable. This is important if deploying on a cluster.
+
+-   Through the *Apache Isis* core framework, every managed object also
+    automatically has any domain service dependencies automatically
+    injected into it),
+
+-   Again, courtesy of *Apache Isis* core, every persisted object has a
+    unique, immutable and serializable object Id (OID). This can be a
+    very useful resource when integrating with external services such as
+    REST and ESBs.
+
+-   *Apache Isis* core also provides automatic dirty tracking, and
+    optimistic locking.
+
+Downsides? Currently *Apache Isis* does not support some of the
+Enterprise Java APIs such as JTA, so interactions with domain services
+that ultimately delegate to transaction stores (such as JMS) cannot be
+enlisted in the same transaction as the persistence store. And,
+obviously, it introduces a dependency on the *Apache Isis* framework
+over and above technologies such as the JPA provider implementation.
+
+### Differences in Philosophy
+
+Aside from differences in capability, there's also a difference in
+philosophy.
+
+Frameworks such as *Wicketopia* make it easy to render a form to edit
+the properties of an entity, but the application developer remains in
+control of the rest of the application service layer, and in wiring the
+various pages together.
+
+With the *Wicket Viewer*, though, it is the framework that is in control
+of this layer too, because the links between pages are ultimately
+rendered by the property editor Components provided by the viewer, and -
+in rendering the links representing references - controls . What that
+also means is that every page rendered by the *Wicket Viewer* is always
+of either an object (typical case), or an action parameter dialog, or of
+a collection (the result of invoking an action).
+
+Within this constraint, there is a lot of flexibility, though, because
+every element of the page can be customized. Internally the *Wicket
+Viewer* uses the chain of responsibility pattern to determine how which
+Component type to use to render a page element. This works at a
+fine-grained level (eg a date editor) and also at a larger-scale, (eg
+rendering a collection of Locatable entities on a google maps). See ?
+for more on this.
+
+### Part of the *Apache Isis* framework
+
+The *Wicket Viewer* is only one of a number of viewers available for
+*Apache Isis*. What that means is that you can take your same domain
+objects model and deploy them in other architectures. For example, the
+DnD viewer is a rich, desktop GUI that employs a drag-n-drop metaphor
+(hence its name). It can be run as a standalone single-user application
+or in client/server mode. Even if you don't deploy this viewer in
+production, the way that it displays domain objects makes for a very
+good development tool: something akin to a UML design tool, only
+animated.
+
+*Apache Isis* also supports a number of different ways to test your
+application. Domain objects written to the *Apache Isis* programming
+model are just pojos, and so can easily be unit tested using frameworks
+such as [JUnit](http://junit.org) and [JMock](http://jmock.org). As a
+step up from that, *Apache Isis* provides its own integrations with
+JUnit, providing a "headless" programming model that wraps your domain
+objects in proxies. This allows unit testing as if through the lens of a
+GUI; trying to invoke an action that is disabled will throw an
+exception.
+
+Moving up to story testing/BDD, *Apache Isis* integrates with
+[Concordion](http://concordion.org). The business analyst specifies the
+behavior of the system in HTML, and then the developer wire up this
+specification to the domain model using a set of fixtures that interact
+with the domain objects following the same rules as the viewers. This
+allows application-level testing without the hassle of using a GUI
+testing framework such as *Selenium*.
+
+One final point: *Apache Isis* has a strong separation between its
+programming model and the framework that understands that programming
+model. So, your domain objects depend only on the *Apache Isis* applib
+(which defines annotations such as @RegEx), not the framework itself.
+This allows you to take your domain objects and deploy them on other
+frameworks later, if need be. The *Wicket Viewer* maintains this strong
+separation, defining its own applib for annotations and features unique
+to the *Wicket Viewer*.
+
+The *Wicket Viewer* compared to other *Apache Isis* viewers
+-----------------------------------------------------------
+
+Because the *Wicket Viewer* renders your domain model within a webapp,
+it is to some extent similar to the HTML viewer that is provided
+out-of-the-box by *Apache Isis* itself. However, unlike the HTML viewer,
+the *Wicket Viewer* is highly customizable. The *Wicket Viewer* itself
+essentially consists of a set of [Wicket](http://wicket.apache.org)
+Components that are used to render domain objects, individual member
+elements of domain objects, and indeed collections of domain objects.
+The components provided are sufficient to render any *Apache Isis*
+domain model. However, any developer with experience of *Wicket* can
+easily write their own components and register them with the *Wicket
+Viewer*. In this way, custom renderings of domain objects can be
+achieved.
+
+Typical Flow for Developing a Wicket Objects Application
+--------------------------------------------------------
+
+There are, of course, lots of ways to skin any given cat, but here's how
+you might go about developing an *Apache Isis* application to be
+deployed using the *Wicket Viewer*:
+
+-   use the *Apache Isis* application archetype to create an outline
+    domain model, running against the in-memory object store. The
+    structure of a *Apache Isis* application is reviewed in ?.
+
+-   (optionally) use the DnD viewer to define some of the basic domain
+    services and entities. Or, you might want to skip this step and
+    develop solely using Wicket.
+
+-   run your application under *Wicket*, either using a built-in Jetty
+    web server (see ?) or as a regular webapp (see ?).
+
+-   customize the look-n-feel to use your preferred fonts and logos by
+    updating the CSS (see ?). You can also use CSS to fine-tune the
+    layout for selected objects or object members (see ?).
+
+-   continue to develop your domain application, identifying properties,
+    collections and defining behavior through actions. Also, define
+    fixtures to represent pre-canned scenarios to explore with your
+    domain expert. Check out the core *Apache Isis* documentation for
+    more on developing domain objects.
+
+-   fine tune the set of Components used to render your entities. The
+    *Wicket Viewer* provides built-in Components to render every element
+    of your domain objects, and in some cases provides more than one
+    Component. When the *Wicket Viewer* provides a number of alternate
+    views like this, you might prefer only one. Or, you might use an
+    existing Component and adapt it into your own Component. In either
+    case, you'll need to fine-tune the set of ComponentFactorys (see ?).
+
+-   implement custom representations of some entities (or collections of
+    entities), where it makes sense. For example, you might want to
+    display objects that have a location in a google maps mashup, or
+    objects that have a date/time in a calendar. Ultimately these custom
+    representations are also just ComponentFactorys. A number of
+    Components (including one for google maps and one for charting) are
+    described in ?; use these directly or use them as inspiration for
+    your own.
+
+-   support specialized use cases, if you have any. That is, rather than
+    require the end-user to interact directly with persisted entities,
+    introduce transient process objects to manage workflow and bulk
+    input, or write transient report objects to provide dashboards
+
+-   as you continue to develop your application, you may need to
+    integrate with external services. For example, you might want to
+    send out an email, or invoke a web service exposed by some other
+    system in your enterprise. Define an interface for these domain
+    services, and register their implementation in the *Apache Isis*
+    configuration file. See the main *Apache Isis* documentation for
+    more details on this.
+
+-   ultimately your application will be ready to deploy. Before you do,
+    though, remember that you'll need to sort out persistence and
+    security (see ?)
+
+Enough verbiage. The next chapter is a run through of a simple
+application, screenshot by screenshot, so you can quickly assess whether
+the *Wicket Viewer* fits your needs.
+
+Application Walkthrough
+=======================
+
+> This chapter is a collection of screenshots to give you an idea of
+> what the
+> Wicket Viewer
+> can do.
+
+Most of the screenshots in this chapter require no specific GUI code; in
+fact the first six sections require no specialized code other than the
+domain objects. The GUI that you see is generated by the *Wicket Viewer*
+at runtime, directly from the domain model. You can find all the code
+for the domain objects in ?.
+
+Logging on, and the Application Services Menu
+---------------------------------------------
+
+The *Wicket Viewer* integrates with *Apache Isis* authentication
+mechanism (which is itself pluggable); so we start off with a login
+screen:
+
+![](images/010-login.png)
+
+The initial home page shows a welcome message and more importantly a
+menu bar for each of the registered services defined in the domain
+application. These application services are the start points for the
+user, allowing them to find existing objects and to create new ones. For
+example the `Employees` menu item corresponds to the EmployeeRepository
+class.
+
+![](images/020-services-bar.png)
+
+From the menu bar we can get to the menu items for each service. So, for
+example, the `Employees` domain service provides two actions,
+`All Employees` and `Find
+      Employees`. These are generated automatically from corresponding
+methods - allEmployees() and findEmployees() - in the EmployeeRepository
+service.
+
+![](images/030-services-action.png)
+
+Viewing Entities and Collections of Entities
+--------------------------------------------
+
+Because the `All Employees` action takes no arguments, invoking it just
+returns its results. In this case the action returns a collection (of
+Employees), and so the viewer renders the collection as a table. If the
+action had returned a single object, then that would have been rendered
+instead.
+
+![](images/040-action-result.png)
+
+Clicking on one of the links takes us to a page rendering that object,
+in this case an Employee. There is a form for the entity's properties on
+the left, and summary details on the right. In this particular case the
+entity has no collections; we'll see one that does shortly.
+
+![](images/050-entity-form.png)
+
+In the summary section we an image, a title and the entity actions. All
+these are rendered directly from a metamodel built by inspecting the
+entity's class. Just as we can invoke actions on the services, we can
+also invoke actions on the entities; for example, to view this
+Employee's Claims with the `Claims For,,,` action.
+
+![](images/060-entity-actions.png)
+
+As before, this action returns a collection (of Claims) and so is
+rendered as a table.
+
+![](images/070-entity-action-results.png)
+
+Clicking on a link to a Claim again renders the entity. This is rendered
+in a similar manner to the Employee entity seen earlier. However, the
+Claim entity also has a collection (of ClaimItems), so these are also
+rendered.
+
+![](images/080-entity-form-and-collections.png)
+
+Editing Objects
+---------------
+
+In *Apache Isis* applications, we modify objects either by invoking
+actions on them or by editing them directly. Which properties are
+editable is determined by the entity itself, with the *Wicket Viewer*
+providing an appropriate editor for each property type. In the following
+screenshot we see the Claim entity being edited.
+
+![](images/090-entity-editing-scalars.png)
+
+As well as supporting the editing of properties with value types
+(boolean, String, Date etc), the viewer also allows properties
+representing references to other entities to be edited. For example,
+each Claim has an `approver` (of type Approver, and implemented by
+Employee). Thus, the viewer gives us the ability to find an Employee
+from the `Employees` repository:
+
+![](images/100-entity-editing-references.png)
+
+Leaving us with the `approver` reference set up:
+
+![](images/110-entity-editing-references-result.png)
+
+Invoking Actions
+----------------
+
+Being able to edit entities makes it easy to build CRUD-style
+(create/read/update/delete) applications. The *Wicket Viewer* also
+allows arbitrary actions to be performed on entities. In the screenshot
+below, we see the `Add Item` action (corresponding to addItem() method
+on the Claim class) being called:
+
+![](images/120-entity-action.png)
+
+Because this action takes parameters, the viewer renders a dialog form
+for the user to complete. Just as with the entity editor, we can specify
+parameters of any type, including references to other entities if we
+wish). In this particular case the parameter types are just numbers and
+strings:
+
+![](images/130-entity-action-parameters.png)
+
+When the action is complete, the entity is modified, by adding a new
+ClaimItem into the Claim's `items` collection.
+
+![](images/140-entity-modified.png)
+
+Admittedly, this `Add Item` action just a CRUD-style action. But the
+business logic in the action could be arbitrarily complex. The other
+action in Claim is `Submit`, which could perform lots of business
+processing:
+
+![](images/150-entity-action-more-complex.png)
+
+This action also takes an argument, being a reference to an Approver:
+
+![](images/160-entity-action-reference-parameters.png)
+
+When the action is performed, the object's state is updated, along with
+any other processing. For example, a message could have been sent via an
+enterprise service bus to the processing department. Note the title of
+the Claim entity has also been updated:
+
+![](images/170-entity-title-updated.png)
+
+Business Rules
+--------------
+
+In addition to actions, we can also capture business rules by means of
+validation. For example, a Claim, once submitted, cannot be submitted
+again. With the *Wicket Viewer*, this is shown by a tooltip:
+
+![](images/180-entity-validation.png)
+
+*Apache Isis* supports three different types of validation: whether the
+class member (property, collection or action) is visible; whether it is
+usable, and, whether the proposed value or arguments are valid. Or, more
+pithily, "can you see it, can you use it, can you do it". The viewer
+surfaces all of these different validation rules in the GUI.
+
+View Components
+---------------
+
+The pages rendered by the *Wicket Viewer* are built from multiple
+*Apache Wicket* Components. For example, the application service menu
+bar has its own Component:
+
+![](images/200-view-components-application-service.png)
+
+Likewise, there's a Component to render an entire entity:
+
+![](images/205-view-components-entity-simple.png)
+
+And there's a Component to display the properties of an entity:
+
+![](images/210-view-components-entity-properties.png)
+
+And in turn there's a Component for every entity property:
+
+![](images/220-view-components-entity-property.png)
+
+There are Components such as these for every class member, including
+collections, actions and, indeed, action parameters. In fact, you can
+think of the *Wicket Viewer* as basically a collection of pre-canned
+Components that know how to render the *Apache Isis* metamodel.
+
+Note, by the way, the CSS classes. Each HTML element can be targetted
+either by its type (a string scalar), or by its class member (the
+Claim's `description` property), or both. The viewer therefore makes it
+easy to contribute custom CSS that applies to every page.
+
+Alternate Views
+---------------
+
+As we've seen already, there's a Component to render an entire entity.
+In fact, it's possible to provide more than one Component to do this. We
+could also view the entity in a tabbed view:
+
+![](images/230-multiple-views-for-entities.png)
+
+When there is more than one Component capable of rendering the object,
+the viewer allows the view to be selected:
+
+![](images/230-multiple-views-for-entities.png)
+
+This works for collections too; using the selector we can view a
+collection of Employees in a variety of ways:
+
+![](images/240-multiple-views-for-collections.png)
+
+Selecting the `icons` view shows the collection of Employees as icons:
+
+![](images/270-customize-ui-icons.png)
+
+So far all the screenshots we've seen have been generated from a basic
+domain application, with no custom GUI coding (see ?). But the *Wicket
+Viewer* is also extensible. So, we can write custom views/components,
+provide a corresponding ComponentFactory and then plug them into the
+*Wicket Viewer*'s registry of ComponentFactorys. For example, if we make
+Employee implement a view-specific Locatable interface, then we can
+render them on a google-maps mashup:
+
+![](images/280-customize-ui-maps-mashup.png)
+
+The selector, by the way, is actually implemented as yet another
+view/component, capable of rendering the entity or collection. It
+queries the ComponentFactory registry to determine how many other
+ComponentFactorys there are capable of rendering the entity or
+collection; if more than one then it provides a drop-down and then
+delegates to the other Components to do the work.
+
+Specialized Use Cases
+---------------------
+
+Being able to easily render entities and collection of entities without
+any custom UI coding is great for being able to develop an understanding
+of the problem domain. However, it doesn't always make sense to let the
+user interact directly with the domain objects. For example, if the
+domain objects are very fine-grained such that clicking into them would
+be tedious for the user to do, it makes sense to introduce another
+object that collect the required data and walk the graph of domain
+objects on the users behalf. Or, more straight forwardly, the use case
+might be particularly complex or subtle, and we want to provide the user
+with additional guidance.
+
+The *Wicket Viewer* therefore lets us work with objects designed to
+guide the user through the use case. Because they represent a particular
+solution to help the user achieve their objective, you can think of them
+as being part of the solution space (whereas regular domain objects
+belong to the problem space). Another name also given for objects of
+this type is "process objects"; they take the user through a particular
+process.
+
+For example, we might have a wizard that takes the user through the
+process of making a new Claim:
+
+![](images/300-process-objects.png)
+
+The object this action returns is not a Claim, instead it is a
+ClaimWizard. Unlike Claim, this is not persisted; its state is bound to
+a particular users' session. The design of the ClaimWizard is like any
+other wizard, taking the user through a number of pages; first an
+introductory page:
+
+![](images/310-wizard-intro-page.png)
+
+After that we are taken through pages for each of the properties; For
+example the next page prompts for the Claim's `claimant`:
+
+![](images/320-wizard-claimant-page.png)
+
+The Claim's `approver` and `description` properties likewise have the
+own pages, for example:
+
+![](images/330-wizard-approver-page.png)
+
+The final page allows the user to review details, then confirm:
+
+![](images/340-wizard-finish-page.png)
+
+On finish, the ClaimWizard will create and persists the Claim.
+
+Process objects like wizards are primarily concerned with inputting
+data. We can also have objects that are tailored towards the output of
+data, eg for reporting. For example, we could have a (non-persisted)
+ClaimSummary object that sums up Claim amounts by Claimant:
+
+![](images/400-analyze-claim-summary.png)
+
+We can then combine this with custom views, eg to represent a collection
+of such ClaimExpenseSummarys as a pie chart:
+
+![](images/410-analyze-summary-piechart.png)
+
+There's some guidance on writing application code to deal with such
+specialized use cases in ?.
+
+Running the Application
+=======================
+
+> This chapter describes how to take an *Apache Isis* application and
+> get it running using the *Wicket Viewer*, with the non-customized,
+> generic OOUI.
+
+Applications deployed to run with the *Wicket Viewer* are, ultimately
+just *Wicket* webapps that happen to boot up *Apache Isis*. As such,
+they are bootstrapped with a `web.xml` that is structured the same as
+any other *Wicket* application.
+
+This chapter describes how to run up such a webapp starting with a
+regular *Apache Isis* application.
+
+Structure of an Apache Isis Application
+---------------------------------------
+
+We recommend that you use *Apache Isis*' Maven application archetype to
+set up your application. Doing so will result in a multi-module project
+that contains the following modules:
+
+-   `app`
+
+    Main (parent) module, whose `pom.xml` references the submodules
+
+-   `app/dom`
+
+    Domain object model, plus interfaces for services, repositories and
+    factories
+
+-   app/domsvc
+
+    Implementation of domain services (not repositories/factories)
+
+-   `app/objectstore-default`
+
+    Implementation of repositories/factories for the default object
+    store
+
+-   `app/fixture`
+
+    Fixtures, used to seed (in-memory) object store when running in
+    exploration/prototype mode
+
+-   `app/commandline`
+
+    Bootstrap for running from the command line (typically, the DnD
+    viewer or HTML viewer)
+
+-   `app/viewer-wicket`
+
+    The application set up to be run using wicket, and packaged up as a
+    webapp.
+
+You will see that the archetypes creates a number of other
+`app/viewer-xxx` and `app/objectstore-xxx` modules. You can delete those
+modules that aren't relevant to your final deployment (probably the
+`app/viewer-xxx` modules and most of the `app/objectstore-xxx` modules,
+or you can just ignore them. If you do delete any modules, remember to
+remove their reference from the parent `app/pom.xml` file'a \<modules\>
+section).
+
+### The viewer-wicket module's web.xml
+
+The `viewer-wicket` module aready contains a `web.xml` file to define
+the web app, in the `src/main/webapp` directory. The contents of this is
+largely boilerplate, and will be very familiar if you already know the
+*Apache Wicket* framework:
+
+    <?xml version="1.0" encoding="ISO-8859-1"?>
+    <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+     xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
+     version="2.4">
+
+      <display-name>claims</display-name>
+      <filter>
+        <filter-name>wicket.app</filter-name>
+        <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
+        <init-param>
+          <param-name>applicationClassName</param-name>
+          <param-value>org.apache.isis.viewer.wicket.viewer.IsisWicketApplication</param-value>
+        </init-param>
+      </filter>
+
+      <filter-mapping>
+        <filter-name>wicket.app</filter-name>
+        <url-pattern>/*</url-pattern>
+      </filter-mapping>
+
+    </web-app>
+
+The IsisWicketApplication is a subclass of *Wicket*'s own WebApplication
+that bootstraps *Apache Isis*, handles authentication and sets up
+sessions and transactions.
+
+### Deployment Mode
+
+*Apache Wicket* webapps can be run in one of two "configuration type"s,
+either in the `development` configuration type or in the `deployment`
+configuration type. In Wicket, the configuration type can be specified
+as either:
+
+1.  A system property: `-D
+                wicket.configuration`
+
+2.  as a servlet specific `<init-param>`
+
+3.  as a context specific `<context-param>`
+
+Setting this value changes various properties, such as whether templates
+are reloaded. Wicket's Application\#configure() method is the place to
+look for all the differences.
+
+For its part, *Apache Isis* defines various "deployment mode"s. For
+example, `exploration` and `prototype` mode are both intended for
+single-user development, with the former requiring no login and also
+including any actions annotated as `@Exploration`. For multi-user
+(production) use, *Apache Isis* provides the `server` deployment mode,
+associating a separate *Apache Isis* runtime (IsisContext) to each
+thread (ie bound to a `ThreadLocal`).
+
+The *Wicket Viewer* maps *Wicket*'s `development` configuration type to
+*Apache Isis*' `prototype` deployment mode. However, the `server`
+deployment mode provided by *Apache Isis* is not quite appropriate for a
+*Wicket* webapp, because there could be multiple concurrent requests for
+a given user (originating from the same browser/user agent). The *Wicket
+Viewer* therefore defines a custom deployment mode which binds the
+*Apache Isis* runtime to the *Wicket* session (see the
+IsisContextForWicket class if you're interested in such things).
+
+What all this means is that selecting between *Apache Isis* deployment
+modes is done just by specifying the *Apache Wicket* configuration type.
+If you're already familiar with Wicket there's therefore nothing new to
+learn: just configure the webapp to run in either `development` or
+`deployment` mode.
+
+### Security
+
+Since both of the deployment modes supported by *Wicket Objects* require
+a login, it means we need to set up security. The IsisWicketApplication
+class provided by the *Wicket Viewer* subclasses from *Wicket*'s own
+AuthenticatedWebApplication, and serves up a sign-in page. To ensure
+that this sign-in page appears, every web page served up by the *Wicket
+Viewer* is annotated with
+`@AuthorizeInstantiation("org.apache.isis.viewer.wicket.roles.USER")`,
+which requires that every login has a role called
+`org.apache.isis.viewer.wicket.roles.USER`.
+
+*Apache Isis* deployment modes provide a couple of ways of avoiding
+login during development. For example, in `exploration` mode the viewers
+automatically login, while in `exploration` mode and `prototype` mode
+specifying a `LogonFixture` will means an automatic logon using the
+details provided within that fixture. The *Wicket Viewer* does not
+support `exploration` mode, however, and does not currently support
+LogonFixture. What that means is that it's always necessary to login.
+
+The *Wicket Viewer* delegates to *Apache Isis* to authentication.
+*Apache Isis*' default authentication mechanism is a no-op and requires
+no configuration. If you decide to a different authentication mechanism
+(eg file-based security, org.apache.isis.alternatives.security:file, or
+LDAP-based security, org.apache.isis.alternatives.security:ldap) then
+refer to the appropriate documentation for that module.
+
+The *Wicket Viewer* simply always places each login into the
+`org.apache.isis.viewer.wicket.roles.USER` role, to ensure that once the
+sign-in page is navigated past, that the user can access each web page.
+
+One consequence of this design is that the roles cannot currently be
+used by the *Apache Isis* domain objects nor the authorization
+mechanism; every user is simply in the USER role. This will be addressed
+in the future.
+
+Running as a WAR
+----------------
+
+The most straightforward approach for running a *Wicket
+Viewer*application is to build a WAR archive from the `viewer-wicket`
+module. Most IDEs will then allow this WAR to be deployed on an external
+servlet container, for debugging. For example, in Eclipse this is done
+by Run As \> Server:
+
+![](images/RunAsServer.png)
+
+and then select the external servlet container to deploy against:
+
+![](images/RunAsServer-dialog.png)
+
+If deploying this way, then the web browser URL may be slightly
+different. For example with Eclipse the URL to specify is
+<http://localhost:8080/webappname>.
+
+Running using *Isis*' WebServer class
+-------------------------------------
+
+As an alternative to deploying as a webapp, you can use Isis' WebServer
+class. This class has its own main() to bootstraps Jetty and configures
+a webapp from the `web.xml` file. If using Eclipse, for example, then yo
+can create a `.launch` configuration to run your application using
+WebServer.
+
+### Adding Dependency to Bootstrap
+
+The only thing you need to do is to edit the `viewer-wicket` module's
+`pom.xml`; add/comment in:
+
+    <dependency>
+     <groupId>org.apache.isis.core</groupId>
+     <artifactId>webserver</artifactId>
+    </dependency>
+
+### Running the Application
+
+Running the application is just a matter of running the
+org.apache.isis.WebServer. For example, in Eclipse the following launch
+configuration should suffice:
+
+![](images/WebServer-launch.png)
+
+Running this will boot strap a Jetty webserver:
+
+![](images/WebServer-running.png)
+
+You can then log on using <http://localhost:8080>:
+
+![](images/WicketObjects-logon-page.png)
+
+Customizing the Startup
+-----------------------
+
+Most of the remaining chapters address various means by which the UI
+generated by the *Wicket Viewer* can be customized. Before we go any
+further, though, we should mention that the viewer uses dependency
+injection provided by [Guice](http://google-guice.googlecode.com). It
+comes with default bindings, so customizing the *Wicket Viewer* requires
+overriding these default bindings.
+
+Note: this usage of Guice is likely to be replaced by JSR-299 based
+dependency mechanism.
+
+Specifically, in IsisWicketApplication, there's a method
+newIsisWicketModule():
+
+    public class IsisWicketApplication extends AuthenticatedWebApplication {
+      ...
+      protected Module newIsisWicketModule() {
+        return new IsisWicketModule();
+      }
+    }
+
+This method can be overridden in your own subclass of
+IsisWicketApplication, providing a new implementation of *Guice*'s
+Module interface. Typically you'll just want to override one or two of
+the bindings, so you should use the Modules helper class:
+
+    public class MyApplication extends IsisWicketApplication {
+      ...
+      protected Module newIsisWicketModule() {
+        return Modules.override(super.newIsisWicketModule()).with(
+          new AbstractModule() {
+            @Override
+            protected void configure() {
+              // override bindings here
+            }
+          }
+        );
+      }
+    }
+
+To pick up your subclass of IsisWicketApplication, just update the
+`web.xml` file.
+
+Customizing CSS
+===============
+
+> This chapter describes how to customize the UI generated by the
+> *Wicket Viewer* by providing application-specific CSS.
+
+CSS is used heavily by the *Wicket Viewer*, with the HTML of each
+Component using CSS classes for the styling. But you can override the
+default styling by supplying an application-level CSS file.
+
+By default this file should be called `application.css` and should
+reside within `WEB-INF` directory.
+
+There are two levels at which we can override the default styling,
+either generically or specific to a particular domain. Let's look at
+both.
+
+Generic CSS
+-----------
+
+The name of each class indicates the nature of what is being rendered,
+and each Component provides some default styling so that the
+out-of-the-box UI is usable. For example, an entity icon, title and
+actions are rendered as:
+
+![](images/EntitySummary.png)
+
+The icon and title for this are rendered as:
+
+    <div class="entitySummary">
+      <img src="?wicket:interface=:4:entity:entity:entitySummary:entityImage:1:IResourceListener::"
+           class="entityImage" wicket:id="entityImage">
+      <label class="entityTitle" wicket:id="entityTitle">Fred Smith</label>
+      ...
+    </div>
+
+So, to change the font, you could use:
+
+    div.entitySummary > label.entityTitle {
+      color: maroon;
+      font-size: xx-large;
+      font-weight: bolder;
+    }
+
+This would render the entity icon and title as:
+
+![](images/EntitySummary-Css.png)
+
+In this way you can develop a look-n-feel for the application (or
+perhaps your organization).
+
+Specific CSS
+------------
+
+As well as targetting HTML elements generically, individual class
+members can also be targetted for a particular domain model.
+
+For example, the properties of a Claim object might be rendered as:
+
+![](images/Claim-properties.png)
+
+The HTML for the description property is:
+
+    <div class="Claim-description" wicket:id="scalar">
+      <wicket:panel>
+        <div class="string scalarPanel">
+          <label wicket:id="scalarIfRegular" for="scalarValue3d">
+            <span class="scalarName" wicket:id="scalarName">description</span>
+            <span class="scalarValue">
+              <input type="text" title="" size="25" disabled="disabled"
+                 value="Meeting at clients" wicket:id="scalarValue"
+                 id="scalarValue3d"
+                 name="properties:1:scalar:scalarIfRegular:scalarValue">
+            </span>
+          </label>
+          <span wicket:id="feedback">
+            <wicket:panel>
+            </wicket:panel>
+          </span>
+        </div>
+      </wicket:panel>
+    </div>
+
+To change the label of this specific element, we could use:
+
+    .Claim-description .scalarName {
+      color: maroon;
+      font-weight: bolder;
+    }
+
+which would give us:
+
+![](images/Claim-properties-CSS.png)
+
+This is a slightly trite example, but demonstrates the point.
+
+Changing the location/name of the application CSS file
+------------------------------------------------------
+
+As already mentioned, the default for the application CSS file is in
+`WEB-INF/application.css`. If for any reason you want to change this,
+you can do so by subclassing IsisWicketApplication and overriding the
+newIsisWicketModule() method.
+
+For example, to use `css/myapp.css` (under `WEB-INF`) you would write:
+
+    public class MyApplication extends IsisWicketApplication {
+      ...
+      protected Module newIsisWicketModule() {
+        return Modules.override(super.newIsisWicketModule()).with(
+          new AbstractModule() {
+            @Override
+            protected void configure() {
+              bindConstant().annotatedWith(ApplicationCssUrl.class).to("css/myapp.css");
+            }
+          }
+        );
+      }
+    }
+
+Your custom application should then be registered in `web.xml`, as
+described in ?.
+
+Using the Wicket Viewer AppLib
+==============================
+
+> This chapter explains how to use certain viewer-specific features in
+> your domain application.
+
+Many *Isis* components have their own application library, the *Wicket
+Viewer* included. This allows you to specify additional viewer-specific
+semantics within your domain model; the viewer can then render them
+appropriately.
+
+Configuring the Project Modules
+-------------------------------
+
+In order to use the viewer-specific features, the `dom` module must
+reference the applib, and the `viewer-wicket` module must be configured
+appropriately.
+
+### Referencing the AppLib
+
+In the dom project, add in a `<dependency>` to the *Wicket Viewer*
+applib:
+
+    <dependencies>
+      ...
+
+      <!-- Wicket Viewer -->
+      <dependency>
+        <groupId>org.apache.isis.viewer.wicket</groupId>
+        <artifactId>applib</artifactId>
+      </dependency>
+
+      ...
+    </dependencies>
+
+This will allow you to use the *Wicket Viewer*'s annotations etc. in
+your domain objects.
+
+### Configuring the *Wicket Viewer* Facets
+
+In addition, you must also configure your application so that the *Isis*
+metamodel contains the information to be picked up by the *Wicket
+Viewer*.
+
+In the `viewer-wicket` module, add the following to `isis.properties`:
+
+    isis.reflector.facets.include=\
+        org.apache.isis.viewer.wicket.metamodel.wizardpagedesc.WizardPageDescriptionAnnotationFacetFactory\
+       ,org.apache.isis.viewer.wicket.metamodel.cssclass.CssClassAnnotationFacetFactory
+
+This basically instructs *Apache Isis* to capture additional information
+(facets) in its metamodel. Now let's look at what these facets are used
+for.
+
+@CssClass
+---------
+
+The @CssClass annotation allows you to specify an additional CSS class
+(or classes) applied on any object type, property, collection or action.
+This CSS will be added verbatim to any CSS classes that are normally
+applied by the *Wicket Viewer* itself.
+
+\*\*\* TODO: complete.
+
+There is further discussion on
+
+@WizardPageDescription
+----------------------
+
+It's common for wizards to have a description explaining what the user
+is expected to do. This can be modeled as a String property annotated
+with @WizardPageDescription, for example:
+
+    @NotPersistable
+    public class ClaimWizard extends AbstractDomainObject {
+
+        ...
+
+        @WizardPageDescription
+        @MemberOrder(sequence = "1")
+        public String getPageDescription() { ... }
+
+        ...
+    }
+
+Adding this annotation causes the *Wicket Viewer* to select a different
+Component to be used to render this property; specifically, one that
+renders the value as a large label. You can see the result of this
+annotation in below (the label above the claimant).
+
+![](images/320-wizard-claimant-page.png)
+
+Note that this annotation only affects the description; the three
+buttons shown in the above screenshot are as a result of the wizard
+being implemented as a custom component. For more on that topic, see ?.
+
+Customizing the Component Set
+=============================
+
+> This chapter describes the how to customize the UI by modifying the
+> set of components used to render objects, properties, collections or
+> actions.
+
+The *Wicket Viewer* offers several ways in which the UI rendered can be
+customized, from simply tweaking the CSS (see ?) through to the use of
+custom components for mashups and wizards. This chapter explains how to
+use pre-existing custom components; ? explains how to write your own
+components, and lists some components that have already been developed.
+
+There are more details provided in each of the sections that follow.
+
+Component Factories
+-------------------
+
+### ComponentFactory interface
+
+At its heart the *Wicket Viewer* consists of a set of *Wicket*
+`Component`s and corresponding IModel\<?\>s that are used to render
+entities, collections of elements and indeed individual members of
+elements. Each Components is created by a corresponding
+ComponentFactory, with the factory to use discovered using a
+chain-of-responsibility pattern.
+
+The Components created by the *Wicket Viewer* vary in size from
+rendering an entire collection of entities all the way down to a single
+property of an entity. You can find the full set of built-in Components
+by searching for implementations of ComponentFactory:
+
+![](images/ComponentFactory-hierarchy.png)
+
+For example, the CollectionContentsAsAjaxTableFactory class is used to
+render a collection of entities (eg returned from an action invocation)
+as, erm, an ajax table;
+
+    public class CollectionContentsAsAjaxTableFactory extends ComponentFactoryAbstract {
+      private static final long serialVersionUID = 1L;
+      private static final String NAME = "styled";
+
+      public CollectionContentsAsAjaxTableFactory() {
+        super(ComponentType.COLLECTION_OF_ENTITIES, NAME);
+      }
+
+      @Override
+      public boolean appliesTo(IModel<?> model) {
+        return model instanceof EntityCollectionModel;
+      }
+
+      public Component createComponent(String id, IModel<?> model) {
+        EntityCollectionModel collectionModel = (EntityCollectionModel) model;
+        return new CollectionContentsAsAjaxTable(id, collectionModel);
+      }
+    }
+
+The selection of the ComponentFactory is based on two criteria: the
+ComponentType, and the IModel\<?\>. Broadly speaking the ComponentType
+standardizes the `wicket:id` used in the HTML fragment (so
+`<div wicket:id="collectionContents"/>` would map onto the
+ComponentType.COLLECTION\_CONTENTS, while the IModel\<?\> is the
+corresponding information used for the rendering of that component. But
+there's a semi-formal relationship between these two concepts; the
+ComponentType effectively acting as a power-type for the subclass of
+IModel\<?\> that is supplied.
+
+The superclass ComponentFactoryAbstract takes responsibility for
+checking that the ComponentType matches the `wicket:id`, while
+delegating the checking of the IModel to its subtype:
+
+    public abstract class ComponentFactoryAbstract implements ComponentFactory ... {
+      ...
+      public final boolean appliesTo(ComponentType componentType, IModel<?> model) {
+        return componentType == getComponentType() && appliesTo(model);
+      }
+
+      protected abstract boolean appliesTo(IModel<?> model);
+      ...
+    }
+
+The subclass then refines this check by overriding appliesTo() to also
+check the model; returning true indicates that the ComponentFactory is
+able to render that model, after which the createComponent() method is
+then called to actually create the instance. So in
+CollectionContentsAsAjaxTableFactory, its implementation simply checks
+if the supplied model is an EntityCollectionModel. More
+sophisticated/less generic ComponentFactory might also make additional
+checks; you can find some examples of these in ?.
+
+### Registering ComponentFactorys using META-INF Services
+
+The easiest way to register new ComponentFactorys is using the JDK's own
+ServiceLoader capability.
+
+All that is needed is for your ComponentFactory to be registered in a
+file on the classpath call
+`META-INF/services/org.apache.isis.viewer.wicket.ui.ComponentFactory`.
+The contents of this file should be the fully qualified class name of
+your ComponentFactory implementation. And that's it! Registering the
+ComponentFactory is done automatically just by virtue of updating the
+classpath. You'll find that the custom components described in ? all use
+this technique.
+
+Note that this technique does not allow you to remove existing
+ComponentFactorys, only add new ones. So if you do want to exclude any
+of the built-in ComponentFactorys, then you will still need to use the
+ComponentFactoryList method.
+
+> **Note**
+>
+> This capability is provided by the ComponentFactoryRegistry component,
+> which is also bound in the IsisWicketModule. If you wanted to, you
+> could replace this higher-level registry component with your own
+> implementation. There's probably very little need to do this though,
+> given that you can just as easily customize using ComponentFactoryList
+> or `META-INF` services.
+
+### Registering ComponentFactorys using a custom ComponentFactoryList
+
+The set of ComponentFactorys that are provided with the *Wicket Viewer*
+are specified by the ComponentFactoryList interface.
+
+If you only want to add new factories, we recommend using the technique
+described in ?. But if you want to remove support for any of the default
+factories, or perhaps change the order in which factories are
+registered, you'll need to do use write and the bind in your own
+implementation of ComponentFactoryList.
+
+To write your implementation of ComponentFactoryList, start with the
+default implementation, ComponentFactoryListDefault. You'll see that it
+is written so that it can be easily subclassed and overridden on an
+as-needed basis. Worst case scenario, you can copy-and-paste code as
+necessary.
+
+Then, you need to override the binding in newIsisWicketModule(), eg:
+
+    public class MyApplication extends IsisWicketApplication {
+      ...
+      protected Module newIsisWicketModule() {
+        return Modules.override(super.newIsisWicketModule()).with(
+          new AbstractModule() {
+            @Override
+            protected void configure() {
+              bind(ComponentFactoryList.class).to(MyComponentFactoryList.class);
+            }
+          }
+        );
+      }
+    }
+
+You should also ensure that your MyComponentFactoryList is annotated
+with @Singleton.
+
+And, do remember to update `web.xml` to reference your subclass of
+IsisWicketApplication.
+
+Advanced Customization
+----------------------
+
+This section covers a couple of more advanced customization topics.
+
+### Page Registry
+
+While the content of any given web page rendered by the *Wicket Viewer*
+is made up of multiple Components, there are in fact only a small number
+of WebPages:
+
+-   WelcomePage displays the initial home page with a welcome message
+
+-   EntityPage displays a single entity
+
+-   ActionPage displays an action dialog or the results of invoking an
+    action.
+
+Each of these has a corresponding HTML page which defines the content of
+that page. In many cases the look-n-feel of these pages can be adjusted
+simply using CSS, as described in ?. If necessary though an entirely
+different page layout can be specified, for example to put the menubar
+on the left rather than at the top.
+
+The easiest approach to define a new page is to subclass PageAbstract
+superclass and then provide a different implementation of PageRegistry.
+As for ComponentFactorys, this is done by providing a new
+implementation, and then overriding a binding in newIsisWicketModule().
+
+The default pages are specified by PageClassListDefault:
+
+    public class PageClassListDefault implements PageClassList  {
+
+      @Override
+      pulic void registerPages(PageRegistrySpi pageRegistry) {
+        pageRegistry.registerPage(PageType.SIGN_IN, WicketSignInPage.class);
+        pageRegistry.registerPage(PageType.SIGN_OUT, WicketSignOutPage.class);
+        pageRegistry.registerPage(PageType.ENTITY, EntityPage.class);
+        pageRegistry.registerPage(PageType.HOME, HomePage.class);
+        pageRegistry.registerPage(PageType.ACTION, ActionPage.class);
+      }
+    }
+
+You can easily copy-n-paste this to create your own implementation. Note
+though that there must be a registered page for every PageType instance,
+otherwise the *Wicket Viewer* will fail fast on boot time.
+
+Overriding the binding is done in the usual way:
+
+    public class MyApplication extends IsisWicketApplication {
+      ...
+      protected Module newIsisWicketModule() {
+        return Modules.override(super.newIsisWicketModule()).with(
+          new AbstractModule() {
+            @Override
+            protected void configure() {
+              bind(PageClassList.class).to(MyPageClassList.class);
+            }
+          }
+        );
+      }
+    }
+
+Don't forget to update the web.xml to specify your subclass of
+IsisWicketApplication.
+
+> **Note**
+>
+> If all you want is to provide a custom rendering of a particular
+> interface or class, then you should instead write and register a
+> ComponentFactory, with a ComponentType.ENTITY and filtering the
+> EntityModel. The custom components described in ? do this, as does the
+> component registered in the test application to render a wizard (see
+> ?).
+
+### Subclassing IsisWicketApplication
+
+As we've seen, you can also customize *Wicket Objects* in various ways
+by subclassing the IsisWicketApplication bootstrap. The most common
+reason for doing so is to override the default implementation of
+ComponentFactoryList.
+
+This design follows the general style of *Wicket*; in fact, you'll see
+that IsisWicketApplication itself overrides a number of other methods
+(such as newRequestCycle() and newConverterLocator()), in order to hook
+*Apache Isis* into the rest of Wicket.
+
+In general it's unlikely that you'll need to alter the behavior of these
+hook methods; but it's useful to know that *Wicket Objects* doesn't
+particularly interfere with the way in which you may be used to
+customizing regular *Wicket* applications.
+
+Writing Custom Components
+=========================
+
+> This chapter provides some further guidance on objects designed to
+> support specialized use cases.
+
+Back in the application walkthrough (see ?) we saw that the *Wicket
+Viewer* has support for non-persisted objects that are designed to
+support specialized use cases. Whereas regular persisted domain objects
+can be thought of as part of the problem space, such non-persisted
+objects can be thought of as being part of the solution space because
+they offer a particular solution to a particular user objective. We call
+these objects *process objects* because objects they help a user perform
+a particular process.
+
+This chapter provides some general guidance on writing such process
+objects, and outlines the support that exists in *Wicket Objects* for
+writing custom components for such objects.
+
+Custom Components for Process Objects
+-------------------------------------
+
+Because process objects are there to guide the user, they often go
+hand-in-hand with custom components so that they can be rendered in a
+particular way.
+
+For example, in the application walkthrough we saw that a ClaimWizard
+process object is rendered with its previous(), next() and finish()
+actions as regular buttons:
+
+![](images/320-wizard-claimant-page.png)
+
+Similarly, the ClaimExpenseSummary object (or rather, a collection of
+them) hooks into the capabilities of the googlecharts component
+(discussed in ?):
+
+![](images/410-analyze-summary-piechart.png)
+
+You can use any of the *Wicket Viewer*'s built-in Components that are
+used to build the generic views for your own custom views; search down
+the ComponentFactory inheritance hierarchy and there's a good chance
+you'll find something of use. For example, you can easily add components
+to represent properties in a wizard (the custom Component for
+ClaimWizard does precisely this; you can inspect the code in ?).
+
+The *Wicket Viewer* also has a small number of Components intended for
+writing custom views.
+
+### ProcessObjectPanelAbstract
+
+The ProcessObjectPanelAbstract is intended to be used as the superclass
+for any panel-like Component that will render a process object. Its
+model is intended to be an EntityModel, which wraps the process object.
+
+This adapter class contains a number of convenience methods to help
+build panels:
+
+-   addProperties(Form\<?\> form, String id)
+
+    This adds the currently visible properties from the process object
+    to the provided form
+
+-   isValid(Form\<?\> form)
+
+    This validates the process object, where the form holds the
+    properties
+
+-   executeNoArgAction(String actionId)
+
+    Executes the indicated action on the process object (expected to
+    take no-arguments).
+
+Although minimal, these methods are sufficient to build a basic wizard.
+The executeNoArgAction() method, for example, allows `Previous`, `Next`
+and `Finish` buttons to be added.
+
+### Help Wanted!
+
+As you can see, the level of support provided by the *Wicket Viewer* for
+custom views is, admittedly, quite limited. There's nothing to prevent
+you from writing your own, of course, but to do so you'll need to use
+with the *Apache Isis* metamodel APIs.
+
+We hope to extend the components available as *Wicket Objects* continues
+to be developed. But in the meantime, if you build a Component that you
+think would be generally useful, please consider contributing it back to
+this project for the benefit of others.
+
+Use a Page enum for Wizards
+---------------------------
+
+Here's just a bit of good old-fashioned advice; consider using an enum
+to track the state of your wizards.
+
+For example, the ClaimWizard in the example app has the following enum
+defined:
+
+    @NotPersistable
+    public class ClaimWizard extends AbstractDomainObject {
+
+        public enum Page {
+            INTRO("This wizard will take you through the process of creating a claim"),
+            CLAIMANT("Enter the claimant that is making this claim"),
+            APPROVER("By default, the claimant's own approver will approve this claim.  " +
+              "Update here if another approver will approve this claim."),
+            DESCRIPTION("Update the description if required."),
+            SUMMARY("Confirm all details, or go back and amend if needed");
+
+            private String description;
+            private Page(String description) {
+                this.description = description;
+            }
+
+            public String getDescription() {
+                return description;
+            }
+
+            public boolean hasPrevious() {
+                return ordinal() > 0;
+            }
+            public Page previous() {
+                if (hasPrevious()) {
+                    return values()[ordinal() - 1];
+                } else {
+                    return this;
+                }
+            }
+
+            public boolean hasNext() {
+                return ordinal() < values().length - 1;
+            }
+            public Page next() {
+                if (hasNext()) {
+                    return values()[ordinal() + 1];
+                } else {
+                    return this;
+                }
+            }
+
+            @Ignore
+            public boolean is(Page... pages) {
+                for (Page page : pages) {
+                    if (page == this) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        }
+
+        ...
+    }
+
+This not only defines the descriptions of each Page, it also includes
+some logic for the previous() and next() actions to delegate to:
+
+    @NotPersistable
+    public class ClaimWizard extends AbstractDomainObject {
+
+        // {{ Page
+        private Page page;
+        @Hidden
+        public Page getPage() { ... }
+        public void setPage(final Page page) { ... }
+        // }}
+
+
+        // {{ Page Description
+        @WizardPageDescription
+        @MemberOrder(sequence = "1")
+        public String getPageDescription() { ... }
+
+        // {{ previous
+        @MemberOrder(sequence = "1")
+        public void previous() {
+            setPage(getPage().previous());
+        }
+        public String disablePrevious() {
+            return coalesce(noPreviousPage(), confirmIfOnSummaryPage());
+        }
+        private String noPreviousPage() {
+            return !getPage().hasPrevious() ? "no previous page" : null;
+        }
+        // }}
+
+        // {{ next
+        @MemberOrder(sequence = "2")
+        public void next() {
+            setPage(getPage().next());
+        }
+        public String disableNext() {
+            return coalesce(noNextPage(), confirmIfOnSummaryPage());
+        }
+        private String noNextPage() {
+            return !getPage().hasNext() ? "no next page" : null;
+        }
+        // }}
+
+        ...
+    }
+
+Custom Components in isis-contrib
+=================================
+
+> This chapter describes a number of custom components for the *Wicket
+> Viewer*. Some of these integrate third party components and/or
+> experimental.
+
+As was described in ?, the *Wicket Viewer* is designed to be extensible,
+allowing you to plug in more sophisticated renderings of the domain
+objects that make up your application. This chapter describes a number
+of custom components that demonstrate this capability, most of which are
+basically wrappers around functionality within the *Wicket*'s companion
+[WicketStuff](http://wicketstuff.org) project.
+
+The components here are probably best considered as examples rather than
+formally part of the *Wicket Viewer*, if only because we want the
+*Wicket Viewer* to depend just on core *Wicket*, not *WicketStuff*. But
+what you will find is that all the components here follow a similar
+layout, so you can easily adapt copy them into your own projects and
+adapt them as you feel fit.
+
+About the Components
+--------------------
+
+The source for these components is available at \*\*\*.
+
+### Common Layout
+
+Most of the components define their own interfaces or annotations; these
+are then implemented or annotated on the domain classes so that the
+component knows whether it applies or not (see discussion on
+ComponentFactory, in ?).
+
+To minimize the coupling between the domain objects and the component
+implementation, we separate out the interfaces/annotations into an
+applib.
+
+![](images/views-common-layout.png)
+
+The naming convention for these modules is:
+
+-   `org.starobjects.wicket:view-xxx` for the parent module for view
+    'xxx'
+
+-   `org.starobjects.wicket:view-xxx-applib` for the applib submodule
+
+-   `org.starobjects.wicket:view-xxx-view` for the view submodule (that
+    contains the actual ComponentFactory and Component implementations)
+
+### Adding Dependency Management for Custom Views
+
+In the parent project's `pom.xml`, specify the modules of the custom
+views that you want to use, along with the version:
+
+    <dependencyManagement>
+      <dependencies>
+        ...
+
+        <!-- Wicket Viewer view extensions -->
+        <dependency>
+          <groupId>org.apache.isis.viewer.wicket</groupId>
+          <artifactId>view-gmap2-applib</artifactId>
+          <version>${wicketobjects.version}</version>
+        </dependency>
+
+        <dependency>
+          <groupId>org.apache.isis.viewer.wicket</groupId>
+          <artifactId>view-gmap2-view</artifactId>
+          <version>${wicketobjects.version}</version>
+        </dependency>
+
+        <dependency>
+          <groupId>org.apache.isis.viewer.wicket</groupId>
+          <artifactId>view-googlecharts-applib</artifactId>
+          <version>${wicketobjects.version}</version>
+        </dependency>
+
+        <dependency>
+          <groupId>org.apache.isis.viewer.wicket</groupId>
+          <artifactId>view-googlecharts-view</artifactId>
+          <version>${wicketobjects.version}</version>
+        </dependency>
+
+        <dependency>
+          <groupId>org.apache.isis.viewer.wicket</groupId>
+          <artifactId>view-calendarviews-applib</artifactId>
+          <version>${wicketobjects.version}</version>
+        </dependency>
+
+        <dependency>
+          <groupId>org.apache.isis.viewer.wicket</groupId>
+          <artifactId>view-calendarviews-view</artifactId>
+          <version>${wicketobjects.version}</version>
+        </dependency>
+
+        <dependency>
+          <groupId>org.apache.isis.viewer.wicket</groupId>
+          <artifactId>view-cooldatasoftmenu-view</artifactId>
+          <version>${wicketobjects.version}</version>
+        </dependency>
+
+        ...
+      <dependencies>
+    </dependencyManagement>
+
+### Adding the Custom View's AppLibs as Dependencies
+
+\*\*\* Again, if you intend to use any of the custom components (see ?),
+then also add in dependencies to their respective applibs (if they have
+one):
+
+    <dependencies>
+      ...
+
+      <!-- Wicket Viewer view extensions -->
+      <dependency>
+        <groupId>org.starobjects.wicket</groupId>
+        <artifactId>view-calendarviews-applib</artifactId>
+      </dependency>
+
+      <dependency>
+        <groupId>org.starobjects.wicket</groupId>
+        <artifactId>view-gmap2-applib</artifactId>
+      </dependency>
+
+      <dependency>
+        <groupId>org.starobjects.wicket</groupId>
+        <artifactId>view-googlecharts-applib</artifactId>
+      </dependency>
+
+      ...
+    </dependencies>
+
+### Update Classpath
+
+The classpath for both the `dom` submodule and the `commandline` /
+`webapp` submodule each need to be updated (see ? for an overview of the
+typical structure of an *Apache Isis* application):
+
+-   the `dom` submodule should be updated to reference the
+    view-xxx-applib submodule for each custom component
+
+-   the `commandline` / `webapp` module should be updated to reference
+    the `view-xxx-view` submodule for each custom component
+
+Gmap2
+-----
+
+The gmap2 component renders a collection of objects in a Google map:
+
+![](images/280-customize-ui-maps-mashup.png)
+
+All that is required is for the object to implement Locatable interface,
+which in turn returns a Location value object:
+
+    package org.apache.isis.viewer.wicket.view.gmap2.applib;
+
+    public interface Locatable {
+        Location getLocation();
+    }
+
+If deploying on localhost, no API key is required. However, internet
+deployments do require an key, which should be specified as an
+init-parameter for the *Wicket* filter in `web.xml`:
+
+    <?xml version="1.0" encoding="ISO-8859-1"?>
+    <web-app ... >
+
+      ...
+
+      <filter>
+        <filter-name>wicket.app</filter-name>
+        <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
+        <init-param>
+          <param-name>applicationClassName</param-name>
+          <param-value>org.apache.isis.viewer.wicket.viewer.app.IsisWicketApplication</param-value>
+        </init-param>
+        <init-param>
+          <param-name>GOOGLE_MAPS_API_KEY</param-name>
+          <param-value>(key here)</param-value>
+        </init-param>
+      </filter>
+
+    </web-app>
+
+Google Charts
+-------------
+
+The googlechart component provides basic charting capabilities. It
+currently supports a single chart type; to render a collection as a pie
+chart:
+
+![](images/410-analyze-summary-piechart.png)
+
+All that is required is for the object to implement the (horribly named)
+PieChartable:
+
+    package org.apache.isis.viewer.wicket.view.googlecharts.applib;
+
+    public interface PieChartable {
+
+      double getPieChartValue();
+      String getPieChartLabel();
+    }
+
+The label is used to point to each sector on the pie chart, the value
+determines the size of each sector relative to the other sectors.
+
+CoolDataSoft Application Services Menu
+--------------------------------------
+
+The CoolDataSoft application services menu provides a different
+look-n-feel for the application services menu, using Ajax instead of
+CSS:
+
+![](images/cooldatasoft-appmenu.png)
+
+The implementation is based upon code lifted from the
+[wicket-menu](http://code.google.com/p/wicket-menu/) project, hosted on
+code.google.com. Please note that this code is GPLv3 and so cannot be
+used freely in commercial applications.
+
+Deployment Topics
+=================
+
+> This chapter touches on various topics that should be addressed prior
+> to deployment.
+
+Before you can deploy your application into production there are a
+number of things to be addressed. Most significantly of these is
+persistence, but security is another important topic.
+
+Because the *Wicket Viewer* runs on top of *Apache Isis*, many of the
+deployment tasks are based on the way in which *Apache Isis* tackles
+them.
+
+This chapter briefly outlines the main tasks from a *Wicket Objects*
+perspective. You might, though, want to dig out my book, [Domain Driven
+Design using Naked Objects](http://pragprog.com/titles/dhnako), for more
+in-depth coverage of the *Apache Isis* side-of-things (it covers *Apache
+Isis*' predecessor, *Naked Objects*, but is still broadly applicable).
+
+Running in a WebApp
+-------------------
+
+When developing *Apache Isis* applications you can run from either the
+`commandline` project or from the `webapp` project (see ?). If you've
+been using the former, then you'll need to switch to running from the
+latter so that your application can be built as a WAR for deployment.
+Take care to ensure that:
+
+-   the classpath dependencies are the same (so that any custom
+    components you're using or have written are picked up)
+
+-   that the `isis.properties` config file is the same
+
+Persistence
+-----------
+
+If you've been using the in-memory object store for development,
+obviously you'll need to switch to a persistent object store before you
+deploy.
+
+Going into the details of that is outside the scope of this guide, but
+it's worth noting that you have a number of options:
+
+-   the simplest persistence mechanism (albeit still only really for
+    prototyping) is to use the XML object store. You can specify this in
+    `isis.properties`:
+
+        isis.persistor=xml
+        isis.xmlos.dir=/tmp/xml
+
+-   more likely though you'll want to use a relational database. One
+    option is [JPA Objects](http://jpaobjects.sourceforge.net), another
+    sister project to *Apache Isis* (like *Wicket Objects* itself, in
+    fact). There's reasonable coverage in the
+    [DDDuNO](http://pragprog.com/titles/dhnako) book.
+
+-   *Apache Isis* also has (will have) a JDBC-based object store, SQL
+    Object Store
+
+-   If relational databases aren't your thing, *Apache Isis* also has
+    (will have) a BerkeleyDB Object Store
+
+Security
+--------
+
+By default, *Wicket Objects* is configured to use *Apache Isis*' default
+authentication and authorization. This are both file-based, with a
+simple passwords file to define users, and a similar file to define
+authorization. *Apache Isis* does though provide an implementation for
+both that use LDAP. This is discussed in the other *Apache Isis*
+documentation and in Dan Haywood's book. Alternatively, you could always
+write your own implementations to hook into your own security
+infrastructure.
+
+See also ?.
+
+Wicket DEPLOYMENT mode
+----------------------
+
+Finally, you'll also want to switch into Wicket deployment mode (ie for
+production). This is done in the normal way, by modifying *web.xml*:
+
+    <?xml version="1.0" encoding="ISO-8859-1"?>
+    <web-app ... >
+
+      ...
+
+      <filter>
+        <filter-name>wicket.app</filter-name>
+        <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
+        <init-param>
+          <param-name>applicationClassName</param-name>
+          <param-value>org.apache.isis.viewer.wicket.viewer.app.IsisWicketApplication</param-value>
+        </init-param>
+        <init-param>
+          <param-name>configuration</param-name>
+          <param-value>deployment</param-value>
+        </init-param>
+      </filter>
+
+    </web-app>
+
+Doing this also disables *Apache Isis* "exploration" actions (any action
+annotated with @Exploration will no longer be visible).
+
+Example Application
+===================
+
+> This appendix contains (almost) all the code that makes up the example
+> application shown in the screenshots in ?. The purpose in including
+> these listings is just to give you an idea of what it takes to write a
+> *Wicket Objects* application; this isn't a full tutorial on what it
+> all means.
+
+If you're interested in trying out the application, you'll find it at
+<https://wicketobjects.svn.sourceforge.net/svnroot/wicketobjects/trunk/testapp/claims>.
+
+Domain Application (Problem Space / Persisted Objects)
+------------------------------------------------------
+
+Most of the application shown in the screenshots (see ?) requires only
+the domain model. This is made up of three main entities, Employee,
+Claim and ClaimItem. The dependency between employee and claims package
+is acyclic; every Claim has a Claimant and an Approver, and Employee
+implements both the Approver and Claimant interfaces.
+
+### claims package
+
+#### Claim
+
+The Claim class is by far the largest domain class. Below is a listing
+of all the methods; the body of the getters and setters and some of the
+validation methods have been omitted.
+
+    package org.apache.isis.examples.claims.dom.claim;
+
+    import java.util.ArrayList;
+    import java.util.List;
+
+    import org.apache.isis.applib.AbstractDomainObject;
+    import org.apache.isis.applib.annotation.Disabled;
+    import org.apache.isis.applib.annotation.Ignore;
+    import org.apache.isis.applib.annotation.MaxLength;
+    import org.apache.isis.applib.annotation.MemberOrder;
+    import org.apache.isis.applib.annotation.Named;
+    import org.apache.isis.applib.annotation.Optional;
+    import org.apache.isis.applib.value.Date;
+    import org.apache.isis.applib.value.Money;
+    import org.apache.isis.viewer.wicket.applib.CalendarEvent;
+    import org.apache.isis.viewer.wicket.applib.Calendarable;
+
+    public class Claim extends AbstractDomainObject implements Calendarable {
+
+        // {{ Title
+        public String title() {
+            return getStatus() + " - " + getDate();
+        }
+        // }}
+
+        // {{ Lifecycle
+        public void created() {
+            status = "New";
+            date = new Date();
+        }
+        // }}
+
+        // {{ Rush
+        private boolean rush;
+        @MemberOrder(sequence = "1.2")
+        public boolean getRush() { ... }
+        public void setRush(final boolean flag) { ... }
+        // }}
+
+        // {{ Description
+        private String description;
+        @MemberOrder(sequence = "1")
+        public String getDescription() { ... }
+        public void setDescription(String description) { ... }
+        public String validateDescription(final String description) { ... }
+        // }}
+
+        // {{ Date
+        private Date date;
+        @MemberOrder(sequence = "2")
+        public Date getDate() { ... }
+        public void setDate(Date date) { ... }
+        public String disableDate() { ... }
+        // }}
+
+        // {{ Status
+        private String status;
+        @Disabled
+        @MemberOrder(sequence = "3")
+        @MaxLength(5)
+        public String getStatus() { ... }
+        public void setStatus(String status) { ... }
+        // }}
+
+        // {{ Claimant
+        private Claimant claimant;
+        @Disabled
+        @MemberOrder(sequence = "4")
+        public Claimant getClaimant() { ... }
+        public void setClaimant(Claimant claimant) { ... }
+        // }}
+
+        // {{ Approver
+        private Approver approver;
+        @MemberOrder(sequence = "5")
+        @Optional
+        public Approver getApprover() { ... }
+        public void setApprover(Approver approver) { ... }
+        public String disableApprover() { ... }
+        public String validateApprover(final Approver approver) {
+            if (approver == null)
+                return null;
+            return approver == getClaimant() ? "Can't approve own claims" : null;
+        }
+        // }}
+
+        // {{ Items
+        private List<ClaimItem> items = new ArrayList<ClaimItem>();
+        @MemberOrder(sequence = "6")
+        public List<ClaimItem> getItems() { ... }
+        public void addToItems(ClaimItem item) { ... }
+        // }}
+
+        // {{ action: Submit
+        public void submit(Approver approver) { ... }
+        public String disableSubmit() {
+            return getStatus().equals("New") ? null
+                    : "Claim has already been submitted";
+        }
+        public Object default0Submit() {
+            return getClaimant().getApprover();
+        }
+        // }}
+
+        // {{ action: addItem
+        public void addItem(@Named("Days since") int days,
+                @Named("Amount") double amount,
+                @Named("Description") String description) {
+            ClaimItem claimItem = newTransientInstance(ClaimItem.class);
+            Date date = new Date();
+            date = date.add(0, 0, days);
+            claimItem.setDateIncurred(date);
+            claimItem.setDescription(description);
+            claimItem.setAmount(new Money(amount, "USD"));
+            persist(claimItem);
+            addToItems(claimItem);
+        }
+        public String disableAddItem() { ... }
+            return "Submitted".equals(getStatus()) ? "Already submitted" : null;
+        }
+        // }}
+
+        // object-level validation
+        public String validate() { ... }
+    }
+
+Some points worth noting:
+
+-   Although Claim is inheriting from *Apache Isis*'
+    AbstractDomainObject class, this isn't mandatory.
+
+-   Claim has reference properties of type Claimant and Approver. As
+    we'll see below these are interfaces. References to both interface
+    and classes is supported in *Apache Isis*.
+
+-   The Claim uses a Money class, a value type provided by *Apache
+    Isis*. It's also possible to write ones own value types (or indeed
+    use third-party value types such as JodaTime).
+
+#### ClaimItem
+
+A Claim has a collection of ClaimItems. A ClaimItem is somewhat simpler
+than Claim, and doesn't have any particular behavior itself:
+
+    package org.apache.isis.examples.claims.dom.claim;
+
+    import org.apache.isis.applib.AbstractDomainObject;
+    import org.apache.isis.applib.annotation.MemberOrder;
+    import org.apache.isis.applib.value.Date;
+    import org.apache.isis.applib.value.Money;
+
+    public class ClaimItem extends AbstractDomainObject {
+
+        // {{ Title
+        public String title() {
+            return getDescription();
+        }
+        // }}
+
+        // {{ DateIncurred
+        private Date dateIncurred;
+        @MemberOrder(sequence = "1")
+        public Date getDateIncurred() { ... }
+        public void setDateIncurred(Date dateIncurred) { ... }
+        // }}
+
+        // {{ Description
+        private String description;
+        @MemberOrder(sequence = "2")
+        public String getDescription() { ... }
+        public void setDescription(String description) { ... }
+        // }}
+
+        // {{ Amount
+        private Money amount;
+        @MemberOrder(sequence = "3")
+        public Money getAmount() { ... }
+        public void setAmount(Money price) { ... }
+        // }}
+    }
+
+#### Approver and Claimant
+
+The Approver and Claimant interfaces decouple Claim from any classes
+outside the claims package. The Approver interface is, in fact, empty:
+
+    package org.apache.isis.examples.claims.dom.claim;
+
+    public interface Approver {
+
+    }
+
+There's not a lot more to Claimant:
+
+    package org.apache.isis.examples.claims.dom.claim;
+
+    public interface Claimant {
+
+        Approver getApprover();
+
+        String title();
+    }
+
+#### ClaimRepository
+
+The ClaimRepository interface is one of the two domain services (as
+appearing in the menu bar), and is defined as:
+
+    package org.apache.isis.examples.claims.dom.claim;
+
+    import java.util.List;
+
+    import org.apache.isis.applib.annotation.Named;
+    import org.apache.isis.applib.value.Date;
+
+    @Named("Claims")
+    public interface ClaimRepository {
+
+        public List<Claim> allClaims();
+
+        public List<Claim> findClaims(@Named("Description") String description);
+
+        public List<Claim> claimsFor(Claimant claimant);
+
+        public List<Claim> claimsSince(Claimant claimant, Date since);
+
+        public ClaimWizard newClaim(Claimant claimant);
+
+        public List<ClaimantExpenseSummary> analyseClaimantExpenses();
+    }
+
+### employee package
+
+The employee package depends on the claim package in that the Employee
+class implements the Claimant and Approver interfaces. Among other
+things, this allows the actions of the ClaimRepository to be
+"contributed" to the Employee class (appear in a "claims" submenu for
+each Employee).
+
+#### Employee
+
+The Employee class is the other main class in this app:
+
+    package org.apache.isis.examples.claims.dom.employee;
+
+    import org.apache.isis.applib.AbstractDomainObject;
+    import org.apache.isis.applib.annotation.Disabled;
+    import org.apache.isis.applib.annotation.MemberOrder;
+    import org.apache.isis.examples.claims.dom.claim.Approver;
+    import org.apache.isis.examples.claims.dom.claim.Claimant;
+    import org.apache.isis.viewer.wicket.applib.Locatable;
+    import org.apache.isis.viewer.wicket.applib.Location;
+
+    public class Employee extends AbstractDomainObject implements Claimant,
+            Approver, Locatable {
+
+        // {{ Title
+        public String title() {
+            return getName();
+        }
+
+        // }}
+
+        // {{ Icon
+        public String iconName() {
+            return getName().replaceAll(" ", "");
+        }
+        // }}
+
+        // {{ Name
+        private String name;
+        @MemberOrder(sequence = "1")
+        public String getName() { ... }
+        public void setName(String lastName) { ... }
+        // }}
+
+        // {{ Approver
+        private Approver approver;
+        @MemberOrder(sequence = "2")
+        public Approver getApprover() { ... }
+        public void setApprover(Approver approver) { ... }
+        // }}
+
+        // {{ Location
+        private Location location;
+        @Disabled
+        @MemberOrder(sequence = "1")
+        public Location getLocation() { ... }
+        public void setLocation(final Location location) { ... }
+        // }}
+    }
+
+A couple points worth noting:
+
+-   The Employee class has an iconName() method. This is used to render
+    Employees with a customized image for each instance.
+
+-   Employee also implements Locatable. This is used to render the
+    Employee in the gmap2 (google maps mashup) view (see ?).
+
+#### EmployeeRepository
+
+The EmployeeRepository interface defines the other domain service (on
+the services menu):
+
+    package org.apache.isis.examples.claims.dom.employee;
+
+    import java.util.List;
+
+    import org.apache.isis.applib.annotation.Named;
+
+    @Named("Employees")
+    public interface EmployeeRepository {
+
+        public List<Employee> allEmployees();
+        public List<Employee> findEmployees(@Named("Name") String name);
+    }
+
+Specialized Use Cases
+---------------------
+
+Domain objects to support specialized use cases (solution space objects)
+are not persisted; instead their state is serialized into the *Wicket*
+page components.
+
+### ClaimWizard
+
+The ClaimWizard uses an internal `page` field (of type Page enum) to
+determine which page the user is on; from this we determine which
+properties should be visible, and whether the `previous()`, `next()` and
+`finish()` actions are available.
+
+    package org.apache.isis.examples.claims.dom.claim;
+
+    import java.util.Calendar;
+    import java.util.List;
+
+    import org.apache.isis.applib.AbstractDomainObject;
+    import org.apache.isis.applib.annotation.Disabled;
+    import org.apache.isis.applib.annotation.Hidden;
+    import org.apache.isis.applib.annotation.Ignore;
+    import org.apache.isis.applib.annotation.MemberOrder;
+    import org.apache.isis.applib.annotation.NotPersistable;
+    import org.apache.isis.applib.annotation.TypicalLength;
+    import org.apache.isis.applib.clock.Clock;
+    import org.apache.isis.examples.claims.dom.employee.EmployeeRepository;
+    import org.apache.isis.viewer.wicket.applib.WizardPageDescription;
+
+    @NotPersistable
+    public class ClaimWizard extends AbstractDomainObject {
+
+        public enum Page {
+            INTRO("This wizard will take you through the process of creating a claim"),
+            CLAIMANT("Enter the claimant that is making this claim"),
+            APPROVER("By default, the claimant's own approver will approve this claim.  " +
+                     "Update here if another approver will approve this claim."),
+            DESCRIPTION("Update the description if required."),
+            SUMMARY("Confirm all details, or go back and amend if needed");
+
+            private String description;
+            private Page(String description) {
+                this.description = description;
+            }
+
+            public String getDescription() {
+                return description;
+            }
+
+            public boolean hasPrevious() {
+                return ordinal() > 0;
+            }
+            public Page previous() {
+                if (hasPrevious()) {
+                    return values()[ordinal() - 1];
+                } else {
+                    return this;
+                }
+            }
+
+            public boolean hasNext() {
+                return ordinal() < values().length - 1;
+            }
+            public Page next() {
+                if (hasNext()) {
+                    return values()[ordinal() + 1];
+                } else {
+                    return this;
+                }
+            }
+
+            @Ignore
+            public boolean is(Page... pages) {
+                for (Page page : pages) {
+                    if (page == this) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        }
+
+        // {{ Lifecycle
+        public void created() {
+            setPage(Page.INTRO);
+            setDescription("Expenses for week #" + weekNum());
+        }
+        private int weekNum() {
+            return getTimeAsCalendar().get(Calendar.WEEK_OF_YEAR);
+        }
+        protected Calendar getTimeAsCalendar() {
+            return Clock.getTimeAsCalendar();
+        }
+        // }}
+
+        // {{ Page
+        private Page page;
+        @Hidden
+        public Page getPage() { ... }
+        public void setPage(final Page page) { ... }
+        // }}
+
+        // {{ Page Description
+        @WizardPageDescription
+        @TypicalLength(60)
+        @MemberOrder(sequence = "1")
+        public String getPageDescription() {
+            return getPage().getDescription();
+        }
+        // }}
+
+        // {{ Claimant
+        private Claimant claimant;
+   

<TRUNCATED>

[3/3] git commit: ISIS-233: invoking action with missing args or surplus args

Posted by da...@apache.org.
ISIS-233: invoking action with missing args or surplus args

* nb: this has broken some of the unit tests, due to change in JsonRepresentation's handling of null.  Need to fix.
* have also added some ZzzToDo placeholders for future tests to be written (for 405 method_not_acceptable).


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

Branch: refs/heads/dan/ISIS-233-ro
Commit: 8210a58d75d41877ab03d8c2b1808bfc57ef1d08
Parents: 25ebaa9
Author: Dan Haywood <da...@apache.org>
Authored: Thu Mar 21 08:51:05 2013 +0000
Committer: Dan Haywood <da...@apache.org>
Committed: Thu Mar 21 08:51:05 2013 +0000

----------------------------------------------------------------------
 .../restfulobjects/applib/JsonRepresentation.java  |    5 +-
 .../restfulobjects/applib/LinkRepresentation.java  |    6 +
 .../applib/client/RestfulResponse.java             |    2 +-
 .../RestfulObjectsApplicationExceptionMapper.java  |   38 +-
 .../resources/DomainObjectResourceServerside.java  |   10 +-
 .../server/resources/DomainResourceHelper.java     |   38 +-
 ...ZzzTodo_idempotent_fail_method_not_allowed.java |    5 +
 ...Todo_nonidempotent_fail_method_not_allowed.java |    5 +
 .../ZzzTodo_safe_fail_method_not_allowed.java      |    5 +
 .../ZzzTodo_addTo_fail_method_not_allowed.java     |    5 +
 .../ZzzTodo_details_fail_method_not_allowed.java   |    5 +
 ...ZzzTodo_removeFrom_fail_method_not_allowed.java |    5 +
 .../ZzzTodo_clear_fail_method_not_allowed.java     |    5 +
 .../ZzzTodo_details_fail_method_not_allowed.java   |    5 +
 .../ZzzTodo_modify_fail_method_not_allowed.java    |    5 +
 .../action/invoke/DomainServiceTest_forbidden.java |  122 +
 ...mainServiceTest_req_safe_arg_bad_malformed.java |   20 +-
 ..._req_safe_simplearg_fail_mandatory_missing.java |   34 +-
 ...q_safe_simplearg_fail_mandatory_value_null.java |  133 +
 ...ceTest_req_safe_simplearg_fail_surplus_arg.java |  135 +
 ...est_req_idempotent_fail_method_not_allowed.java |  112 +
 ..._req_nonidempotent_fail_method_not_allowed.java |  112 +
 ...rviceTest_req_safe_fail_method_not_allowed.java |  112 +
 .../wicket/src/docbkx/guide/isis-wicket-viewer.md  | 2958 +++++++++++++++
 .../tck/dom/actions/ActionsEntityRepository.java   |    2 +-
 .../tck/dom/busrules/BusRulesEntityRepository.java |    4 +-
 26 files changed, 3803 insertions(+), 85 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/JsonRepresentation.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/JsonRepresentation.java b/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/JsonRepresentation.java
index 17c0cad..2f6e52c 100644
--- a/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/JsonRepresentation.java
+++ b/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/JsonRepresentation.java
@@ -867,12 +867,9 @@ public class JsonRepresentation {
         if (!isMap()) {
             throw new IllegalStateException("does not represent map");
         }
-        if (value == null) {
-            return;
-        }
         final Path path = Path.parse(key);
         final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
-        node.put(path.getTail(), new POJONode(value));
+        node.put(path.getTail(), value != null? new POJONode(value): NullNode.getInstance() );
     }
 
     public void mapPut(final String key, final JsonRepresentation value) {

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/LinkRepresentation.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/LinkRepresentation.java b/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/LinkRepresentation.java
index bafbdba..4058734 100644
--- a/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/LinkRepresentation.java
+++ b/component/viewer/restfulobjects/applib/src/main/java/org/apache/isis/viewer/restfulobjects/applib/LinkRepresentation.java
@@ -47,6 +47,11 @@ public final class LinkRepresentation extends JsonRepresentation {
         asObjectNode().put("rel", rel);
         return this;
     }
+    
+    public LinkRepresentation withRel(Rel rel) {
+        return withRel(rel.getName());
+    }
+
 
     public String getHref() {
         return asObjectNode().path("href").getTextValue();
@@ -161,4 +166,5 @@ public final class LinkRepresentation extends JsonRepresentation {
         return "Link [rel=" + getRel() + ", href=" + getHref() + ", method=" + getHttpMethod() + ", type=" + getType() + "]";
     }
 
+
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/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 c1869ac..2562ef6 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
@@ -124,7 +124,7 @@ public class RestfulResponse<T> {
         public final static HttpStatusCode UNAUTHORIZED = new HttpStatusCode(401, Status.UNAUTHORIZED);
 
         // public static final int SC_PAYMENT_REQUIRED = 402;
-        // public static final int SC_FORBIDDEN = 403;
+        public static final HttpStatusCode FORBIDDEN = new HttpStatusCode(403, Status.FORBIDDEN);
 
         public final static HttpStatusCode NOT_FOUND = new HttpStatusCode(404, Status.NOT_FOUND);
         public final static HttpStatusCode METHOD_NOT_ALLOWED = new HttpStatusCode(405, new StatusTypeImpl(405, Family.CLIENT_ERROR, "Method not allowed"));

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/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 36a6b08..6033588 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
@@ -41,13 +41,28 @@ public class RestfulObjectsApplicationExceptionMapper implements ExceptionMapper
     @Override
     public Response toResponse(final RestfulObjectsApplicationException ex) {
         final ResponseBuilder builder = Response.status(ex.getHttpStatusCode().getJaxrsStatusType());
-        final String body = bodyFor(ex);
-        if(body != null) {
+
+        // body and content-type
+        final JsonRepresentation bodyRepr = ex.getBody();
+        final Throwable cause = ex.getCause();
+        if (bodyRepr != null) {
+            final String body = bodyRepr.toString();
             builder.entity(body);
             builder.type(MediaType.APPLICATION_JSON); // generic; the spec doesn't define what the media type should be
-        } else {
+        } else if(cause == null) {
+            builder.type(MediaType.APPLICATION_JSON); // generic; the spec doesn't define what the media type should be
+        } else { 
+            String body;
+            try {
+                body = JsonMapper.instance().write(ExceptionPojo.create(cause));
+            } catch (final Exception e) {
+                // fallback
+                body = "{ \"exception\": \"" + ExceptionUtils.getFullStackTrace(cause) + "\" }";
+            }
+            builder.entity(body);
             builder.type(RestfulMediaType.APPLICATION_JSON_ERROR);
         }
+
         final String message = ex.getMessage();
         if (message != null) {
             builder.header(RestfulResponse.Header.WARNING.getName(), RestfulResponse.Header.WARNING.render(message));
@@ -55,23 +70,6 @@ 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) {

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/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
index 24ff44f..afa6a64 100644
--- 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
@@ -189,7 +189,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr);
         final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
 
-        final OneToOneAssociation property = helper.getPropertyThatIsVisibleAndUsable(propertyId, Intent.MUTATE, getResourceContext().getWhere());
+        final OneToOneAssociation property = helper.getPropertyThatIsVisibleForIntent(propertyId, Intent.MUTATE, getResourceContext().getWhere());
 
         final ObjectSpecification propertySpec = property.getSpecification();
         final String bodyAsString = DomainResourceHelper.asStringUtf8(body);
@@ -216,7 +216,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr);
         final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
 
-        final OneToOneAssociation property = helper.getPropertyThatIsVisibleAndUsable(propertyId, Intent.MUTATE, getResourceContext().getWhere());
+        final OneToOneAssociation property = helper.getPropertyThatIsVisibleForIntent(propertyId, Intent.MUTATE, getResourceContext().getWhere());
 
         final Consent consent = property.isAssociationValid(objectAdapter, null);
         if (consent.isVetoed()) {
@@ -256,7 +256,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr);
         final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
 
-        final OneToManyAssociation collection = helper.getCollectionThatIsVisibleAndUsable(collectionId, Intent.MUTATE, getResourceContext().getWhere());
+        final OneToManyAssociation collection = helper.getCollectionThatIsVisibleForIntent(collectionId, Intent.MUTATE, getResourceContext().getWhere());
 
         if (!collection.getCollectionSemantics().isSet()) {
             throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.BAD_REQUEST, "Collection '%s' does not have set semantics", collectionId);
@@ -287,7 +287,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr);
         final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
 
-        final OneToManyAssociation collection = helper.getCollectionThatIsVisibleAndUsable(collectionId, Intent.MUTATE, getResourceContext().getWhere());
+        final OneToManyAssociation collection = helper.getCollectionThatIsVisibleForIntent(collectionId, Intent.MUTATE, getResourceContext().getWhere());
 
         if (!collection.getCollectionSemantics().isListOrArray()) {
             throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "Collection '%s' does not have list or array semantics", collectionId);
@@ -317,7 +317,7 @@ public class DomainObjectResourceServerside extends ResourceAbstract implements
         final ObjectAdapter objectAdapter = getObjectAdapterElseThrowNotFound(domainType, oidStr);
         final DomainResourceHelper helper = new DomainResourceHelper(getResourceContext(), objectAdapter);
 
-        final OneToManyAssociation collection = helper.getCollectionThatIsVisibleAndUsable(collectionId, Intent.MUTATE, getResourceContext().getWhere());
+        final OneToManyAssociation collection = helper.getCollectionThatIsVisibleForIntent(collectionId, Intent.MUTATE, getResourceContext().getWhere());
 
         final ObjectSpecification collectionSpec = collection.getSpecification();
         final ObjectAdapter argAdapter = helper.parseAsMapWithSingleValue(collectionSpec, getResourceContext().getQueryString());

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/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 8697183..d914f5f 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
@@ -188,7 +188,7 @@ public final class DomainResourceHelper {
 
     Response propertyDetails(final String propertyId, final MemberMode memberMode, final Caching caching, Where where) {
 
-        final OneToOneAssociation property = getPropertyThatIsVisibleAndUsable(propertyId, Intent.ACCESS, where);
+        final OneToOneAssociation property = getPropertyThatIsVisibleForIntent(propertyId, Intent.ACCESS, where);
 
         final ObjectPropertyReprRenderer renderer = new ObjectPropertyReprRenderer(resourceContext, null, null, JsonRepresentation.newMap());
 
@@ -205,7 +205,7 @@ public final class DomainResourceHelper {
 
     Response collectionDetails(final String collectionId, final MemberMode memberMode, final Caching caching, Where where) {
 
-        final OneToManyAssociation collection = getCollectionThatIsVisibleAndUsable(collectionId, Intent.ACCESS, where);
+        final OneToManyAssociation collection = getCollectionThatIsVisibleForIntent(collectionId, Intent.ACCESS, where);
 
         final ObjectCollectionReprRenderer renderer = new ObjectCollectionReprRenderer(resourceContext, null, null, JsonRepresentation.newMap());
 
@@ -221,7 +221,7 @@ public final class DomainResourceHelper {
     // //////////////////////////////////////////////////////////////
 
     Response actionPrompt(final String actionId, Where where) {
-        final ObjectAction action = getObjectActionThatIsVisibleAndUsable(actionId, Intent.ACCESS, where);
+        final ObjectAction action = getObjectActionThatIsVisibleForIntent(actionId, Intent.ACCESS, where);
 
         final ObjectActionReprRenderer renderer = new ObjectActionReprRenderer(resourceContext, null, null, JsonRepresentation.newMap());
 
@@ -243,7 +243,7 @@ public final class DomainResourceHelper {
     }
 
     Response invokeActionQueryOnly(final String actionId, final JsonRepresentation arguments, Where where) {
-        final ObjectAction action = getObjectActionThatIsVisibleAndUsable(actionId, Intent.ACCESS, where);
+        final ObjectAction action = getObjectActionThatIsVisibleForIntent(actionId, Intent.MUTATE, where);
 
         final ActionSemantics.Of actionSemantics = action.getSemantics();
         if (actionSemantics != ActionSemantics.Of.SAFE) {
@@ -255,7 +255,7 @@ public final class DomainResourceHelper {
 
     Response invokeActionIdempotent(final String actionId, final JsonRepresentation arguments, Where where) {
 
-        final ObjectAction action = getObjectActionThatIsVisibleAndUsable(actionId, Intent.MUTATE, where);
+        final ObjectAction action = getObjectActionThatIsVisibleForIntent(actionId, Intent.MUTATE, where);
 
         final ActionSemantics.Of actionSemantics = action.getSemantics();
         if (!actionSemantics.isIdempotentInNature()) {
@@ -265,7 +265,7 @@ public final class DomainResourceHelper {
     }
 
     Response invokeAction(final String actionId, final JsonRepresentation arguments, Where where) {
-        final ObjectAction action = getObjectActionThatIsVisibleAndUsable(actionId, Intent.MUTATE, where);
+        final ObjectAction action = getObjectActionThatIsVisibleForIntent(actionId, Intent.MUTATE, where);
 
         return invokeActionUsingAdapters(action, arguments);
     }
@@ -362,37 +362,37 @@ public final class DomainResourceHelper {
     // get{MemberType}ThatIsVisibleAndUsable
     // ///////////////////////////////////////////////////////////////////
 
-    protected OneToOneAssociation getPropertyThatIsVisibleAndUsable(final String propertyId, final Intent intent, Where where) {
+    protected OneToOneAssociation getPropertyThatIsVisibleForIntent(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);
+        return memberThatIsVisibleForIntent(property, MemberType.PROPERTY, intent, where);
     }
 
-    protected OneToManyAssociation getCollectionThatIsVisibleAndUsable(final String collectionId, final Intent intent, Where where) {
+    protected OneToManyAssociation getCollectionThatIsVisibleForIntent(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);
+        return memberThatIsVisibleForIntent(collection, MemberType.COLLECTION, intent, where);
     }
 
-    protected ObjectAction getObjectActionThatIsVisibleAndUsable(final String actionId, final Intent intent, Where where) {
+    protected ObjectAction getObjectActionThatIsVisibleForIntent(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);
+        return memberThatIsVisibleForIntent(action, MemberType.ACTION, intent, where);
     }
 
-    protected <T extends ObjectMember> T memberThatIsVisibleAndUsable(final T objectMember, final MemberType memberType, final Intent intent, Where where) {
+    protected <T extends ObjectMember> T memberThatIsVisibleForIntent(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()) {
@@ -401,8 +401,7 @@ public final class DomainResourceHelper {
         if (intent.isMutate()) {
             final Consent usable = objectMember.isUsable(authenticationSession, objectAdapter, where);
             if (usable.isVetoed()) {
-                final String memberTypeStr = memberType.name().toLowerCase();
-                throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.NOT_ACCEPTABLE, "%s is not usable: '%s' (%s)", memberTypeStr, memberId, usable.getReason());
+                throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.FORBIDDEN, usable.getReason());
             }
         }
         return objectMember;
@@ -426,7 +425,6 @@ public final class DomainResourceHelper {
      */
     ObjectAdapter parseAsMapWithSingleValue(final ObjectSpecification objectSpec, final String bodyAsString) {
         final JsonRepresentation arguments = readAsMap(bodyAsString);
-
         return parseAsMapWithSingleValue(objectSpec, arguments);
     }
 
@@ -488,9 +486,9 @@ public final class DomainResourceHelper {
         for (final Entry<String, JsonRepresentation> arg : arguments.mapIterable()) {
             final String argName = arg.getKey();
             if (action.getParameterById(argName) == null) {
-                String reason = String.format("Action '%s' does not have a parameter %s but an argument of that name was provided", action.getId(), argName);
+                String reason = String.format("Argument '%s' found but no such parameter", argName);
                 arguments.mapPut("x-ro-invalidReason", reason);
-                throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.VALIDATION_FAILED, reason);
+                throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.BAD_REQUEST, arguments, reason);
             }
         }
 
@@ -501,9 +499,9 @@ public final class DomainResourceHelper {
             final String paramId = param.getId();
             final JsonRepresentation argRepr = arguments.getRepresentation(paramId);
             if (argRepr == null && !param.isOptional()) {
-                String reason = String.format("Action '%s', no argument found for (mandatory) parameter '%s'", action.getId(), paramId);
+                String reason = String.format("No argument found for (mandatory) parameter '%s'", paramId);
                 arguments.mapPut("x-ro-invalidReason", reason);
-                throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.VALIDATION_FAILED, reason);
+                throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.BAD_REQUEST, arguments, reason);
             }
             argList.add(argRepr);
         }

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_idempotent_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_idempotent_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_idempotent_fail_method_not_allowed.java
new file mode 100644
index 0000000..03caba4
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_idempotent_fail_method_not_allowed.java
@@ -0,0 +1,5 @@
+package org.apache.isis.viewer.restfulobjects.tck.domainobject.oid.action.invoke;
+
+public class ZzzTodo_idempotent_fail_method_not_allowed {
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_nonidempotent_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_nonidempotent_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_nonidempotent_fail_method_not_allowed.java
new file mode 100644
index 0000000..53cfcb5
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_nonidempotent_fail_method_not_allowed.java
@@ -0,0 +1,5 @@
+package org.apache.isis.viewer.restfulobjects.tck.domainobject.oid.action.invoke;
+
+public class ZzzTodo_nonidempotent_fail_method_not_allowed {
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_safe_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_safe_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_safe_fail_method_not_allowed.java
new file mode 100644
index 0000000..f062ecd
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/action/invoke/ZzzTodo_safe_fail_method_not_allowed.java
@@ -0,0 +1,5 @@
+package org.apache.isis.viewer.restfulobjects.tck.domainobject.oid.action.invoke;
+
+public class ZzzTodo_safe_fail_method_not_allowed {
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_addTo_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_addTo_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_addTo_fail_method_not_allowed.java
new file mode 100644
index 0000000..6b4f1e9
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_addTo_fail_method_not_allowed.java
@@ -0,0 +1,5 @@
+package org.apache.isis.viewer.restfulobjects.tck.domainobject.oid.collection;
+
+public class ZzzTodo_addTo_fail_method_not_allowed {
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_details_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_details_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_details_fail_method_not_allowed.java
new file mode 100644
index 0000000..b3b0420
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_details_fail_method_not_allowed.java
@@ -0,0 +1,5 @@
+package org.apache.isis.viewer.restfulobjects.tck.domainobject.oid.collection;
+
+public class ZzzTodo_details_fail_method_not_allowed {
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_removeFrom_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_removeFrom_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_removeFrom_fail_method_not_allowed.java
new file mode 100644
index 0000000..adcba23
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/collection/ZzzTodo_removeFrom_fail_method_not_allowed.java
@@ -0,0 +1,5 @@
+package org.apache.isis.viewer.restfulobjects.tck.domainobject.oid.collection;
+
+public class ZzzTodo_removeFrom_fail_method_not_allowed {
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_clear_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_clear_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_clear_fail_method_not_allowed.java
new file mode 100644
index 0000000..20dd6dd
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_clear_fail_method_not_allowed.java
@@ -0,0 +1,5 @@
+package org.apache.isis.viewer.restfulobjects.tck.domainobject.oid.property;
+
+public class ZzzTodo_clear_fail_method_not_allowed {
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_details_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_details_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_details_fail_method_not_allowed.java
new file mode 100644
index 0000000..5235bbc
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_details_fail_method_not_allowed.java
@@ -0,0 +1,5 @@
+package org.apache.isis.viewer.restfulobjects.tck.domainobject.oid.property;
+
+public class ZzzTodo_details_fail_method_not_allowed {
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_modify_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_modify_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_modify_fail_method_not_allowed.java
new file mode 100644
index 0000000..b001fac
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainobject/oid/property/ZzzTodo_modify_fail_method_not_allowed.java
@@ -0,0 +1,5 @@
+package org.apache.isis.viewer.restfulobjects.tck.domainobject.oid.property;
+
+public class ZzzTodo_modify_fail_method_not_allowed {
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_forbidden.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_forbidden.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_forbidden.java
new file mode 100644
index 0000000..e80690a
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_forbidden.java
@@ -0,0 +1,122 @@
+/*
+ *  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.codehaus.jackson.JsonParseException;
+import org.codehaus.jackson.map.JsonMappingException;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+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.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.DomainServiceResource;
+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;
+
+public class DomainServiceTest_forbidden {
+
+    @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, when
+        final JsonRepresentation givenAction = Util.givenAction(client, "BusinessRulesEntities", "visibleButNotInvocableAction");
+        final ObjectActionRepresentation actionRepr = givenAction.as(ObjectActionRepresentation.class);
+        
+        // then
+        final String disabledReason = actionRepr.getDisabledReason();
+        assertThat(disabledReason, is("Always disabled"));
+        
+
+        final LinkRepresentation invokeLink = new LinkRepresentation()
+            .withRel(Rel.INVOKE)
+            .withHref("http://localhost:39393/services/BusinessRulesEntities/actions/visibleButNotInvocableAction/invoke");
+        
+        // when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args = JsonRepresentation.newMap();
+        args.mapPut("id.value", 123);
+
+        final RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
+        
+        // then
+        thenResponseIsErrorWithInvalidReason(restfulResponse, disabledReason);
+    }
+
+
+    @Test
+    public void usingResourceProxy() throws Exception {
+
+        // given, when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args.mapPut("id.value", 123);
+
+        Response response = serviceResource.invokeActionQueryOnly("BusinessRulesEntities", "visibleButNotInvocableAction", UrlEncodingUtils.urlEncode(args));
+        RestfulResponse<ActionResultRepresentation> restfulResponse = RestfulResponse.ofT(response);
+        
+        // then
+        thenResponseIsErrorWithInvalidReason(restfulResponse, "Always disabled");
+    }
+
+    private static void thenResponseIsErrorWithInvalidReason(final RestfulResponse<ActionResultRepresentation> restfulResponse, String disabledReason) throws JsonParseException, JsonMappingException, IOException {
+        assertThat(restfulResponse, hasStatus(HttpStatusCode.FORBIDDEN));
+        assertThat(restfulResponse.getHeader(Header.WARNING), is(disabledReason));
+
+        // 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));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/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
index 6196072..383a075 100644
--- 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
@@ -20,7 +20,6 @@ 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.isLink;
-import static org.hamcrest.CoreMatchers.*;
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertThat;
 
@@ -29,31 +28,26 @@ import java.io.IOException;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
+import org.codehaus.jackson.JsonParseException;
+import org.codehaus.jackson.map.JsonMappingException;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
 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 {
 

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/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
index 2116f54..81af0c1 100644
--- 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
@@ -21,7 +21,6 @@ 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.apache.isis.viewer.restfulobjects.tck.RestfulMatchers.isLink;
-import static org.hamcrest.CoreMatchers.*;
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertThat;
 
@@ -30,31 +29,28 @@ import java.io.IOException;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
+import org.codehaus.jackson.JsonParseException;
+import org.codehaus.jackson.map.JsonMappingException;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
 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 {
 
@@ -93,8 +89,10 @@ public class DomainServiceTest_req_safe_simplearg_fail_mandatory_missing {
         assertThat(args, RestfulMatchers.mapHas("to"));
         
         // when
-        args.mapPut("from.value", (Integer)null);
+        args = JsonRepresentation.newMap();
+        // nothing for 'from'
         args.mapPut("to.value", 0);
+        assertThat(args.size(), is(1));
 
         final RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
         
@@ -102,13 +100,13 @@ public class DomainServiceTest_req_safe_simplearg_fail_mandatory_missing {
         thenResponseIsErrorWithInvalidReason(restfulResponse);
     }
 
-    @Ignore("to write")
+
     @Test
     public void usingResourceProxy() throws Exception {
 
         // given, when
         JsonRepresentation args = JsonRepresentation.newMap();
-        args.mapPut("from.value", 1);
+        // nothing for 'from'
         args.mapPut("to.value", 0);
         Response response = serviceResource.invokeActionQueryOnly("ActionsEntities", "subList", UrlEncodingUtils.urlEncode(args));
         RestfulResponse<ActionResultRepresentation> restfulResponse = RestfulResponse.ofT(response);
@@ -118,8 +116,8 @@ public class DomainServiceTest_req_safe_simplearg_fail_mandatory_missing {
     }
 
     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"));
+        assertThat(restfulResponse, hasStatus(HttpStatusCode.BAD_REQUEST));
+        assertThat(restfulResponse.getHeader(Header.WARNING), is("No argument found for (mandatory) parameter 'from'"));
 
         // 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));
@@ -127,9 +125,7 @@ public class DomainServiceTest_req_safe_simplearg_fail_mandatory_missing {
         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")); 
+        assertThat(repr.getString("x-ro-invalidReason"), is("No argument found for (mandatory) parameter 'from'")); 
     }
 
 

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_simplearg_fail_mandatory_value_null.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_value_null.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_value_null.java
new file mode 100644
index 0000000..5408be2
--- /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_value_null.java
@@ -0,0 +1,133 @@
+/*
+ *  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.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.codehaus.jackson.JsonParseException;
+import org.codehaus.jackson.map.JsonMappingException;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+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.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.DomainServiceResource;
+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;
+
+public class DomainServiceTest_req_safe_simplearg_fail_mandatory_value_null {
+
+    @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);
+    }
+
+
+    @Test
+    public void usingResourceProxy() throws Exception {
+
+        // given, when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args.mapPut("from.value", (Integer)null);
+        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/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_simplearg_fail_surplus_arg.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_surplus_arg.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/DomainServiceTest_req_safe_simplearg_fail_surplus_arg.java
new file mode 100644
index 0000000..a4798a8
--- /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_surplus_arg.java
@@ -0,0 +1,135 @@
+/*
+ *  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.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.codehaus.jackson.JsonParseException;
+import org.codehaus.jackson.map.JsonMappingException;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+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.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.DomainServiceResource;
+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;
+
+public class DomainServiceTest_req_safe_simplearg_fail_surplus_arg {
+
+    @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 = JsonRepresentation.newMap();
+        args.mapPut("from.value", 0);
+        args.mapPut("to.value", 1);
+        args.mapPut("nonExistent.value", 2);
+        assertThat(args.size(), is(3));
+
+        final RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
+        
+        // then
+        thenResponseIsErrorWithInvalidReason(restfulResponse);
+    }
+
+
+    @Test
+    public void usingResourceProxy() throws Exception {
+
+        // given, when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args.mapPut("from.value", 0);
+        args.mapPut("to.value", 1);
+        args.mapPut("nonExistent.value", 2);
+        assertThat(args.size(), is(3));
+
+        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.BAD_REQUEST));
+        assertThat(restfulResponse.getHeader(Header.WARNING), is("Argument 'nonExistent' found but no such parameter"));
+
+        // 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("x-ro-invalidReason"), is("Argument 'nonExistent' found but no such parameter")); 
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_idempotent_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_idempotent_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_idempotent_fail_method_not_allowed.java
new file mode 100644
index 0000000..ecc445e
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_idempotent_fail_method_not_allowed.java
@@ -0,0 +1,112 @@
+/*
+ *  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.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.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;
+
+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.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.DomainServiceResource;
+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.Util;
+
+public class ZzzToDo_DomainServiceTest_req_idempotent_fail_method_not_allowed {
+
+    @Rule
+    public IsisWebServerRule webServerRule = new IsisWebServerRule();
+
+    private RestfulClient client;
+
+    private DomainServiceResource serviceResource;
+
+    @Before
+    public void setUp() throws Exception {
+        client = webServerRule.getClient();
+
+        serviceResource = client.getDomainServiceResource();
+    }
+    
+    @Ignore("to write - copied from req_safe")
+    @Test
+    public void usingClientFollow() throws Exception {
+
+        // given, when
+        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());
+
+        invokeLink.withMethod(RestfulHttpMethod.POST);
+        
+        // when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args = JsonRepresentation.newMap();
+        args.mapPut("id.value", 123);
+
+        final RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
+        
+        // then
+        thenResponseIsErrorWithInvalidReason(restfulResponse);
+    }
+
+    
+    // not possible to test using resourceProxy
+
+
+    private static void thenResponseIsErrorWithInvalidReason(final RestfulResponse<ActionResultRepresentation> restfulResponse) throws JsonParseException, JsonMappingException, IOException {
+        assertThat(restfulResponse, hasStatus(HttpStatusCode.METHOD_NOT_ALLOWED));
+        assertThat(restfulResponse.getHeader(Header.WARNING), is("object is immutable")); // not a good message, but as per spec
+
+        // 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));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_nonidempotent_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_nonidempotent_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_nonidempotent_fail_method_not_allowed.java
new file mode 100644
index 0000000..957d767
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_nonidempotent_fail_method_not_allowed.java
@@ -0,0 +1,112 @@
+/*
+ *  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.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.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;
+
+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.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.DomainServiceResource;
+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.Util;
+
+public class ZzzToDo_DomainServiceTest_req_nonidempotent_fail_method_not_allowed {
+
+    @Rule
+    public IsisWebServerRule webServerRule = new IsisWebServerRule();
+
+    private RestfulClient client;
+
+    private DomainServiceResource serviceResource;
+
+    @Before
+    public void setUp() throws Exception {
+        client = webServerRule.getClient();
+
+        serviceResource = client.getDomainServiceResource();
+    }
+    
+    @Ignore("to write - copied from req_safe")
+    @Test
+    public void usingClientFollow() throws Exception {
+
+        // given, when
+        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());
+
+        invokeLink.withMethod(RestfulHttpMethod.POST);
+        
+        // when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args = JsonRepresentation.newMap();
+        args.mapPut("id.value", 123);
+
+        final RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
+        
+        // then
+        thenResponseIsErrorWithInvalidReason(restfulResponse);
+    }
+
+    
+    // not possible to test using resourceProxy
+
+
+    private static void thenResponseIsErrorWithInvalidReason(final RestfulResponse<ActionResultRepresentation> restfulResponse) throws JsonParseException, JsonMappingException, IOException {
+        assertThat(restfulResponse, hasStatus(HttpStatusCode.METHOD_NOT_ALLOWED));
+        assertThat(restfulResponse.getHeader(Header.WARNING), is("object is immutable")); // not a good message, but as per spec
+
+        // 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));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/8210a58d/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_safe_fail_method_not_allowed.java
----------------------------------------------------------------------
diff --git a/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_safe_fail_method_not_allowed.java b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_safe_fail_method_not_allowed.java
new file mode 100644
index 0000000..b7eec2a
--- /dev/null
+++ b/component/viewer/restfulobjects/tck/src/test/java/org/apache/isis/viewer/restfulobjects/tck/domainservice/serviceId/action/invoke/ZzzToDo_DomainServiceTest_req_safe_fail_method_not_allowed.java
@@ -0,0 +1,112 @@
+/*
+ *  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.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.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;
+
+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.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.DomainServiceResource;
+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.Util;
+
+public class ZzzToDo_DomainServiceTest_req_safe_fail_method_not_allowed {
+
+    @Rule
+    public IsisWebServerRule webServerRule = new IsisWebServerRule();
+
+    private RestfulClient client;
+
+    private DomainServiceResource serviceResource;
+
+    @Before
+    public void setUp() throws Exception {
+        client = webServerRule.getClient();
+
+        serviceResource = client.getDomainServiceResource();
+    }
+    
+    @Ignore("currently failing")
+    @Test
+    public void usingClientFollow() throws Exception {
+
+        // given, when
+        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());
+
+        invokeLink.withMethod(RestfulHttpMethod.POST);
+        
+        // when
+        JsonRepresentation args = JsonRepresentation.newMap();
+        args = JsonRepresentation.newMap();
+        args.mapPut("id.value", 123);
+
+        final RestfulResponse<ActionResultRepresentation> restfulResponse = client.followT(invokeLink, args);
+        
+        // then
+        thenResponseIsErrorWithInvalidReason(restfulResponse);
+    }
+
+    
+    // not possible to test using resourceProxy
+
+
+    private static void thenResponseIsErrorWithInvalidReason(final RestfulResponse<ActionResultRepresentation> restfulResponse) throws JsonParseException, JsonMappingException, IOException {
+        assertThat(restfulResponse, hasStatus(HttpStatusCode.METHOD_NOT_ALLOWED));
+        assertThat(restfulResponse.getHeader(Header.WARNING), is("object is immutable")); // not a good message, but as per spec
+
+        // 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));
+    }
+
+}