http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.java
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.java
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.java
new file mode 100644
index 0000000..7c528ac
--- /dev/null
+++ 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.java
@@ -0,0 +1,140 @@
+#set( $symbol_pound = '#' )
+#set( $symbol_dollar = '$' )
+#set( $symbol_escape = '\' )
+/*
+ *  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 app;
+
+import app.ToDoItemAnalysis.DateRange;
+import dom.todo.ToDoItem;
+import dom.todo.ToDoItems;
+
+import java.util.List;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import org.joda.time.DateTime;
+import org.apache.isis.applib.annotation.BookmarkPolicy;
+import org.apache.isis.applib.annotation.CollectionLayout;
+import org.apache.isis.applib.annotation.DomainObject;
+import org.apache.isis.applib.annotation.DomainObjectLayout;
+import org.apache.isis.applib.annotation.Nature;
+import org.apache.isis.applib.annotation.RenderType;
+import org.apache.isis.applib.annotation.Title;
+import org.apache.isis.applib.services.clock.ClockService;
+import org.apache.isis.applib.util.ObjectContracts;
+
+@DomainObjectLayout(
+        named="By Date Range",
+        bookmarking = BookmarkPolicy.AS_ROOT
+)
+@DomainObject(
+        nature = Nature.VIEW_MODEL
+)
+public class ToDoItemsByDateRangeViewModel
+        implements Comparable<ToDoItemsByDateRangeViewModel> {
+
+    //region > constructors
+    public ToDoItemsByDateRangeViewModel() {
+    }
+    public ToDoItemsByDateRangeViewModel(final DateRange dateRange) {
+        setDateRange(dateRange);
+    }
+    //endregion
+
+    //region > dateRange (property)
+    private DateRange dateRange;
+
+    @Title
+    public DateRange getDateRange() {
+        return dateRange;
+    }
+
+    public void setDateRange(final DateRange dateRange) {
+        this.dateRange = dateRange;
+    }
+    //endregion
+
+    //region > count (derived property)
+    public int getCount() {
+        return getItemsNotYetComplete().size();
+    }
+    //endregion
+
+    //region > getItemsNotYetComplete (collection)
+    /**
+     * All those items {@link ToDoItems${symbol_pound}notYetComplete() not yet 
complete}, for this {@link ${symbol_pound}getDateRange() date range}.
+     */
+    @CollectionLayout(
+            render = RenderType.EAGERLY
+    )
+    public List<ToDoItem> getItemsNotYetComplete() {
+        final List<ToDoItem> notYetComplete = toDoItems.notYetCompleteNoUi();
+        return Lists.newArrayList(Iterables.filter(notYetComplete, 
thoseInDateRange()));
+    }
+
+    private Predicate<ToDoItem> thoseInDateRange() {
+        return new Predicate<ToDoItem>() {
+            @Override
+            public boolean apply(final ToDoItem t) {
+                return Objects.equal(dateRangeFor(t), getDateRange());
+            }
+        };
+    }
+    
+    private DateRange dateRangeFor(final ToDoItem t) {
+        if(t.getDueBy() == null) {
+            return DateRange.Unknown;
+        }
+        final DateTime dueBy = t.getDueBy().toDateTimeAtStartOfDay();
+        final DateTime today = clockService.now().toDateTimeAtStartOfDay();
+        
+        if(dueBy.isBefore(today)) {
+            return DateRange.OverDue;
+        }
+        if(dueBy.isBefore(today.plusDays(1))) {
+            return DateRange.Today;
+        }
+        if(dueBy.isBefore(today.plusDays(2))) {
+            return DateRange.Tomorrow;
+        }
+        if(dueBy.isBefore(today.plusDays(7))) {
+            return DateRange.ThisWeek;
+        }
+        return DateRange.Later;
+    }
+    //endregion
+
+    //region > compareTo
+    @Override
+    public int compareTo(final ToDoItemsByDateRangeViewModel other) {
+        return ObjectContracts.compare(this, other, "dateRange");
+    }
+    //endregion
+
+    //region > injected services
+    @javax.inject.Inject
+    private ToDoItems toDoItems;
+
+    @javax.inject.Inject
+    private ClockService clockService;
+    //endregion
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.layout.json
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.layout.json
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.layout.json
new file mode 100644
index 0000000..648e146
--- /dev/null
+++ 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.layout.json
@@ -0,0 +1,33 @@
+{
+  "columns": [
+    {
+      "span": 4,
+      "memberGroups": {
+        "General": {
+          "members": {
+            "dateRange": {},
+            "count": {}
+          }
+        }
+      }
+    },
+    {
+      "span": 0,
+      "memberGroups": {}
+    },
+    {
+      "span": 0,
+      "memberGroups": {}
+    },
+    {
+      "span": 8,
+      "collections": {
+        "itemsNotYetComplete": {}
+      }
+    }
+  ],
+  "actions": {
+    "downloadLayout": {},
+    "refreshLayout": {}
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.png
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.png
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.png
new file mode 100644
index 0000000..034a1f9
Binary files /dev/null and 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/app/ToDoItemsByDateRangeViewModel.png
 differ

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/Categorized.java
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/Categorized.java
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/Categorized.java
new file mode 100644
index 0000000..9bb6bad
--- /dev/null
+++ 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/Categorized.java
@@ -0,0 +1,35 @@
+#set( $symbol_pound = '#' )
+#set( $symbol_dollar = '$' )
+#set( $symbol_escape = '\' )
+/*
+ *  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 dom.todo;
+
+/**
+ * Enables {@link 
dom.todo.ToDoItemContributions${symbol_pound}updateCategory(dom.todo.Categorized,
 dom.todo.ToDoItem.Category, dom.todo.ToDoItem.Subcategory)} to be contributed 
to
+ * both the {@link dom.todo.ToDoItem} (entity) and also the {@link 
app.ToDoItemWizard} (wizard).
+ */
+public interface Categorized {
+
+    ToDoItem.Category getCategory();
+    void setCategory(ToDoItem.Category category);
+
+    ToDoItem.Subcategory getSubcategory();
+    void setSubcategory(ToDoItem.Subcategory subcategory);
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem-done.png
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem-done.png
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem-done.png
new file mode 100644
index 0000000..b0fc6e8
Binary files /dev/null and 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem-done.png
 differ

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem-todo.png
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem-todo.png
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem-todo.png
new file mode 100644
index 0000000..99a9fed
Binary files /dev/null and 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem-todo.png
 differ

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.java
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.java
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.java
new file mode 100644
index 0000000..cadc117
--- /dev/null
+++ 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.java
@@ -0,0 +1,982 @@
+#set( $symbol_pound = '#' )
+#set( $symbol_dollar = '$' )
+#set( $symbol_escape = '\' )
+/*
+ *  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 dom.todo;
+
+import java.math.BigDecimal;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import javax.jdo.JDOHelper;
+import javax.jdo.annotations.IdentityType;
+import javax.jdo.annotations.VersionStrategy;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Ordering;
+import org.joda.time.LocalDate;
+import org.apache.isis.applib.DomainObjectContainer;
+import org.apache.isis.applib.Identifier;
+import org.apache.isis.applib.NonRecoverableException;
+import org.apache.isis.applib.RecoverableException;
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.BookmarkPolicy;
+import org.apache.isis.applib.annotation.Collection;
+import org.apache.isis.applib.annotation.CollectionLayout;
+import org.apache.isis.applib.annotation.DomainObject;
+import org.apache.isis.applib.annotation.DomainObjectLayout;
+import org.apache.isis.applib.annotation.Editing;
+import org.apache.isis.applib.annotation.RestrictTo;
+import org.apache.isis.applib.annotation.InvokeOn;
+import org.apache.isis.applib.annotation.InvokedOn;
+import org.apache.isis.applib.annotation.Optionality;
+import org.apache.isis.applib.annotation.Parameter;
+import org.apache.isis.applib.annotation.ParameterLayout;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.annotation.Property;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.applib.security.UserMemento;
+import org.apache.isis.applib.services.actinvoc.ActionInvocationContext;
+import org.apache.isis.applib.services.eventbus.ActionDomainEvent;
+import org.apache.isis.applib.services.eventbus.EventBusService;
+import org.apache.isis.applib.services.scratchpad.Scratchpad;
+import org.apache.isis.applib.services.wrapper.HiddenException;
+import org.apache.isis.applib.services.wrapper.WrapperFactory;
+import org.apache.isis.applib.util.ObjectContracts;
+import org.apache.isis.applib.util.TitleBuffer;
+import org.apache.isis.applib.value.Blob;
+import org.apache.isis.applib.value.Clob;
+
[email protected](identityType=IdentityType.DATASTORE)
[email protected](
+        strategy=javax.jdo.annotations.IdGeneratorStrategy.IDENTITY,
+         column="id")
[email protected](
+        strategy=VersionStrategy.VERSION_NUMBER, 
+        column="version")
[email protected]({
+    @javax.jdo.annotations.Unique(
+            name="ToDoItem_description_must_be_unique", 
+            members={"ownedBy","description"})
+})
[email protected]( {
+    @javax.jdo.annotations.Query(
+            name = "findByOwnedBy", language = "JDOQL",
+            value = "SELECT "
+                    + "FROM dom.todo.ToDoItem "
+                    + "WHERE ownedBy == :ownedBy"),
+    @javax.jdo.annotations.Query(
+            name = "findByOwnedByAndCompleteIsFalse", language = "JDOQL",
+            value = "SELECT "
+                    + "FROM dom.todo.ToDoItem "
+                    + "WHERE ownedBy == :ownedBy "
+                    + "   && complete == false"),
+    @javax.jdo.annotations.Query(
+            name = "findByOwnedByAndCompleteIsTrue", language = "JDOQL",
+            value = "SELECT "
+                    + "FROM dom.todo.ToDoItem "
+                    + "WHERE ownedBy == :ownedBy "
+                    + "&& complete == true"),
+    @javax.jdo.annotations.Query(
+            name = "findByOwnedByAndCategory", language = "JDOQL",
+            value = "SELECT "
+                    + "FROM dom.todo.ToDoItem "
+                    + "WHERE ownedBy == :ownedBy "
+                    + "&& category == :category"),
+    @javax.jdo.annotations.Query(
+            name = "findByOwnedByAndDescriptionContains", language = "JDOQL",
+            value = "SELECT "
+                    + "FROM dom.todo.ToDoItem "
+                    + "WHERE ownedBy == :ownedBy && "
+                    + "description.indexOf(:description) >= 0")
+})
+@DomainObject(
+        autoCompleteRepository = ToDoItems.class, // for drop-downs, unless 
autoCompleteNXxx() is present
+        autoCompleteAction = "autoComplete",
+        // bounded = true,  // for drop-downs if only a small number of 
instances only (overrides autoComplete)
+        objectType = "TODO"
+)
+@DomainObjectLayout(
+        bookmarking = BookmarkPolicy.AS_ROOT
+)
+public class ToDoItem implements Categorized, Comparable<ToDoItem> {
+
+    //region > LOG
+    /**
+     * It isn't common for entities to log, but they can if required.  
+     * Isis uses slf4j API internally (with log4j as implementation), and is 
the recommended API to use. 
+     */
+    private final static org.slf4j.Logger LOG = 
org.slf4j.LoggerFactory.getLogger(ToDoItem.class);
+    //endregion
+
+    // region > title, icon
+    public String title() {
+        final TitleBuffer buf = new TitleBuffer();
+        buf.append(getDescription());
+        if (isComplete()) {
+            buf.append("- Completed!");
+        } else {
+            try {
+                final LocalDate dueBy = wrapperFactory.wrap(this).getDueBy();
+                if (dueBy != null) {
+                    buf.append(" due by", dueBy);
+                }
+            } catch(final HiddenException ignored) {
+            }
+        }
+        return buf.toString();
+    }
+    
+    public String iconName() {
+        return !isComplete() ? "todo" : "done";
+    }
+
+    public String cssClass() { return iconName(); }
+
+    //endregion
+
+    //region > description (property)
+    private String description;
+
+    @javax.jdo.annotations.Column(allowsNull="false", length=100)
+    @Property(
+        regexPattern = 
"${symbol_escape}${symbol_escape}w[@&:${symbol_escape}${symbol_escape}-${symbol_escape}${symbol_escape},${symbol_escape}${symbol_escape}.${symbol_escape}${symbol_escape}+
 ${symbol_escape}${symbol_escape}w]*"
+    )
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(final String description) {
+        this.description = description;
+    }
+    public void modifyDescription(final String description) {
+        setDescription(description);
+    }
+    public void clearDescription() {
+        setDescription(null);
+    }
+    //endregion
+
+    //region > dueBy (property)
+    @javax.jdo.annotations.Persistent(defaultFetchGroup="true")
+    private LocalDate dueBy;
+
+    @javax.jdo.annotations.Column(allowsNull="true")
+    public LocalDate getDueBy() {
+        return dueBy;
+    }
+
+    /**
+     * Demonstrates how to perform security checks within the domain code.
+     *
+     * <p>
+     *     Generally speaking this approach is not recommended; such checks 
should
+     *     wherever possible be externalized in the security subsystem.
+     * </p>
+     */
+    public boolean hideDueBy() {
+        final UserMemento user = container.getUser();
+        return user.hasRole("realm1:noDueBy_role");
+    }
+
+    public void setDueBy(final LocalDate dueBy) {
+        this.dueBy = dueBy;
+    }
+    public void clearDueBy() {
+        setDueBy(null);
+    }
+    // proposed new value is validated before setting
+    public String validateDueBy(final LocalDate dueBy) {
+        if (dueBy == null) {
+            return null;
+        }
+        return toDoItems.validateDueBy(dueBy);
+    }
+    //endregion
+
+    //region > category and subcategory (property)
+
+    public static enum Category {
+        Professional {
+            @Override
+            public List<Subcategory> subcategories() {
+                return Arrays.asList(null, Subcategory.OpenSource, 
Subcategory.Consulting, Subcategory.Education, Subcategory.Marketing);
+            }
+        }, Domestic {
+            @Override
+            public List<Subcategory> subcategories() {
+                return Arrays.asList(null, Subcategory.Shopping, 
Subcategory.Housework, Subcategory.Garden, Subcategory.Chores);
+            }
+        }, Other {
+            @Override
+            public List<Subcategory> subcategories() {
+                return Arrays.asList(null, Subcategory.Other);
+            }
+        };
+        
+        public abstract List<Subcategory> subcategories();
+    }
+
+    public static enum Subcategory {
+        // professional
+        OpenSource, Consulting, Education, Marketing,
+        // domestic
+        Shopping, Housework, Garden, Chores,
+        // other
+        Other;
+
+        public static List<Subcategory> listFor(final Category category) {
+            return category != null? category.subcategories(): 
Collections.<Subcategory>emptyList();
+        }
+
+        static String validate(final Category category, final Subcategory 
subcategory) {
+            if(category == null) {
+                return "Enter category first";
+            }
+            return !category.subcategories().contains(subcategory) 
+                    ? "Invalid subcategory for category '" + category + "'" 
+                    : null;
+        }
+        
+        public static Predicate<Subcategory> thoseFor(final Category category) 
{
+            return new Predicate<Subcategory>() {
+
+                @Override
+                public boolean apply(final Subcategory subcategory) {
+                    return category.subcategories().contains(subcategory);
+                }
+            };
+        }
+    }
+
+    // //////////////////////////////////////
+
+    private Category category;
+
+    @javax.jdo.annotations.Column(allowsNull="false")
+    @Property(
+            editing = Editing.DISABLED,
+            editingDisabledReason = "Use action to update both category and 
subcategory"
+    )
+    public Category getCategory() {
+        return category;
+    }
+
+    public void setCategory(final Category category) {
+        this.category = category;
+    }
+
+    // //////////////////////////////////////
+
+    private Subcategory subcategory;
+
+    @javax.jdo.annotations.Column(allowsNull="true")
+    @Property(
+            editing = Editing.DISABLED,
+            editingDisabledReason = "Use action to update both category and 
subcategory"
+    )
+    public Subcategory getSubcategory() {
+        return subcategory;
+    }
+    public void setSubcategory(final Subcategory subcategory) {
+        this.subcategory = subcategory;
+    }
+    //endregion
+
+    //region > ownedBy (property)
+
+    private String ownedBy;
+
+    @javax.jdo.annotations.Column(allowsNull="false")
+    public String getOwnedBy() {
+        return ownedBy;
+    }
+
+    public void setOwnedBy(final String ownedBy) {
+        this.ownedBy = ownedBy;
+    }
+    //endregion
+
+    //region > complete (property), completed (action), notYetCompleted 
(action)
+
+    private boolean complete;
+
+    @Property(
+        editing = Editing.DISABLED
+    )
+    public boolean isComplete() {
+        return complete;
+    }
+
+    public void setComplete(final boolean complete) {
+        this.complete = complete;
+    }
+
+    @Action(
+            domainEvent =CompletedEvent.class,
+            invokeOn = InvokeOn.OBJECT_AND_COLLECTION
+    )
+    public ToDoItem completed() {
+        setComplete(true);
+        
+        //
+        // remainder of method just demonstrates the use of the 
Bulk.InteractionContext service 
+        //
+        @SuppressWarnings("unused")
+        final List<Object> allObjects = 
actionInvocationContext.getDomainObjects();
+        
+        LOG.debug("completed: "
+                + actionInvocationContext.getIndex() +
+                " [" + actionInvocationContext.getSize() + "]"
+                + (actionInvocationContext.isFirst() ? " (first)" : "")
+                + (actionInvocationContext.isLast() ? " (last)" : ""));
+
+        // if invoked as a regular action, return this object;
+        // otherwise (if invoked as bulk), return null (so go back to the list)
+        return actionInvocationContext.getInvokedOn() == InvokedOn.OBJECT? 
this: null;
+    }
+    // disable action dependent on state of object
+    public String disableCompleted() {
+        return isComplete() ? "Already completed" : null;
+    }
+
+    @Action(
+        invokeOn = InvokeOn.OBJECT_AND_COLLECTION
+    )
+    public ToDoItem notYetCompleted() {
+        setComplete(false);
+
+        // if invoked as a regular action, return this object;
+        // otherwise (if invoked as bulk), return null (so go back to the list)
+        return actionInvocationContext.getInvokedOn() == InvokedOn.OBJECT ? 
this: null;
+    }
+    // disable action dependent on state of object
+    public String disableNotYetCompleted() {
+        return !complete ? "Not yet completed" : null;
+    }
+    //endregion
+
+    //region > completeSlowly (property)
+    // 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    @Action(
+            hidden = Where.EVERYWHERE
+    )
+    public void completeSlowly(final int millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (final InterruptedException ignored) {
+        }
+        setComplete(true);
+    }
+    //endregion
+
+    //region > cost (property), updateCost (action)
+    private BigDecimal cost;
+
+    @javax.jdo.annotations.Column(allowsNull="true", scale=2)
+    @javax.validation.constraints.Digits(integer=10, fraction=2)
+    @Property(
+            editing = Editing.DISABLED,
+            editingDisabledReason = "Update using action"
+    )
+    public BigDecimal getCost() {
+        return cost;
+    }
+
+    public void setCost(final BigDecimal cost) {
+        this.cost = cost!=null?cost.setScale(2, 
BigDecimal.ROUND_HALF_EVEN):null;
+    }
+
+    @Action(
+            semantics = SemanticsOf.IDEMPOTENT
+    )
+    public ToDoItem updateCost(
+            @Parameter(optional = Optionality.TRUE)
+            @ParameterLayout(named = "New cost")
+            @javax.validation.constraints.Digits(integer=10, fraction=2)
+            final BigDecimal cost) {
+        LOG.debug("%s: cost updated: %s -> %s", container.titleOf(this), 
getCost(), cost);
+        
+        // just to simulate a long-running action
+        try {
+            Thread.sleep(3000);
+        } catch (final InterruptedException ignored) {
+        }
+        
+        setCost(cost);
+        return this;
+    }
+
+    // provide a default value for argument ${symbol_pound}0
+    public BigDecimal default0UpdateCost() {
+        return getCost();
+    }
+    
+    // validate action arguments
+    public String validateUpdateCost(final BigDecimal proposedCost) {
+        if(proposedCost == null) { return null; }
+        return proposedCost.compareTo(BigDecimal.ZERO) < 0? "Cost must be 
positive": null;
+    }
+    //endregion
+
+    //region > notes (property)
+    private String notes;
+
+    @javax.jdo.annotations.Column(allowsNull="true", length=400)
+    public String getNotes() {
+        return notes;
+    }
+
+    public void setNotes(final String notes) {
+        this.notes = notes;
+    }
+    //endregion
+
+    //region > attachment (property)
+    private Blob attachment;
+    @javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = {
+            @javax.jdo.annotations.Column(name = "attachment_name"),
+            @javax.jdo.annotations.Column(name = "attachment_mimetype"),
+            @javax.jdo.annotations.Column(name = "attachment_bytes", jdbcType 
= "BLOB", sqlType = "BLOB")
+    })
+    @Property(
+            optional = Optionality.TRUE
+    )
+    public Blob getAttachment() {
+        return attachment;
+    }
+
+    public void setAttachment(final Blob attachment) {
+        this.attachment = attachment;
+    }
+    //endregion
+
+    //region > doc (property)
+    private Clob doc;
+    @javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = {
+            @javax.jdo.annotations.Column(name = "doc_name"),
+            @javax.jdo.annotations.Column(name = "doc_mimetype"),
+            @javax.jdo.annotations.Column(name = "doc_chars", jdbcType = 
"CLOB", sqlType = "CLOB")
+    })
+    @Property(
+            optional = Optionality.TRUE
+    )
+    public Clob getDoc() {
+        return doc;
+    }
+
+    public void setDoc(final Clob doc) {
+        this.doc = doc;
+    }
+    //endregion
+
+    //region > version (derived property)
+    public Long getVersionSequence() {
+        if(!(this instanceof javax.jdo.spi.PersistenceCapable)) {
+            return null;
+        }
+        return (Long) JDOHelper.getVersion((javax.jdo.spi.PersistenceCapable) 
this);
+    }
+    // hide property (imperatively, based on state of object)
+    public boolean hideVersionSequence() {
+        return !(this instanceof javax.jdo.spi.PersistenceCapable);
+    }
+    //endregion
+
+    //region > dependencies (property), add (action), remove (action)
+
+    // overrides the natural ordering
+    public static class DependenciesComparator implements Comparator<ToDoItem> 
{
+        @Override
+        public int compare(final ToDoItem p, final ToDoItem q) {
+            final Ordering<ToDoItem> byDescription = new Ordering<ToDoItem>() {
+                public int compare(final ToDoItem p, final ToDoItem q) {
+                    return 
Ordering.natural().nullsFirst().compare(p.getDescription(), q.getDescription());
+                }
+            };
+            return byDescription
+                    .compound(Ordering.<ToDoItem>natural())
+                    .compare(p, q);
+        }
+    }
+
+    @javax.jdo.annotations.Persistent(table="ToDoItemDependencies")
+    @javax.jdo.annotations.Join(column="dependingId")
+    @javax.jdo.annotations.Element(column="dependentId")
+
+    private Set<ToDoItem> dependencies = new TreeSet<>();
+    //private SortedSet<ToDoItem> dependencies = new TreeSet<>();  // not 
compatible with neo4j (as of DN v3.2.3)
+
+    @Collection()
+    @CollectionLayout(/*sortedBy = DependenciesComparator.class*/) // not 
compatible with neo4j (as of DN v3.2.3)
+    public Set<ToDoItem> getDependencies() {
+        return dependencies;
+    }
+
+    public void setDependencies(final Set<ToDoItem> dependencies) {
+        this.dependencies = dependencies;
+    }
+
+    public void addToDependencies(final ToDoItem toDoItem) {
+        getDependencies().add(toDoItem);
+    }
+    public void removeFromDependencies(final ToDoItem toDoItem) {
+        getDependencies().remove(toDoItem);
+    }
+
+    public ToDoItem add(
+            @ParameterLayout(typicalLength = 20)
+            final ToDoItem toDoItem) {
+        // By wrapping the call, Isis will detect that the collection is 
modified
+        // and it will automatically send CollectionInteractionEvents to the 
Event Bus.
+        // ToDoItemSubscriptions is a demo subscriber to this event
+        wrapperFactory.wrapSkipRules(this).addToDependencies(toDoItem);
+        return this;
+    }
+    public List<ToDoItem> autoComplete0Add(
+            @Parameter(minLength = 2)
+            final String search) {
+        final List<ToDoItem> list = toDoItems.autoComplete(search);
+        list.removeAll(getDependencies());
+        list.remove(this);
+        return list;
+    }
+
+    public String disableAdd(final ToDoItem toDoItem) {
+        if(isComplete()) {
+            return "Cannot add dependencies for items that are complete";
+        }
+        return null;
+    }
+    // validate the provided argument prior to invoking action
+    public String validateAdd(final ToDoItem toDoItem) {
+        if(getDependencies().contains(toDoItem)) {
+            return "Already a dependency";
+        }
+        if(toDoItem == this) {
+            return "Can't set up a dependency to self";
+        }
+        return null;
+    }
+
+    public ToDoItem remove(
+            @ParameterLayout(typicalLength = 20)
+            final ToDoItem toDoItem) {
+        // By wrapping the call, Isis will detect that the collection is 
modified
+        // and it will automatically send a CollectionInteractionEvent to the 
Event Bus.
+        // ToDoItemSubscriptions is a demo subscriber to this event
+        wrapperFactory.wrapSkipRules(this).removeFromDependencies(toDoItem);
+        return this;
+    }
+    // disable action dependent on state of object
+    public String disableRemove(final ToDoItem toDoItem) {
+        if(isComplete()) {
+            return "Cannot remove dependencies for items that are complete";
+        }
+        return getDependencies().isEmpty()? "No dependencies to remove": null;
+    }
+    // validate the provided argument prior to invoking action
+    public String validateRemove(final ToDoItem toDoItem) {
+        if(!getDependencies().contains(toDoItem)) {
+            return "Not a dependency";
+        }
+        return null;
+    }
+    // provide a drop-down
+    public java.util.Collection<ToDoItem> choices0Remove() {
+        return getDependencies();
+    }
+    //endregion
+
+    //region > clone (action)
+
+    // the name of the action in the UI
+    // nb: method is not called "clone()" is inherited by java.lang.Object and
+    // (a) has different semantics and (b) is in any case automatically ignored
+    // by the framework
+    public ToDoItem duplicate(
+            @Parameter(regexPattern = 
"${symbol_escape}${symbol_escape}w[@&:${symbol_escape}${symbol_escape}-${symbol_escape}${symbol_escape},${symbol_escape}${symbol_escape}.${symbol_escape}${symbol_escape}+
 ${symbol_escape}${symbol_escape}w]*" )
+            @ParameterLayout(named="Description")
+            final String description,
+            @ParameterLayout(named="Category")
+            final Category category,
+            @ParameterLayout(named="Subcategory")
+            final Subcategory subcategory,
+            @Parameter(optional = Optionality.TRUE)
+            @ParameterLayout(named="Due by")
+            final LocalDate dueBy,
+            @Parameter(optional = Optionality.TRUE)
+            @ParameterLayout(named="Cost")
+            final BigDecimal cost) {
+        return toDoItems.newToDo(description, category, subcategory, dueBy, 
cost);
+    }
+    public String default0Duplicate() {
+        return getDescription() + " - Copy";
+    }
+    public Category default1Duplicate() {
+        return getCategory();
+    }
+    public Subcategory default2Duplicate() {
+        return getSubcategory();
+    }
+    public LocalDate default3Duplicate() {
+        return getDueBy();
+    }
+    public List<Subcategory> choices2Duplicate(
+            final String description, final Category category) {
+        return toDoItems.choices2NewToDo(description, category);
+    }
+    public String validateDuplicate(
+            final String description, 
+            final Category category, final Subcategory subcategory, 
+            final LocalDate dueBy, final BigDecimal cost) {
+        return toDoItems.validateNewToDo(description, category, subcategory, 
dueBy, cost);
+    }
+    //endregion
+
+    //region > delete (action)
+    @Action(
+            domainEvent =DeletedEvent.class,
+            invokeOn = InvokeOn.OBJECT_AND_COLLECTION
+    )
+    public List<ToDoItem> delete() {
+        
+        container.removeIfNotAlready(this);
+
+        container.informUser("Deleted " + container.titleOf(this));
+        
+        // invalid to return 'this' (cannot render a deleted object)
+        return toDoItems.notYetComplete();
+    }
+    //endregion
+
+    //region > totalCost (property)
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            invokeOn = InvokeOn.COLLECTION_ONLY
+    )
+    public BigDecimal totalCost() {
+        BigDecimal total = (BigDecimal) scratchpad.get("runningTotal");
+        if(getCost() != null) {
+            total = total != null ? total.add(getCost()) : getCost();
+            scratchpad.put("runningTotal", total);
+        }
+        return total.setScale(2, BigDecimal.ROUND_HALF_EVEN);
+    }
+    //endregion
+
+    //region > openSourceCodeOnGithub (action)
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            restrictTo = RestrictTo.PROTOTYPING
+    )
+    public URL openSourceCodeOnGithub() throws MalformedURLException {
+        return new 
URL("https://github.com/apache/isis/tree/master/example/application/${parentArtifactId}/dom/src/main/java/dom/todo/ToDoItem.java";);
+    }
+    //endregion
+
+    //region > demoException (action)
+
+    static enum DemoExceptionType {
+        RecoverableException,
+        RecoverableExceptionAutoEscalated,
+        NonRecoverableException;
+    }
+
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            restrictTo = RestrictTo.PROTOTYPING
+    )
+    public void demoException(
+            @ParameterLayout(named="Type")
+            final DemoExceptionType type) {
+        switch(type) {
+        case NonRecoverableException:
+            throw new NonRecoverableException("Demo throwing " + type.name());
+        case RecoverableException:
+            throw new RecoverableException("Demo throwing " + type.name());
+        case RecoverableExceptionAutoEscalated:
+            try {
+                // this will trigger an exception (because category cannot be 
null), causing the xactn to be aborted
+                setCategory(null);
+                container.flush();
+            } catch(Exception e) {
+                // it's a programming mistake to throw only a recoverable 
exception here, because of the xactn's state.
+                // the framework should instead auto-escalate this to a 
non-recoverable exception
+                throw new RecoverableException("Demo throwing " + type.name(), 
e);
+            }
+        }
+    }
+    //endregion
+
+    //region > lifecycle callbacks
+
+    public void created() {
+        LOG.debug("lifecycle callback: created: " + this.toString());
+    }
+    public void loaded() {
+        LOG.debug("lifecycle callback: loaded: " + this.toString());
+    }
+    public void persisting() {
+        LOG.debug("lifecycle callback: persisting: " + this.toString());
+    }
+    public void persisted() {
+        LOG.debug("lifecycle callback: persisted: " + this.toString());
+    }
+    public void updating() {
+        LOG.debug("lifecycle callback: updating: " + this.toString());
+    }
+    public void updated() {
+        LOG.debug("lifecycle callback: updated: " + this.toString());
+    }
+    public void removing() {
+        LOG.debug("lifecycle callback: removing: " + this.toString());
+    }
+    public void removed() {
+        LOG.debug("lifecycle callback: removed: " + this.toString());
+    }
+    //endregion
+
+    //region > object-level validation
+
+    /**
+     * Prevent user from viewing another user's data.
+     */
+    public boolean hidden() {
+        // uncomment to enable.  As things stand, the disabled() method below 
instead will make object "read-only".
+        //return !Objects.equal(getOwnedBy(), container.getUser().getName());
+        return false;
+    }
+
+    /**
+     * Prevent user from modifying any other user's data.
+     */
+    public String disabled(final Identifier.Type identifierType){
+        final UserMemento currentUser = container.getUser();
+        final String currentUserName = currentUser.getName();
+        if(Objects.equal(getOwnedBy(), currentUserName)) { return null; }
+        return "This object is owned by '" + getOwnedBy() + "' and cannot be 
modified by you";
+    }
+
+    /**
+     * In a real app, if this were actually a rule, then we'd expect that
+     * invoking the {@link ${symbol_pound}completed() done} action would clear 
the {@link ${symbol_pound}getDueBy() dueBy}
+     * property (rather than require the user to have to clear manually).
+     */
+    public String validate() {
+        if(isComplete() && getDueBy() != null) {
+            return "Due by date must be set to null if item has been 
completed";
+        }
+        return null;
+    }
+
+
+    //endregion
+
+
+    //region > programmatic helpers
+    @Programmatic // excluded from the framework's metamodel
+    public boolean isDue() {
+        if (getDueBy() == null) {
+            return false;
+        }
+        return !toDoItems.isMoreThanOneWeekInPast(getDueBy());
+    }
+    //endregion
+
+    //region > events
+
+    public static abstract class AbstractActionDomainEvent extends 
ActionDomainEvent<ToDoItem> {
+        private static final long serialVersionUID = 1L;
+        private final String description;
+        public AbstractActionDomainEvent(
+                final String description,
+                final ToDoItem source,
+                final Identifier identifier,
+                final Object... arguments) {
+            super(source, identifier, arguments);
+            this.description = description;
+        }
+        public String getEventDescription() {
+            return description;
+        }
+    }
+
+    public static class CompletedEvent extends AbstractActionDomainEvent {
+        private static final long serialVersionUID = 1L;
+        public CompletedEvent(
+                final ToDoItem source, 
+                final Identifier identifier, 
+                final Object... arguments) {
+            super("completed", source, identifier, arguments);
+        }
+    }
+
+    public static class NoLongerCompletedEvent extends 
AbstractActionDomainEvent {
+        private static final long serialVersionUID = 1L;
+        public NoLongerCompletedEvent(
+                final ToDoItem source, 
+                final Identifier identifier, 
+                final Object... arguments) {
+            super("no longer completed", source, identifier, arguments);
+        }
+    }
+
+    public static class DeletedEvent extends AbstractActionDomainEvent {
+        private static final long serialVersionUID = 1L;
+        public DeletedEvent(
+                final ToDoItem source, 
+                final Identifier identifier, 
+                final Object... arguments) {
+            super("deleted", source, identifier, arguments);
+        }
+    }
+
+    //endregion
+
+    //region > predicates
+
+    public static class Predicates {
+        
+        public static Predicate<ToDoItem> thoseOwnedBy(final String 
currentUser) {
+            return new Predicate<ToDoItem>() {
+                @Override
+                public boolean apply(final ToDoItem toDoItem) {
+                    return Objects.equal(toDoItem.getOwnedBy(), currentUser);
+                }
+            };
+        }
+
+        public static Predicate<ToDoItem> thoseCompleted(
+                final boolean completed) {
+            return new Predicate<ToDoItem>() {
+                @Override
+                public boolean apply(final ToDoItem t) {
+                    return Objects.equal(t.isComplete(), completed);
+                }
+            };
+        }
+
+        public static Predicate<ToDoItem> thoseWithSimilarDescription(final 
String description) {
+            return new Predicate<ToDoItem>() {
+                @Override
+                public boolean apply(final ToDoItem t) {
+                    return t.getDescription().contains(description);
+                }
+            };
+        }
+
+        @SuppressWarnings("unchecked")
+        public static Predicate<ToDoItem> thoseSimilarTo(final ToDoItem 
toDoItem) {
+            return com.google.common.base.Predicates.and(
+                    thoseNot(toDoItem),
+                    thoseOwnedBy(toDoItem.getOwnedBy()),
+                    thoseCategorised(toDoItem.getCategory()));
+        }
+
+        public static Predicate<ToDoItem> thoseNot(final ToDoItem toDoItem) {
+            return new Predicate<ToDoItem>() {
+                @Override
+                public boolean apply(final ToDoItem t) {
+                    return t != toDoItem;
+                }
+            };
+        }
+
+        public static Predicate<ToDoItem> thoseCategorised(final Category 
category) {
+            return new Predicate<ToDoItem>() {
+                @Override
+                public boolean apply(final ToDoItem toDoItem) {
+                    return Objects.equal(toDoItem.getCategory(), category);
+                }
+            };
+        }
+
+        public static Predicate<ToDoItem> thoseSubcategorised(
+                final Subcategory subcategory) {
+            return new Predicate<ToDoItem>() {
+                @Override
+                public boolean apply(final ToDoItem t) {
+                    return Objects.equal(t.getSubcategory(), subcategory);
+                }
+            };
+        }
+
+        public static Predicate<ToDoItem> thoseCategorised(
+                final Category category, final Subcategory subcategory) {
+            return com.google.common.base.Predicates.and(
+                    thoseCategorised(category), 
+                    thoseSubcategorised(subcategory)); 
+        }
+
+    }
+
+    //endregion
+
+    //region > toString, compareTo
+    @Override
+    public String toString() {
+        return ObjectContracts.toString(this, 
"description,complete,dueBy,ownedBy");
+    }
+
+    /**
+     * Required so can store in {@link SortedSet sorted set}s (eg {@link 
${symbol_pound}getDependencies()}). 
+     */
+    @Override
+    public int compareTo(final ToDoItem other) {
+        return ObjectContracts.compare(this, other, 
"complete,dueBy,description");
+    }
+    //endregion
+
+    //region > injected services
+    @javax.inject.Inject
+    DomainObjectContainer container;
+
+    @javax.inject.Inject
+    ToDoItems toDoItems;
+
+    @javax.inject.Inject
+    Scratchpad scratchpad;
+
+    /**
+     * public only so can be injected from integ tests
+     */
+    @javax.inject.Inject
+    public ActionInvocationContext actionInvocationContext;
+
+    /**
+     * public only so can be injected from integ tests
+     */
+    @javax.inject.Inject
+    public EventBusService eventBusService;
+
+    @javax.inject.Inject
+    WrapperFactory wrapperFactory;
+
+    //endregion
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.layout.json
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.layout.json
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.layout.json
new file mode 100644
index 0000000..cda5fdc
--- /dev/null
+++ 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.layout.json
@@ -0,0 +1,222 @@
+/**
+ *  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.
+ */
+{
+    "columns": [
+    {
+        "span": 6,
+        "memberGroups": {
+            "General": {
+                "members": {
+                    "description": {
+                        "propertyLayout": {
+                            "cssClass": "x-key",
+                            "labelPosition": "TOP"
+                        }
+                    },
+                    "category": {
+                        "propertyLayout": {
+                            "labelPosition": "TOP"
+                        }
+                    },
+                    "subcategory": {
+                        "propertyLayout": {
+                            "labelPosition": "NONE"
+                        },
+                        "actions": {
+                            "updateCategory": {
+                                "actionLayout": {
+                                    "named": "Update"
+                                }
+                            },
+                            "analyseCategory": {
+                                "actionLayout": {
+                                    "cssClass": "btn-default",
+                                    "cssClassFa": "fa fa-pie-chart"
+                                }
+                            }
+                        }
+                    },
+                    "ownedBy": {
+                        "propertyLayout": {
+                            "hidden": "EVERYWHERE"
+                        }
+                    },
+                    "complete": {
+                        "propertyLayout": {
+                            "named": "Whether this todo item has been 
<i>completed</i> or not.",
+                            "namedEscaped": false,
+                            "describedAs": "Whether this todo item has been 
completed, or not.",
+                            "labelPosition": "LEFT"
+                        },
+                        "actions": {
+                            "completed": {
+                                "actionLayout": {
+                                    "named": "Done",
+                                    "describedAs": "Update this todo item as 
complete",
+                                    "cssClass": "btn-success",
+                                    "cssClassFa": "fa fa-thumbs-up"
+                                }
+                            },
+                            "notYetCompleted": {
+                                "actionLayout": {
+                                    "named": "Not done",
+                                    "describedAs": "Update this todo item as 
not yet complete",
+                                    "cssClass": "btn-info",
+                                    "cssClassFa": "fa fa-thumbs-down"
+                                }
+                            },
+                            "scheduleExplicitly": {
+                            },
+                            "scheduleImplicitly": {
+                            }
+                        }
+                    }
+                }
+            },
+            "Misc": {
+                "members": {
+                    "versionSequence": {
+                        "propertyLayout": {
+                            "named": "Version",
+                            "hidden": "ALL_TABLES"
+                        },
+                        "disabled": {}
+                    }
+                }
+            }
+        }
+    },
+    {
+        "span": 6,
+        "memberGroups": {
+            "Priority": {
+                "members": {
+                     "relativePriority": {
+                        "actions": {
+                            "previous": {
+                                "actionLayout": {
+                                    "cssClassFa": "fa fa-step-backward"
+                                }
+                            },
+                            "next": {
+                                "actionLayout": {
+                                    "cssClassFa": "fa fa-step-forward",
+                                    "cssClassFaPosition": "right"
+                                }
+                            }
+                        }
+                    },
+                    "dueBy": {
+                        "propertyLayout": {
+                            "renderedAsDayBefore": "false"
+                        }
+                    }
+                }
+            },
+            "Other": {
+                "members": {
+                    "cost": {
+                        "actions": {
+                            "updateCost":{
+                                "actionLayout": {
+                                    "named": "Update",
+                                    "cssClassFa": "fa fa-dollar"
+                                }
+                            }
+                        }
+                    },
+                    "notes": {
+                        "propertyLayout": {
+                            "hidden": "ALL_TABLES",
+                            "labelPosition": "TOP",
+                            "multiLine": 5
+                        }
+                    },
+                    "attachment": {
+                        "propertyLayout": {
+                            "hidden": "STANDALONE_TABLES"
+                        }
+                    },
+                    "doc": {
+                        "propertyLayout": {
+                            "hidden": "STANDALONE_TABLES"
+                        }
+                    }
+                }
+            }
+        }
+    },
+    {
+        "span": 0
+    },
+    {
+        "span": 6,
+        "collections": {
+            "dependencies": {
+                "collectionLayout": {
+                    "named": "Todo items that are <i>dependencies</i> of this 
item.",
+                    "namedEscaped": false,
+                    "describedAs": "Todo items that must be completed before 
this one",
+                    "paged": 5,
+                    "render": "EAGERLY"
+                },
+                "disabled": {},
+                "actions": {
+                    "add":{
+                        "actionLayout": {
+                            "cssClass": "btn-default",
+                            "cssClassFa": "fa fa-plus-square"
+                        }
+                    },
+                    "remove": {
+                        "actionLayout": {
+                            "cssClass": "x-caution btn-default",
+                            "cssClassFa": "fa fa-minus-square"
+                        }
+                    }
+                }
+            },
+            "similarTo": {
+                "collectionLayout": {
+                    "named": "Todo items that are <i>similar to</i> this one",
+                    "namedEscaped": false,
+                    "paged": 3,
+                    "render": "LAZILY"
+                },
+                "disabled": {}
+            }
+        }
+    }
+    ],
+    "actions": {
+        "totalCost": {},
+        "delete": {
+            "actionLayout": {
+                "cssClass": "btn btn-danger",
+                "cssClassFa": "fa fa-trash fa-lg"
+            }
+        },
+        "duplicate": {
+            "actionLayout": {
+                "named": "Clone",
+                "describedAs": "Create a new todo item from this one",
+                "cssClass": "btn-warn",
+                "cssClassFa": "fa fa-copy fa-lg"
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.png
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.png
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.png
new file mode 100644
index 0000000..99a9fed
Binary files /dev/null and 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItem.png
 differ

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemChangedPayloadFactory.java
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemChangedPayloadFactory.java
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemChangedPayloadFactory.java
new file mode 100644
index 0000000..1d3b870
--- /dev/null
+++ 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemChangedPayloadFactory.java
@@ -0,0 +1,51 @@
+#set( $symbol_pound = '#' )
+#set( $symbol_dollar = '$' )
+#set( $symbol_escape = '\' )
+/*
+ *  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 dom.todo;
+
+import org.apache.isis.applib.annotation.PublishingChangeKind;
+import org.apache.isis.applib.annotation.PublishingPayloadFactoryForObject;
+import org.apache.isis.applib.services.publish.EventPayload;
+import org.apache.isis.applib.services.publish.EventPayloadForObjectChanged;
+
+public class ToDoItemChangedPayloadFactory implements 
PublishingPayloadFactoryForObject {
+
+    public static class ToDoItemPayload extends 
EventPayloadForObjectChanged<ToDoItem> {
+
+        public ToDoItemPayload(ToDoItem changed) {
+            super(changed);
+        }
+        
+        /**
+         * Expose the item's {@link ToDoItem${symbol_pound}getDescription() 
description} more explicitly
+         * in the payload.
+         */
+        public String getDescription() {
+            return getChanged().getDescription();
+        }
+    }
+
+    @Override
+    public EventPayload payloadFor(final Object changedObject, final 
PublishingChangeKind publishingChangeKind) {
+        return new ToDoItemPayload((ToDoItem) changedObject);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemContributions.java
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemContributions.java
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemContributions.java
new file mode 100644
index 0000000..c33097e
--- /dev/null
+++ 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemContributions.java
@@ -0,0 +1,274 @@
+#set( $symbol_pound = '#' )
+#set( $symbol_dollar = '$' )
+#set( $symbol_escape = '\' )
+/*
+ *  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 dom.todo;
+
+import dom.todo.ToDoItem.Category;
+import dom.todo.ToDoItem.Subcategory;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import org.joda.time.LocalDate;
+import org.apache.isis.applib.AbstractFactoryAndRepository;
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.Contributed;
+import org.apache.isis.applib.annotation.Disabled;
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.Optional;
+import org.apache.isis.applib.annotation.ParameterLayout;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.applib.query.QueryDefault;
+import org.apache.isis.applib.services.queryresultscache.QueryResultsCache;
+import org.apache.isis.applib.value.Clob;
+
+@DomainService(nature = NatureOfService.VIEW_CONTRIBUTIONS_ONLY)
+public class ToDoItemContributions extends AbstractFactoryAndRepository {
+
+    //region > priority (contributed property)
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            hidden = Where.ALL_TABLES
+    )
+    @ActionLayout(
+            describedAs = "The relative priority of this item compared to 
others not yet complete (using 'due by' date)",
+            contributed = Contributed.AS_ASSOCIATION
+    )
+    @Disabled(reason="Relative priority, derived from due date")
+    public Integer relativePriority(final ToDoItem toDoItem) {
+        return queryResultsCache.execute(new Callable<Integer>(){
+            @Override
+            public Integer call() throws Exception {
+                if(toDoItem.isComplete()) {
+                    return null;
+                }
+
+                // sort items, then locate this one
+                int i=1;
+                for (final ToDoItem each : sortedNotYetComplete()) {
+                    if(each == toDoItem) {
+                        return i;
+                    }
+                    i++;
+                }
+                return null;
+            }}, ToDoItemContributions.class, "relativePriority", toDoItem);
+    }
+
+    private List<ToDoItem> sortedNotYetComplete() {
+        return ORDERING_DUE_BY
+        .compound(ORDERING_DESCRIPTION)
+        .sortedCopy(toDoItems.notYetComplete());
+    }
+
+    private static final Ordering<ToDoItem> ORDERING_DUE_BY =
+        Ordering.natural().nullsLast().onResultOf(new Function<ToDoItem, 
LocalDate>(){
+            @Override
+            public LocalDate apply(final ToDoItem input) {
+                return input.getDueBy();
+            }
+        });
+    
+    private static final Ordering<ToDoItem> ORDERING_DESCRIPTION =
+        Ordering.natural().nullsLast().onResultOf(new Function<ToDoItem, 
String>(){
+            @Override
+            public String apply(final ToDoItem input) {
+                return input.getDescription();
+            }
+        });
+
+
+    //endregion
+
+    //region >  next, previous (contributed actions)
+    @Action(semantics = SemanticsOf.SAFE)
+    @ActionLayout(
+            describedAs = "The next item not yet completed",
+            contributed = Contributed.AS_ACTION
+    )
+    public ToDoItem next(final ToDoItem item) {
+        final Integer priority = relativePriority(item);
+        if(priority == null) {
+            return item;
+        }
+        int priorityOfNext = priority != null ? priority + 1 : 0;
+        return itemWithPriorityElse(priorityOfNext, item);
+    }
+    public String disableNext(final ToDoItem toDoItem) {
+        if (toDoItem.isComplete()) {
+            return "Completed";
+        }
+        if(next(toDoItem) == null) {
+            return "No next item";
+        }
+        return null;
+    }
+
+    // //////////////////////////////////////
+
+    @ActionLayout(
+            describedAs = "The previous item not yet completed",
+            contributed = Contributed.AS_ACTION
+    )
+    @Action(semantics = SemanticsOf.SAFE)
+    public ToDoItem previous(final ToDoItem item) {
+        final Integer priority = relativePriority(item);
+        if(priority == null) {
+            return item;
+        }
+        int priorityOfPrevious = priority != null? priority - 1 : 0;
+        return itemWithPriorityElse(priorityOfPrevious, item);
+    }
+    public String disablePrevious(final ToDoItem toDoItem) {
+        if (toDoItem.isComplete()) {
+            return "Completed";
+        }
+        if(previous(toDoItem) == null) {
+            return "No previous item";
+        }
+        return null;
+    }
+
+    // //////////////////////////////////////
+
+    /**
+     * @param priority : 1-based priority
+     */
+    private ToDoItem itemWithPriorityElse(int priority, final ToDoItem 
itemElse) {
+        if(priority < 1) {
+            return null;
+        }
+        final List<ToDoItem> items = sortedNotYetComplete();
+        if(priority > items.size()) {
+            return null;
+        }
+        return priority>=0 && items.size()>=priority? items.get(priority-1): 
itemElse;
+    }
+    //endregion
+
+    //region > similarTo (contributed collection)
+    @ActionLayout(
+            contributed = Contributed.AS_ASSOCIATION
+    )
+    @Action(semantics = SemanticsOf.SAFE)
+    public List<ToDoItem> similarTo(final ToDoItem toDoItem) {
+        final List<ToDoItem> similarToDoItems = allMatches(
+                new QueryDefault<ToDoItem>(ToDoItem.class,
+                        "findByOwnedByAndCategory", 
+                        "ownedBy", currentUserName(), 
+                        "category", toDoItem.getCategory()));
+        return Lists.newArrayList(Iterables.filter(similarToDoItems, 
excluding(toDoItem)));
+    }
+
+
+    private static Predicate<ToDoItem> excluding(final ToDoItem toDoItem) {
+        return new Predicate<ToDoItem>() {
+            @Override
+            public boolean apply(ToDoItem input) {
+                return input != toDoItem;
+            }
+        };
+    }
+    //endregion
+
+    //region > updateCategory (contributed action)
+
+    @ActionLayout(
+            describedAs = "Update category and subcategory"
+    )
+    @Action(semantics = SemanticsOf.IDEMPOTENT)
+    public Categorized updateCategory(
+            final Categorized item,
+            final @ParameterLayout(named="Category") Category category,
+            final @Optional @ParameterLayout(named="Subcategory") Subcategory 
subcategory) {
+        item.setCategory(category);
+        item.setSubcategory(subcategory);
+        return item;
+    }
+    public Category default1UpdateCategory(
+            final Categorized item) {
+        return item != null? item.getCategory(): null;
+    }
+    public Subcategory default2UpdateCategory(
+            final Categorized item) {
+        return item != null? item.getSubcategory(): null;
+    }
+
+    public List<Subcategory> choices2UpdateCategory(
+            final Categorized item, final Category category) {
+        return Subcategory.listFor(category);
+    }
+    
+    public String validateUpdateCategory(
+            final Categorized item, final Category category, final Subcategory 
subcategory) {
+        return Subcategory.validate(category, subcategory);
+    }
+    //endregion
+
+    // region > exportAsJson (action)
+    /**
+     * Demonstrates functionality of streaming back Clob/Blob result within an 
action with a prompt, i.e. Ajax request
+     */
+    @Action(semantics = SemanticsOf.SAFE)
+    public Clob exportAsJson(
+            final ToDoItem toDoItem,
+            @ParameterLayout(named = "File name") String fileName
+    ) {
+        if(!fileName.endsWith(".json")) {
+            fileName += ".json";
+        }
+        return new Clob(
+                fileName,
+                "application/json",
+                "{" +
+                "${symbol_escape}"description${symbol_escape}": 
${symbol_escape}"" + toDoItem.getDescription()+"${symbol_escape}"" +
+                ",${symbol_escape}"complete${symbol_escape}": " + 
""+toDoItem.isComplete() +
+                "}");
+    }
+
+    public String default1ExportAsJson() {
+        return "todo";
+    }
+    //endregion
+
+
+    //region > helpers
+    protected String currentUserName() {
+        return getContainer().getUser().getName();
+    }
+    //endregion
+
+    //region > injected services
+    @javax.inject.Inject
+    private ToDoItems toDoItems;
+
+    @javax.inject.Inject
+    private QueryResultsCache queryResultsCache;
+    //endregion
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java
----------------------------------------------------------------------
diff --git 
a/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java
 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java
new file mode 100644
index 0000000..ea76566
--- /dev/null
+++ 
b/mothballed/example/archetype/todoapp/src/main/resources/archetype-resources/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java
@@ -0,0 +1,402 @@
+#set( $symbol_pound = '#' )
+#set( $symbol_dollar = '$' )
+#set( $symbol_escape = '\' )
+/*
+ *  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 dom.todo;
+
+import java.util.EventObject;
+import java.util.List;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Lists;
+import com.google.common.eventbus.Subscribe;
+import org.apache.isis.applib.DomainObjectContainer;
+import org.apache.isis.applib.NonRecoverableException;
+import org.apache.isis.applib.RecoverableException;
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.DomainServiceLayout;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.ParameterLayout;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.annotation.RestrictTo;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.eventbus.ActionDomainEvent;
+import org.apache.isis.applib.services.eventbus.CollectionDomainEvent;
+import org.apache.isis.applib.services.eventbus.EventBusService;
+import org.apache.isis.applib.services.eventbus.PropertyDomainEvent;
+
+import static com.google.common.collect.Iterables.filter;
+import static com.google.common.collect.Iterables.transform;
+import static com.google.common.collect.Lists.newArrayList;
+
+/**
+ * Subscribes to changes made to  the {@link dom.todo.ToDoItem} entity.
+ *
+ * <p>
+ *     (For demo purposes) the behaviour can be influenced using {@link 
${symbol_pound}subscriberBehaviour(dom.todo.ToDoItemSubscriptions.Behaviour)}.
+ *     In particular, the subscriber can be used to hide/disable/validate 
actions, or just to perform pre- or post-execute
+ *     tasks.  This also includes being set to throw an exception during the 
execution of the action (also in effect
+ *     vetoing the change).
+ * </p>
+ */
+@DomainService(nature = NatureOfService.VIEW_MENU_ONLY)
+@DomainServiceLayout(menuBar = DomainServiceLayout.MenuBar.SECONDARY, 
menuOrder = "30")
+public class ToDoItemSubscriptions {
+
+    //region > LOG
+    private final static org.slf4j.Logger LOG = 
org.slf4j.LoggerFactory.getLogger(ToDoItemSubscriptions.class);
+    //endregion
+
+    //region > postConstruct, preDestroy
+
+    /**
+     * Registers this service with the {@link 
org.apache.isis.applib.services.eventbus.EventBusService}.
+     *
+     * <p>
+     *     Because this service is a singleton, this is called during initial 
bootstrap.
+     * </p>
+     */
+    @Programmatic
+    @PostConstruct
+    public void postConstruct() {
+        LOG.info("postConstruct: registering to event bus");
+        eventBusService.register(this);
+    }
+
+    /**
+     * Unregisters this service from the {@link 
org.apache.isis.applib.services.eventbus.EventBusService}.
+     *
+     * <p>
+     *     Because this service is a singleton, this is only done when the 
system is shutdown.
+     * </p>
+     */
+    @Programmatic
+    @PreDestroy
+    public void preDestroy() {
+        LOG.info("preDestroy: unregistering from event bus");
+        eventBusService.unregister(this);
+    }
+    //endregion
+
+
+    //region > on(Event)...
+
+    public static enum Behaviour {
+        AnyExecuteAccept,
+        AnyExecuteVetoWithRecoverableException,
+        AnyExecuteVetoWithNonRecoverableException,
+        AnyExecuteVetoWithOtherException,
+        UpdateCostActionHide,
+        UpdateCostActionDisable,
+        UpdateCostActionInvalidate,
+        DescriptionPropertyHide,
+        DescriptionPropertyDisable,
+        DescriptionPropertyInvalidate,
+        DependenciesCollectionHide,
+        // not implemented in Wicket viewer, but supported in wrapped objects
+        DependenciesCollectionDisable,
+        // not implemented in Wicket viewer, but supported in wrapped objects
+        DependenciesCollectionInvalidateAdd,
+        DependenciesCollectionInvalidateRemove,
+        SimilarToCollectionHide
+    }
+
+    /**
+     * The desired behaviour of this service.
+     */
+    private Behaviour behaviour = Behaviour.AnyExecuteAccept;
+
+    /**
+     * To demo/test what occurs if a subscriber that might veto an event.
+     */
+    @MemberOrder(name = "Prototyping", sequence = "80")
+    @ActionLayout(
+        named="Set subscriber behaviour"
+    )
+    @Action(
+            semantics = SemanticsOf.IDEMPOTENT,
+            restrictTo = RestrictTo.PROTOTYPING
+    )
+    public void subscriberBehaviour(
+            @ParameterLayout(
+                    named="Behaviour"
+            )
+            final Behaviour behaviour) {
+        this.behaviour = behaviour;
+        container.informUser("Subscriber behaviour set to: " + behaviour);
+    }
+    public Behaviour default0SubscriberBehaviour() {
+        return this.behaviour;
+    }
+
+    @Programmatic
+    public Behaviour getSubscriberBehaviour() {
+        return behaviour;
+    }
+
+
+    private void onExecutedThrowExceptionIfSet(final ActionDomainEvent<?> ev) {
+        if(ev != null && ev.getSemantics().isSafe()) {
+            return;
+        }
+        onExecutedThrowExceptionIfSet();
+    }
+    private void onExecutedThrowExceptionIfSet(final PropertyDomainEvent<?, ?> 
ev) {
+        onExecutedThrowExceptionIfSet();
+    }
+    private void onExecutedThrowExceptionIfSet(final CollectionDomainEvent<?, 
?> ev) {
+        onExecutedThrowExceptionIfSet();
+    }
+
+
+    private void onExecutedThrowExceptionIfSet() {
+        if(behaviour == Behaviour.AnyExecuteVetoWithRecoverableException) {
+            throw new RecoverableException("Rejecting event (recoverable 
exception thrown)");
+        }
+        if(behaviour == Behaviour.AnyExecuteVetoWithNonRecoverableException) {
+            throw new NonRecoverableException("Rejecting event 
(non-recoverable exception thrown)");
+        }
+        if(behaviour == Behaviour.AnyExecuteVetoWithOtherException) {
+            throw new RuntimeException("Throwing some other exception");
+        }
+    }
+    //endregion
+
+    //region > on(Event) for ToDoItem-specific events
+    @Programmatic
+    @Subscribe
+    public void on(final ToDoItem.CompletedEvent ev) {
+        recordEvent(ev);
+        switch(ev.getEventPhase()) {
+            case HIDE:
+                break;
+            case DISABLE:
+                break;
+            case VALIDATE:
+                break;
+            case EXECUTING:
+                break;
+            case EXECUTED:
+                LOG.info("Received ToDoItem.CompletedEvent for : " + 
ev.getSource().toString());
+                break;
+        }
+    }
+    //endregion
+
+    //region > on(Event) ... general purpose
+
+    @Programmatic
+    @Subscribe
+    public void on(final ActionDomainEvent<?> ev) {
+        recordEvent(ev);
+        switch(ev.getEventPhase()) {
+            case HIDE:
+                if(getSubscriberBehaviour() == Behaviour.UpdateCostActionHide) 
{
+                    
if(ev.getIdentifier().getMemberName().equals("updateCost")) {
+                        ev.hide();
+                    }
+                }
+                break;
+            case DISABLE:
+                if(getSubscriberBehaviour() == 
Behaviour.UpdateCostActionDisable) {
+                    
if(ev.getIdentifier().getMemberName().equals("updateCost")) {
+                        ev.disable("ToDoItemSubscriptions says: updateCost 
action disabled!");
+                    }
+                }
+                break;
+            case VALIDATE:
+                if(getSubscriberBehaviour() == 
Behaviour.UpdateCostActionInvalidate &&
+                        
ev.getIdentifier().getMemberName().equals("updateCost")) {
+                    ev.invalidate("ToDoItemSubscriptions says: can't invoke 
updateCost action with these args!");
+                }
+                break;
+            case EXECUTING:
+                break;
+            case EXECUTED:
+                LOG.info("Received ActionDomainEvent, " + 
ev.getSource().toString() + ", invoked " + ev.getIdentifier().getMemberName());
+                onExecutedThrowExceptionIfSet(ev);
+                break;
+        }
+    }
+
+    @Programmatic
+    @Subscribe
+    public void on(PropertyDomainEvent<?,?> ev) {
+        recordEvent(ev);
+        switch(ev.getEventPhase()) {
+            case HIDE:
+                if(getSubscriberBehaviour() == 
Behaviour.DescriptionPropertyHide &&
+                    ev.getIdentifier().getMemberName().equals("description")) {
+                    ev.veto("");
+                }
+                break;
+            case DISABLE:
+                if(getSubscriberBehaviour() == 
Behaviour.DescriptionPropertyDisable &&
+                    ev.getIdentifier().getMemberName().equals("description")) {
+                    ev.veto("ToDoItemSubscriptions says: description property 
disabled!");
+                }
+                break;
+            case VALIDATE:
+                if(getSubscriberBehaviour() == 
Behaviour.DescriptionPropertyInvalidate &&
+                    ev.getIdentifier().getMemberName().equals("description")) {
+                    ev.veto("ToDoItemSubscriptions says: can't change 
description property to this value!");
+                }
+                break;
+            case EXECUTING:
+                break;
+            case EXECUTED:
+                LOG.info("Received PropertyDomainEvent, " + 
ev.getSource().toString() + ", changed " + ev.getIdentifier().getMemberName() + 
" : " + ev.getOldValue() + " -> " + ev.getNewValue());
+                onExecutedThrowExceptionIfSet(ev);
+
+                if(ev.getIdentifier().getMemberName().contains("description")) 
{
+                    String newValue = (String) ev.getNewValue();
+                    if(newValue.matches(".*demo veto.*")) {
+                        throw new RecoverableException("oh no you don't! " + 
ev.getNewValue());
+                    }
+                }
+                break;
+        }
+    }
+
+    @Programmatic
+    @Subscribe
+    public void on(CollectionDomainEvent<?,?> ev) {
+        recordEvent(ev);
+        switch (ev.getEventPhase()) {
+            case HIDE:
+                if(getSubscriberBehaviour() == 
Behaviour.DependenciesCollectionHide &&
+                    ev.getIdentifier().getMemberName().equals("dependencies")) 
{
+                    ev.veto("");
+                }
+                if (getSubscriberBehaviour() == 
Behaviour.SimilarToCollectionHide &&
+                    ev.getIdentifier().getMemberName().equals("similarTo")) {
+                    ev.veto("");
+                }
+                break;
+            case DISABLE:
+                if (getSubscriberBehaviour() == 
Behaviour.DependenciesCollectionDisable &&
+                    ev.getIdentifier().getMemberName().equals("dependencies")) 
{
+                    ev.veto("ToDoItemSubscriptions says: dependencies 
collection disabled!");
+                }
+                break;
+            case VALIDATE:
+                if(getSubscriberBehaviour() == 
Behaviour.DependenciesCollectionInvalidateAdd &&
+                    ev.getIdentifier().getMemberName().equals("dependencies") 
&&
+                    ev.getOf() == CollectionDomainEvent.Of.ADD_TO ) {
+                    ev.veto("ToDoItemSubscriptions says: can't add this object 
to dependencies collection!");
+                }
+                if(getSubscriberBehaviour() == 
Behaviour.DependenciesCollectionInvalidateRemove &&
+                    ev.getIdentifier().getMemberName().equals("dependencies") 
&&
+                    ev.getOf() == CollectionDomainEvent.Of.REMOVE_FROM ) {
+                    ev.veto("ToDoItemSubscriptions says: can't remove this 
object from dependencies collection!");
+                }
+                break;
+            case EXECUTING:
+                break;
+            case EXECUTED:
+                if(ev.getOf() == CollectionDomainEvent.Of.ADD_TO) {
+                    LOG.info("Received CollectionDomainEvent, " + 
ev.getSource().toString() + ", added to " + ev.getIdentifier().getMemberName() 
+ " : " + ev.getValue());
+                } else {
+                    LOG.info("Received CollectionDomainEvent, " + 
ev.getSource().toString() + ", removed from " + 
ev.getIdentifier().getMemberName() + " : " + ev.getValue());
+                }
+                onExecutedThrowExceptionIfSet(ev);
+                break;
+        }
+
+    }
+
+    //endregion
+
+    //region > receivedEvents
+    private final List<java.util.EventObject> receivedEvents = 
Lists.newLinkedList();
+
+    /**
+     * Used in integration tests.
+     */
+    @Programmatic
+    public List<java.util.EventObject> receivedEvents() {
+        return receivedEvents;
+    }
+
+    /**
+     * Used in integration tests.
+     */
+    @Programmatic
+    public <T extends java.util.EventObject> List<T> receivedEvents(final 
Class<T> expectedType) {
+        return newArrayList(
+                    transform(
+                        filter(receivedEvents, instanceOf(expectedType)),
+                        castTo(expectedType)));
+    }
+
+    private static <T extends EventObject> Function<EventObject, T> 
castTo(Class<T> expectedType) {
+        return new Function<EventObject, T>() {
+                    @Override
+                    public T apply(EventObject input) {
+                        return (T) input;
+                    }
+                };
+    }
+
+    private static <T extends EventObject> Predicate<EventObject> 
instanceOf(final Class<T> expectedType) {
+        return new Predicate<EventObject>() {
+            @Override
+            public boolean apply(EventObject input) {
+                return expectedType.isInstance(input);
+            }
+        };
+    }
+
+    /**
+     * Used in integration tests.
+     */
+    @Programmatic
+    public <T extends java.util.EventObject> T 
mostRecentlyReceivedEvent(Class<T> expectedType) {
+        final List<T> receivedEvents = receivedEvents(expectedType);
+        return !receivedEvents.isEmpty() ? receivedEvents.get(0) : null;
+    }
+    private void recordEvent(final java.util.EventObject ev) {
+        receivedEvents.add(0, ev);
+    }
+    /**
+     * Used in integration tests.
+     */
+    @Programmatic
+    public void reset() {
+        receivedEvents.clear();
+        subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteAccept);
+    }
+    //endregion
+
+    //region > injected services
+    @javax.inject.Inject
+    private DomainObjectContainer container;
+
+    @javax.inject.Inject
+    private EventBusService eventBusService;
+    //endregion
+
+
+}

Reply via email to