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 2021/09/27 18:06:32 UTC

[isis] 01/01: ISIS-2873: copies over petclinic tutorial docs from 1.16.2

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

danhaywood pushed a commit to branch ISIS-2873-petclinic
in repository https://gitbox.apache.org/repos/asf/isis.git

commit 598d76ea24b00b0f64378406008efd106c9a7aa0
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Mon Sep 27 18:55:49 2021 +0100

    ISIS-2873: copies over petclinic tutorial docs from 1.16.2
---
 antora/components/tutorials/modules/ROOT/nav.adoc  |   3 +
 .../tutorials/modules/ROOT/pages/about.adoc        |  11 +
 antora/components/tutorials/modules/antora.yml     |  25 +
 .../images/Dashboard-overdue-ui-hints.png          | Bin 0 -> 42774 bytes
 .../modules/petclinic/images/Dashboard-overdue.png | Bin 0 -> 49707 bytes
 .../modules/petclinic/images/HelloWorldObject.png  | Bin 0 -> 44720 bytes
 .../modules/petclinic/images/HelloWorldObjects.png | Bin 0 -> 20802 bytes
 .../petclinic/images/Owner-name-updated.png        | Bin 0 -> 5008 bytes
 .../modules/petclinic/images/Owner-name.png        | Bin 0 -> 3766 bytes
 .../modules/petclinic/images/Owner-pets.png        | Bin 0 -> 25561 bytes
 .../petclinic/images/Owner-updateName-prompt.png   | Bin 0 -> 6542 bytes
 .../images/Owner-with-contact-details.png          | Bin 0 -> 8719 bytes
 .../images/Owners-create-with-phoneNumber.png      | Bin 0 -> 15426 bytes
 .../images/Pet-bookVisit-prompt-with-default.png   | Bin 0 -> 45252 bytes
 .../images/Pet-bookVisit-prompt-with-validate.png  | Bin 0 -> 39819 bytes
 .../petclinic/images/Pet-bookVisit-prompt.png      | Bin 0 -> 37767 bytes
 .../modules/petclinic/images/Pet-icons.png         | Bin 0 -> 31150 bytes
 .../petclinic/images/Pet-visits-collection.png     | Bin 0 -> 34249 bytes
 .../tutorials/modules/petclinic/images/Pet.png     | Bin 0 -> 25482 bytes
 .../petclinic/images/Visit-enterOutcome.png        | Bin 0 -> 39463 bytes
 .../tutorials/modules/petclinic/images/Visit.png   | Bin 0 -> 26923 bytes
 .../petclinic/images/Visits-paid-strikethrough.png | Bin 0 -> 52925 bytes
 .../modules/petclinic/images/dashboard.png         | Bin 0 -> 19084 bytes
 .../images/extended-manifest-run-configuration.png | Bin 0 -> 47592 bytes
 .../tutorials/modules/petclinic/images/hand.png    | Bin 0 -> 326 bytes
 .../petclinic/images/hello-world-objects-menu.png  | Bin 0 -> 7578 bytes
 .../modules/petclinic/images/home-page.png         | Bin 0 -> 43943 bytes
 .../tutorials/modules/petclinic/images/index.png   | Bin 0 -> 73486 bytes
 .../images/intellij-java-compiler-parameters.png   | Bin 0 -> 52596 bytes
 ...iguration-before-launch-datanucleus-enhance.png | Bin 0 -> 7541 bytes
 .../intellij-run-configuration-before-launch.png   | Bin 0 -> 7276 bytes
 .../images/intellij-run-configuration.png          | Bin 0 -> 46429 bytes
 .../tutorials/modules/petclinic/images/login.png   | Bin 0 -> 19956 bytes
 .../modules/petclinic/images/object-a.png          | Bin 0 -> 25310 bytes
 .../petclinic/images/owner-newPet-prompt.png       | Bin 0 -> 7634 bytes
 .../modules/petclinic/images/owner-newPet.png      | Bin 0 -> 9643 bytes
 .../images/project-loaded-into-intellij.png        | Bin 0 -> 31799 bytes
 .../modules/petclinic/images/prototyping-menu.png  | Bin 0 -> 14260 bytes
 .../images/run-fixture-script-menu-item.png        | Bin 0 -> 15882 bytes
 .../petclinic/images/run-fixture-script-prompt.png | Bin 0 -> 10836 bytes
 .../petclinic/images/run-fixture-script-result.png | Bin 0 -> 16521 bytes
 .../modules/petclinic/images/swagger-ui.png        | Bin 0 -> 59554 bytes
 .../modules/petclinic/images/tertiary-menu.png     | Bin 0 -> 4687 bytes
 .../tutorials/modules/petclinic/nav.adoc           |  73 +++
 .../tutorials/modules/petclinic/pages/about.adoc   |  26 +
 ...ing-further-business-logic-worked-examples.adoc | 707 +++++++++++++++++++++
 .../pages/adding-the-remaining-classes.adoc        | 442 +++++++++++++
 .../modules/petclinic/pages/an-example-domain.adoc | 126 ++++
 .../pages/business-rules-and-unit-testing.adoc     | 153 +++++
 .../pages/fleshing-out-the-owner-entity.adoc       | 513 +++++++++++++++
 .../modules/petclinic/pages/getting-started.adoc   | 363 +++++++++++
 .../tutorials/modules/petclinic/pages/i18n.adoc    |   6 +
 .../petclinic/pages/integration-testing.adoc       | 628 ++++++++++++++++++
 .../modules/petclinic/pages/modularity.adoc        | 382 +++++++++++
 .../modules/petclinic/pages/prototyping.adoc       | 190 ++++++
 .../petclinic/pages/removing-boilerplatex.adoc     | 236 +++++++
 .../modules/petclinic/pages/view-models.adoc       |  97 +++
 antora/components/tutorials/nav.adoc               |  73 +++
 antora/playbooks/site-tutorials.yml                |  86 +++
 antora/playbooks/site.yml                          |   3 +
 60 files changed, 4143 insertions(+)

diff --git a/antora/components/tutorials/modules/ROOT/nav.adoc b/antora/components/tutorials/modules/ROOT/nav.adoc
new file mode 100644
index 0000000..9e0d1b7
--- /dev/null
+++ b/antora/components/tutorials/modules/ROOT/nav.adoc
@@ -0,0 +1,3 @@
+
+:Notice: 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 ag [...]
+
diff --git a/antora/components/tutorials/modules/ROOT/pages/about.adoc b/antora/components/tutorials/modules/ROOT/pages/about.adoc
new file mode 100644
index 0000000..9728e9d
--- /dev/null
+++ b/antora/components/tutorials/modules/ROOT/pages/about.adoc
@@ -0,0 +1,11 @@
+= Tutorials
+:page-role: -toc
+
+:Notice: 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 ag [...]
+
+
+This page catalogues the tutorials available to help you learn Apache Isis.
+
+* xref:tutorials:petclinic:about.adoc[Petclinic]
++
+A take on the classic petclinc application
diff --git a/antora/components/tutorials/modules/antora.yml b/antora/components/tutorials/modules/antora.yml
new file mode 100644
index 0000000..0352027
--- /dev/null
+++ b/antora/components/tutorials/modules/antora.yml
@@ -0,0 +1,25 @@
+#  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.
+
+name: tutorials
+title: "Tutorials"
+version: latest
+start_page: ROOT:about.adoc
+nav:
+  - modules/ROOT/nav.adoc
+  - modules/petclinic/nav.adoc
+
diff --git a/antora/components/tutorials/modules/petclinic/images/Dashboard-overdue-ui-hints.png b/antora/components/tutorials/modules/petclinic/images/Dashboard-overdue-ui-hints.png
new file mode 100644
index 0000000..1887eb6
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Dashboard-overdue-ui-hints.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Dashboard-overdue.png b/antora/components/tutorials/modules/petclinic/images/Dashboard-overdue.png
new file mode 100644
index 0000000..fe5e320
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Dashboard-overdue.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/HelloWorldObject.png b/antora/components/tutorials/modules/petclinic/images/HelloWorldObject.png
new file mode 100644
index 0000000..633e72f
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/HelloWorldObject.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/HelloWorldObjects.png b/antora/components/tutorials/modules/petclinic/images/HelloWorldObjects.png
new file mode 100644
index 0000000..834a2e8
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/HelloWorldObjects.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Owner-name-updated.png b/antora/components/tutorials/modules/petclinic/images/Owner-name-updated.png
new file mode 100644
index 0000000..6ed762c
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Owner-name-updated.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Owner-name.png b/antora/components/tutorials/modules/petclinic/images/Owner-name.png
new file mode 100644
index 0000000..4c1f283
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Owner-name.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Owner-pets.png b/antora/components/tutorials/modules/petclinic/images/Owner-pets.png
new file mode 100644
index 0000000..652e7d8
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Owner-pets.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Owner-updateName-prompt.png b/antora/components/tutorials/modules/petclinic/images/Owner-updateName-prompt.png
new file mode 100644
index 0000000..d8dcdbf
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Owner-updateName-prompt.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Owner-with-contact-details.png b/antora/components/tutorials/modules/petclinic/images/Owner-with-contact-details.png
new file mode 100644
index 0000000..29267b5
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Owner-with-contact-details.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Owners-create-with-phoneNumber.png b/antora/components/tutorials/modules/petclinic/images/Owners-create-with-phoneNumber.png
new file mode 100644
index 0000000..1273e3e
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Owners-create-with-phoneNumber.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Pet-bookVisit-prompt-with-default.png b/antora/components/tutorials/modules/petclinic/images/Pet-bookVisit-prompt-with-default.png
new file mode 100644
index 0000000..c6176f3
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Pet-bookVisit-prompt-with-default.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Pet-bookVisit-prompt-with-validate.png b/antora/components/tutorials/modules/petclinic/images/Pet-bookVisit-prompt-with-validate.png
new file mode 100644
index 0000000..985b05a
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Pet-bookVisit-prompt-with-validate.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Pet-bookVisit-prompt.png b/antora/components/tutorials/modules/petclinic/images/Pet-bookVisit-prompt.png
new file mode 100644
index 0000000..8726a88
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Pet-bookVisit-prompt.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Pet-icons.png b/antora/components/tutorials/modules/petclinic/images/Pet-icons.png
new file mode 100644
index 0000000..8969d8f
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Pet-icons.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Pet-visits-collection.png b/antora/components/tutorials/modules/petclinic/images/Pet-visits-collection.png
new file mode 100644
index 0000000..4d06e5c
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Pet-visits-collection.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Pet.png b/antora/components/tutorials/modules/petclinic/images/Pet.png
new file mode 100644
index 0000000..ffd8d7a
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Pet.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Visit-enterOutcome.png b/antora/components/tutorials/modules/petclinic/images/Visit-enterOutcome.png
new file mode 100644
index 0000000..7b913d6
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Visit-enterOutcome.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Visit.png b/antora/components/tutorials/modules/petclinic/images/Visit.png
new file mode 100644
index 0000000..8aedd6f
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Visit.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/Visits-paid-strikethrough.png b/antora/components/tutorials/modules/petclinic/images/Visits-paid-strikethrough.png
new file mode 100644
index 0000000..0546da0
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/Visits-paid-strikethrough.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/dashboard.png b/antora/components/tutorials/modules/petclinic/images/dashboard.png
new file mode 100644
index 0000000..a13bf5a
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/dashboard.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/extended-manifest-run-configuration.png b/antora/components/tutorials/modules/petclinic/images/extended-manifest-run-configuration.png
new file mode 100644
index 0000000..b580d75
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/extended-manifest-run-configuration.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/hand.png b/antora/components/tutorials/modules/petclinic/images/hand.png
new file mode 100644
index 0000000..97cd875
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/hand.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/hello-world-objects-menu.png b/antora/components/tutorials/modules/petclinic/images/hello-world-objects-menu.png
new file mode 100644
index 0000000..c9a45f4
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/hello-world-objects-menu.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/home-page.png b/antora/components/tutorials/modules/petclinic/images/home-page.png
new file mode 100644
index 0000000..5763ab7
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/home-page.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/index.png b/antora/components/tutorials/modules/petclinic/images/index.png
new file mode 100644
index 0000000..a84a0a5
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/index.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/intellij-java-compiler-parameters.png b/antora/components/tutorials/modules/petclinic/images/intellij-java-compiler-parameters.png
new file mode 100644
index 0000000..9a2d43e
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/intellij-java-compiler-parameters.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/intellij-run-configuration-before-launch-datanucleus-enhance.png b/antora/components/tutorials/modules/petclinic/images/intellij-run-configuration-before-launch-datanucleus-enhance.png
new file mode 100644
index 0000000..548d2a9
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/intellij-run-configuration-before-launch-datanucleus-enhance.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/intellij-run-configuration-before-launch.png b/antora/components/tutorials/modules/petclinic/images/intellij-run-configuration-before-launch.png
new file mode 100644
index 0000000..8cd9f74
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/intellij-run-configuration-before-launch.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/intellij-run-configuration.png b/antora/components/tutorials/modules/petclinic/images/intellij-run-configuration.png
new file mode 100644
index 0000000..066e994
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/intellij-run-configuration.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/login.png b/antora/components/tutorials/modules/petclinic/images/login.png
new file mode 100644
index 0000000..a2d3618
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/login.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/object-a.png b/antora/components/tutorials/modules/petclinic/images/object-a.png
new file mode 100644
index 0000000..23f65de
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/object-a.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/owner-newPet-prompt.png b/antora/components/tutorials/modules/petclinic/images/owner-newPet-prompt.png
new file mode 100644
index 0000000..8e1b179
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/owner-newPet-prompt.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/owner-newPet.png b/antora/components/tutorials/modules/petclinic/images/owner-newPet.png
new file mode 100644
index 0000000..a3b459c
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/owner-newPet.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/project-loaded-into-intellij.png b/antora/components/tutorials/modules/petclinic/images/project-loaded-into-intellij.png
new file mode 100644
index 0000000..a00b632
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/project-loaded-into-intellij.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/prototyping-menu.png b/antora/components/tutorials/modules/petclinic/images/prototyping-menu.png
new file mode 100644
index 0000000..a903a93
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/prototyping-menu.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/run-fixture-script-menu-item.png b/antora/components/tutorials/modules/petclinic/images/run-fixture-script-menu-item.png
new file mode 100644
index 0000000..5cb55e9
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/run-fixture-script-menu-item.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/run-fixture-script-prompt.png b/antora/components/tutorials/modules/petclinic/images/run-fixture-script-prompt.png
new file mode 100644
index 0000000..024e200
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/run-fixture-script-prompt.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/run-fixture-script-result.png b/antora/components/tutorials/modules/petclinic/images/run-fixture-script-result.png
new file mode 100644
index 0000000..4ad7117
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/run-fixture-script-result.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/swagger-ui.png b/antora/components/tutorials/modules/petclinic/images/swagger-ui.png
new file mode 100644
index 0000000..897d2e0
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/swagger-ui.png differ
diff --git a/antora/components/tutorials/modules/petclinic/images/tertiary-menu.png b/antora/components/tutorials/modules/petclinic/images/tertiary-menu.png
new file mode 100644
index 0000000..4f6d2ca
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/tertiary-menu.png differ
diff --git a/antora/components/tutorials/modules/petclinic/nav.adoc b/antora/components/tutorials/modules/petclinic/nav.adoc
new file mode 100644
index 0000000..e8f62c2
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/nav.adoc
@@ -0,0 +1,73 @@
+
+* xref:about.adoc[Introduction]
+
+* xref:getting-started.adoc[Getting Started]
+** xref:getting-started.adoc#_prereqs[Prereqs]
+** xref:getting-started.adoc#_generate[Generate]
+** xref:getting-started.adoc#_explore_the_generated_app[Explore the generated app]
+** xref:getting-started.adoc#_pull_down_github_example_solution[image:hand.png[] *010*: Pull down the github example/solution]
+** xref:getting-started.adoc#_set_up_your_dev_env[Set up your dev env]
+** xref:getting-started.adoc#_naked_objects_pattern[Naked Objects pattern]
+** xref:getting-started.adoc#_ui_hints[UI Hints]
+
+* xref:an-example-domain.adoc[An Example Domain]
+** xref:an-example-domain.adoc#_rename_code_helloworldobject_code_to_code_owner_code[image:hand.png[] *020*: Rename HelloWorldObject to Owner]
+
+* xref:removing-boilerplatex.adoc[Removing Boilerplate]
+** xref:removing-boilerplatex.adoc#_removing_boilerplate_lombok[image:hand.png[] *030*: Lombok]
+** xref:removing-boilerplatex.adoc#_removing_boilerplate_parameter_names[image:hand.png[] *040*: Parameter Names]
+** xref:removing-boilerplatex.adoc#_disable_editing[image:hand.png[] *050*: Disable editing]
+** xref:removing-boilerplatex.adoc#_font_awesome_icons[Font awesome icons]
+** xref:removing-boilerplatex.adoc#_implicit_action_annotations[image:hand.png[] *060*: Implicit Action Annotations]
+
+* xref:fleshing-out-the-owner-entity.adoc[Fleshing out the Owner entity]
+** xref:fleshing-out-the-owner-entity.adoc#_rework_code_owner_code_s_name_code_firstname_code_and_code_lastname_code[image:hand.png[] *070*: Rework Owner's name (firstName and lastName)]
+** xref:fleshing-out-the-owner-entity.adoc#_derived_name_property[image:hand.png[] *080*: Derived name property]
+** xref:fleshing-out-the-owner-entity.adoc#_digression_changing_the_app_name[image:hand.png[] *090*: Digression: Changing the App Name]
+** xref:fleshing-out-the-owner-entity.adoc#_changing_the_object_type_class_alias[image:hand.png[] *100*: Changing the "Object Type" Class Alias]
+** xref:fleshing-out-the-owner-entity.adoc#_add_other_properties_for_code_owner_code[image:hand.png[] *110*: Add other properties for Owner]
+** xref:fleshing-out-the-owner-entity.adoc#_using_specifications_to_encapsulate_business_rules[image:hand.png[] *120*: Using specifications to encapsulate business rules]
+
+* xref:prototyping.adoc[Prototyping]
+** xref:prototyping.adoc#_fixture_scripts_for_owner[image:hand.png[] *130*: Fixture Scripts (for Owner)]
+** xref:prototyping.adoc#_run_with_a_different_manifest[image:hand.png[] *140*: Run with a different manifest]
+
+* xref:adding-the-remaining-classes.adoc[Adding the remaining classes]
+** xref:adding-the-remaining-classes.adoc#_newpet_action_and_code_pet_code_to_code_owner_code_association[image:hand.png[] *150*: `newPet` action, `Pet` to `Owner`]
+** xref:adding-the-remaining-classes.adoc#_collection_of_code_pet_code_s[image:hand.png[] *160*: Collection of Pets)]
+** xref:adding-the-remaining-classes.adoc#_extend_our_fixture[image:hand.png[] *170*: Extend our Fixtures]
+** xref:adding-the-remaining-classes.adoc#_adding_code_visit_code[image:hand.png[] *180*: Adding Visit]
+
+* xref:business-rules-and-unit-testing.adoc[Business Rules & (Unit) Testing]
+** xref:business-rules-and-unit-testing.adoc#_defaults_and_code_clockservice_code[image:hand.png[] *190*: Defaults, and ClockService]
+** xref:business-rules-and-unit-testing.adoc#_unit_tests[image:hand.png[] *200*: Unit Tests]
+** xref:business-rules-and-unit-testing.adoc#_validation[image:hand.png[] *210*: Validation]
+
+* xref:modularity.adoc[Modularity]
+** xref:modularity.adoc#_introducing_packages[image:hand.png[] *220*: Introducing Packages]
+** xref:modularity.adoc#_inverting_responsibilities_refactoring_the_code_pet_code_s_visits[image:hand.png[] *230*: Inverting responsibilities (Refactoring the Pet's visits)]
+** xref:modularity.adoc#_pet_s_visits_a_contributed_collection[image:hand.png[] *240*: Pet’s visits (a contributed collection)]
+** xref:modularity.adoc#_events[image:hand.png[] *250*: Events]
+
+* xref:view-models.adoc[View Models]
+** xref:view-models.adoc#_dashboard[image:hand.png[] *260*: Dashboard]
+
+* xref:integration-testing.adoc[(Integration) Testing]
+** xref:integration-testing.adoc#_an_improved_fixture_script[image:hand.png[] *270*: An improved Fixture Script]
+** xref:integration-testing.adoc#_writing_integration_tests[image:hand.png[] *280*: Writing Integration Tests]
+** xref:integration-testing.adoc#_factor_out_abstract_integration_test[image:hand.png[] *290*: Factor out abstract integration test]
+** xref:integration-testing.adoc#_move_teardowns_to_modules[image:hand.png[] *300*: Move teardowns to modules]
+** xref:integration-testing.adoc#_fake_data_service[image:hand.png[] *310*: Fake Data Service]
+** xref:integration-testing.adoc#_extend_the_fixture_script_to_set_up_visits[image:hand.png[] *320*: Extend the Fixture script to set up visits]
+
+* xref:adding-further-business-logic-worked-examples.adoc[Further business logic]
+** xref:adding-further-business-logic-worked-examples.adoc#_enter_an_outcome[image:hand.png[] *330*: Enter an outcome]
+** xref:adding-further-business-logic-worked-examples.adoc#_pay_for_a_visit[image:hand.png[] *340*: Pay for a visit]
+** xref:adding-further-business-logic-worked-examples.adoc#_prevent_payment_for_a_visit_twice[image:hand.png[] *350*: Prevent payment for a visit twice ("see it? use it? do it?")]
+** xref:adding-further-business-logic-worked-examples.adoc#_find_code_visit_code_s_not_yet_paid_and_overdue[image:hand.png[] *360*: Find Visits not yet paid and overdue]
+** xref:adding-further-business-logic-worked-examples.adoc#_digression_hiding_columns_in_tables[image:hand.png[] *370*: Digression: Hiding Columns in Tables]
+** xref:adding-further-business-logic-worked-examples.adoc#_another_digression_icons_and_css[image:hand.png[] *380*: Another Digression: Icons and CSS]
+** xref:adding-further-business-logic-worked-examples.adoc#_delete_an_code_owner_code_provided_no_unpaid_code_visit_code_s[image:hand.png[] *390*: Delete an Owner provided no unpaid Visits]
+
+//* xref:incode-platform.adoc[Incode Platform]
+//* xref:i18n.adoc[i18n]
diff --git a/antora/components/tutorials/modules/petclinic/pages/about.adoc b/antora/components/tutorials/modules/petclinic/pages/about.adoc
new file mode 100644
index 0000000..5950d22
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/about.adoc
@@ -0,0 +1,26 @@
+= Apache Isis - A Hands-on Tutorial
+
+:Notice: 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 ag [...]
+
+This tutorial is an introduction to https://isis.apache.org[Apache Isis], and develops a variant of the venerable "pet clinic" demo app.
+
+Each exercise has complete instructions to complete, and/or you can pull down the solutions from the companion github repo:
+
+[source,bash]
+----
+git clone https://github.com/apache/isis-app-demo
+----
+
+In the menu (on the left) you'll see that certain steps have a hand symbol and a number.
+These correspond to tags in the git repo, so you can pick up the tutorial wherever you wish.
+
+
+For more information:
+
+* link:http://isis.apache.org[isis.apache.org]
+
+* link:mailto:dan@haywood-associates.co.uk[dan@haywood-associates.co.uk]
+
+* twitter: @dkhaywood
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/adding-further-business-logic-worked-examples.adoc b/antora/components/tutorials/modules/petclinic/pages/adding-further-business-logic-worked-examples.adoc
new file mode 100644
index 0000000..218fefb
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/adding-further-business-logic-worked-examples.adoc
@@ -0,0 +1,707 @@
+= Adding further business logic - Worked Examples
+
+:Notice: 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 ag [...]
+
+
+Let's remind ourselves of the original use cases we identified; some of these have been implemented already (admittedly, not all with tests around them):
+
+* create an `Owner` : yes, implemented
+
+* add and remove ``Pet``s for said `Owner` : yes, implemented.
+
+* book a `Pet` in for a `Visit`: yes, implemented.
+
+* enter an `outcome` and `cost` of a `Visit`: not yet
+
+* allow an `Owner` to pay for a `Visit`: not yet
+
+* find ``Visit``s not yet paid and overdue (more than 28 days old): not yet
+
+* delete an `Owner` and its ``Pet``s and ``Visit``s, so long as there are no unpaid ``Visit``s: partly.
+We currently just delete everything.
+
+In this section we'll implement the missing functionality, along with unit or integration tests as necessary.
+
+
+== Enter an outcome
+
+An outcome for a `Visit` consists of a diagnosis, and also the cost to be paid by the ``Pet``'s `Owner`.
+
+image::{_imagesdir}/Visit-enterOutcome.png[width="800px",link="_images/Visit-enterOutcome.png"]
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/330-enter-an-outcome
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+* add a new integration test, `Visit_enterOutcome_IntegTest`,
++
+[source,java]
+----
+public class Visit_enterOutcome_IntegTest extends PetClinicModuleIntegTestAbstract {
+
+    Visit visit;
+
+    @Before
+    public void setup() {
+        // given
+        Owner owner = runBuilderScript(Owner_enum.JOHN_SMITH);
+        Pet pet = owner.getPets().first();
+        visit = wrap(mixin(Pet_visits.class, pet)).coll().iterator().next();
+    }
+
+    @Test
+    public void happy_case() {
+
+        // when
+        String diagnosis = someRandomDiagnosis();
+        BigDecimal cost = someRandomCost();
+
+        wrap(visit).enterOutcome(diagnosis, cost);
+
+        // then
+        assertThat(visit.getDiagnosis()).isEqualTo(diagnosis);
+        assertThat(visit.getCost()).isEqualTo(cost);
+    }
+
+    private BigDecimal someRandomCost() {
+        return new BigDecimal(20.00 + fakeDataService.doubles().upTo(30.00d));
+    }
+
+    private String someRandomDiagnosis() {
+        return fakeDataService.lorem().paragraph(3);
+    }
+
+    @Inject
+    FakeDataService fakeDataService;
+}
+----
+
+* in `Visit`, add in the two new properties and action.
++
+[source,java]
+----
+@Action(semantics = SemanticsOf.IDEMPOTENT)
+public Visit enterOutcome(
+        @Parameter(maxLength = 4000)
+        @ParameterLayout(multiLine = 5)
+        final String diagnosis,
+        final BigDecimal cost) {
+    this.diagnosis = diagnosis;
+    this.cost = cost;
+    return this;
+}
+
+@javax.jdo.annotations.Column(allowsNull = "true", length = 4000)
+@Property(editing = Editing.DISABLED, editingDisabledReason = "Use 'enter outcome' action")
+@PropertyLayout(multiLine = 5)
+@Getter @Setter
+private String diagnosis;
+
+@javax.jdo.annotations.Column(allowsNull = "true", length = 6, scale = 2)
+@Property(editing = Editing.DISABLED, editingDisabledReason = "Use 'enter outcome' action")
+@Getter @Setter
+private BigDecimal cost;
+
+----
+
+* update `Visit.layout.xml` for the two new properties and action.
+
+* add in some further integration tests to ensure that the properties cannot be edited directly:
++
+[source,java]
+----
+@Test
+public void cannot_edit_outcome_directly() {
+
+    // expecting
+    expectedExceptions.expect(DisabledException.class);
+    expectedExceptions.expectMessage("Use 'enter outcome' action");
+
+    // when
+    String diagnosis = someRandomDiagnosis();
+    wrap(visit).setDiagnosis(diagnosis);
+}
+
+@Test
+public void cannot_edit_cost_directly() {
+
+    // expecting
+    expectedExceptions.expect(DisabledException.class);
+    expectedExceptions.expectMessage("Use 'enter outcome' action");
+
+    // when
+    BigDecimal cost = someRandomCost();
+
+    wrap(visit).setCost(cost);
+}
+----
+
+
+== Pay for a visit
+
+We'll support this use case through a new action "paid", on the `Visit` domain entity.
+
+To support the testing (and with half an eye to a future use case) we'll also implement a "findNotPaid" query on the `Visits` repository domain service.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/340-pay-for-a-visit
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+Let's first work on the happy case:
+
+* Update `Visit` with a new `paid()` action and `paidOn` property.
+Also inject `ClockService`:
++
+[source,java]
+----
+@Action(semantics = SemanticsOf.IDEMPOTENT)
+public Visit paid() {
+    paidOn = clockService.now();
+    return this;
+}
+
+@javax.jdo.annotations.Column(allowsNull = "true")
+@Property(editing = Editing.DISABLED, editingDisabledReason = "Use 'paid' action")
+@Getter @Setter
+private LocalDate paidOn;
+
+...
+
+@Inject
+ClockService clockService;
+----
+
+* Update the `Visits` domain service repository to find ``Visit``s that haven't been paid:
++
+[source,java]
+----
+@Programmatic
+public java.util.List<Visit> findNotPaid() {
+    TypesafeQuery<Visit> q = isisJdoSupport.newTypesafeQuery(Visit.class);
+    final QVisit cand = QVisit.candidate();
+    q = q.filter(
+            cand.paidOn.eq(q.parameter("paidOn", LocalDateTime.class)
+        )
+    );
+    return q.setParameter("paidOn", null)
+            .executeList();
+}
+----
+
+* Extend `OwnerBuilderScript` so that all but the last `Visit` for each ``Owner``'s ``Pet``s has been paid.
++
+Add some further supporting methods:
++
+[source,java]
+----
+private String someDiagnosis() {
+    return fakeDataService.lorem().paragraph(fakeDataService.ints().between(1, 3));
+}
+
+private BigDecimal someCost() {
+    return new BigDecimal(20.00 + fakeDataService.doubles().upTo(30.00d));
+}
+----
++
+In the `execute(...)`, update the `for` loop so that all ``Visit``s have an outcome and all but the last (for each ``Owner``) has been paid:
++
+[source,java]
+----
+for (int i = 0; i < petDatum.numberOfVisits; i++) {
+    ...
+    LocalDateTime someTimeInPast = ...
+    Visit visit = ...
+    wrap(visit).enterOutcome(someDiagnosis(), someCost());
+    if(i != petDatum.numberOfVisits - 1) {
+        setTimeTo(ec, someTimeInPast.plusDays(fakeDataService.ints().between(10,30)));
+        wrap(visit).paid();
+    }
+}
+----
+
+
+== Prevent payment for a visit twice
+
+We've already seen that it's possible to validate arguments to actions; for example that a `Visit` can only be booked in the future.
+But if a `Visit` has already been paid for, then we don't want the user to be able to even attempt to invoke the action.
+
+The framework provides three different types of pre-condition checks:
+
+* "See it?" - should the action/property be visible at all, or has it been hidden?
+
+* "Use it" - if visible, then can the action/property be used or has it been disabled (greyed out)
+
+* "Do it" - if the action/property is ok to be used (action invoked/property edited) then are the proposed action arguments or new property value valid, or are they invalid?
+
+Or in other words, "see it, use it, do it".
+
+As with validation, disablement can be defined either declaratively (annotations) or imperatively (supporting methods).
+Let's see how an imperative supporting method can be used to implement this particular requirement (that a visit can't be paid for twice).
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/350-prevent-payment-for-a-visit-twice
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+* update `Visit_pay_IntegTest` to ensure cannot enter into the `paidOn` property directly:
++
+[source,java]
+----
+@Test
+public void cannot_edit_paidOn_directly() {
+
+    // expecting
+    expectedExceptions.expect(DisabledException.class);
+    expectedExceptions.expectMessage("Use 'paid on' action");
+
+    // when
+    wrap(visit).setPaidOn(clockService.now());
+}
+----
+
+* now, add in the test that asserts that a `Visit` cannot be paid more than once:
++
+[source,java]
+----
+@Test
+public void cannot_pay_more_than_once() {
+
+    // given
+    wrap(visit).paid();
+    assertThat(visits.findNotPaid()).asList().doesNotContain(visit);
+
+    // expecting
+    expectedExceptions.expect(DisabledException.class);
+    expectedExceptions.expectMessage("Already paid");
+
+    // when
+    wrap(visit).paid();
+}
+----
+
+* and finally update `Visit`.
+This is done using a supporting method.
++
+[source,java]
+----
+public String disablePaid() {
+    return getPaidOn() != null ? "Already paid": null;
+}
+----
+
+
+== Find ``Visit``s not yet paid and overdue
+
+In the previous scenario we implemented `Visits#findNotPaid()`.
+Since this is pretty important information, let's surface that to the end-user by adding it to the home page dashboard.
+
+We could also go a little further by allowing the user to use the dashboard to update visits that have been paid.
+This is a good example of how a view model can support specific business processes, in this case saving the end-user from having to navigate down to each and every one of the ``Visit``s.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/360-find-visits-not-yet-paid-and-overdue
+mvn clean package jetty:run
+----
+
+image::{_imagesdir}/Dashboard-overdue.png[width="800px",link="_images/Dashboard-overdue.png"]
+
+=== Exercise
+
+
+* update `Dashboard`:
++
+[source,java]
+----
+@CollectionLayout(defaultView = "table")
+public List<Visit> getOverdue() {
+    List<Visit> notPaid = visits.findNotPaid();
+    LocalDateTime thirtyDaysAgo = clockService.nowAsLocalDateTime().minusDays(30);
+    return notPaid.stream()
+            .filter(x -> x.getVisitAt().isBefore(thirtyDaysAgo))        // <1>
+            .collect(Collectors.toList());
+}
+
+@Action(semantics = SemanticsOf.IDEMPOTENT, associateWith = "overdue")  // <2>
+public Dashboard paid(List<Visit> visits) {
+    for (Visit visit : visits) {
+        if(visit.getPaidOn() == null) {
+            visit.paid();
+        }
+    }
+    return this;
+}
+
+@javax.inject.Inject
+Visits visits;
+
+@javax.inject.Inject
+ClockService clockService;
+----
+<1> An alternative (better?) design would have been to add a new query method in `Visits` to find those overdue, avoiding the client-side filtering that we see above.
+<2> The "associateWith" annotation results in checkboxes alongside the "overdue" collection, with the collection providing the set of values for the parameter.
+
+
+* update `Dashboard.layout.xml` also
+
+* write a new `Dashboard_paid_IntegTest` integration test:
++
+[source,java]
+----
+public class Dashboard_paid_IntegTest extends PetClinicModuleIntegTestAbstract {
+
+    Dashboard dashboard;
+
+    @Before
+    public void setup() {
+        // given
+        runFixtureScript(new PersonaEnumPersistAll<>(Owner_enum.class));
+        dashboard = homePageProvider.dashboard();
+    }
+
+    @Test
+    public void happy_case() {
+
+        // given
+        List<Visit> overdue = dashboard.getOverdue();
+        assertThat(overdue).isNotEmpty();
+
+        // when
+        wrap(dashboard).paid(overdue);
+
+        // then
+        List<Visit> overdueAfter = dashboard.getOverdue();
+        assertThat(overdueAfter).isEmpty();
+
+        for (Visit visit : overdue) {
+            assertThat(visit.getDiagnosis()).isNotNull();
+            assertThat(visit.getPaidOn()).isNotNull();
+        }
+    }
+
+    @Inject
+    HomePageProvider homePageProvider;
+}
+----
+
+* Running the integration test at this point will produce a null pointer exception.
+That's because the framework has had no opportunity to inject any domain services into the `Dashboard`.
++
+Under normal runtime cases this doesn't matter because the only caller of the method is the framework itself, and when the domain object is rendered the framework will automatically ensure that any domain sevices are injected.
++
+In an integration test this doesn't occur, and so we need to manually inject the services.
+It makes most sense to do this in `HomePageProvider`; we use the framework-provided `ServiceRegistry2` domain service:
++
+[source,java]
+----
+@HomePage
+public Dashboard dashboard() {
+    return serviceRegistry2.injectServicesInto(new Dashboard());
+}
+@Inject
+ServiceRegistry2 serviceRegistry2;
+----
+
+
+
+== Digression: Hiding Columns in Tables
+
+We could improve the dashboard a little.
+After all, in the "overdue" collection there's no point in showing the "paidOn"; the value will always be null.
+Also, the "reason" column is also somewhat superfluous (as, arguably, is the "diagnosis" column):
+
+image::{_imagesdir}/Dashboard-overdue-ui-hints.png[width="800px",link="_images/Dashboard-overdue-ui-hints.png"]
+
+The framework offers two different ways to address this, so we'll show both.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/370-digression-hiding-columns-in-tables
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+* The first technique is within the Java code; one could think of this as an implication within the "application layer".
++
+We use a domain service that implements `TableColumnOrderService` as an SPI to "advise" the framework on how to render the collection.
+Traditionally such classes are implemented as a nested static class, in this case of `Dashboard`:
++
+[source,java]
+----
+@DomainService(nature = NatureOfService.DOMAIN)
+public static class RemovePaidOnFromOverdue extends TableColumnOrderService.Default {
+    @Override
+    public List<String> orderParented(
+            final Object parent,
+            final String collectionId,
+            final Class<?> collectionType,
+            final List<String> propertyIds) {
+        if (parent instanceof Dashboard && "overdue".equalsIgnoreCase(collectionId)) {
+            propertyIds.remove("paidOn");
+        }
+        return propertyIds;
+    }
+}
+----
++
+The above code removes the "paidOn" column.
+
+* The second technique is to exploit the fact that the HTML generated by the framework is liberally annotated with domain class identifiers.
+The column can therefore be removed by supplying the appropriate CSS.
+We could think of this as an implementation within the presentation layer.
++
+In the `src/main/webapp/css/application.css` file, add:
++
+[source,css]
+----
+.domainapp-modules-impl-dashboard-Dashboard .entityCollection .overdue .Visit-reason {
+    display: none;
+}
+----
+
+
+
+== Another Digression: Icons and CSS
+
+In the same way that titles can be specified imperatively, so too can icons, using the `iconName()` method.
+One use case is for a domain object that has several states: the `iconName()` defines a suffix which is used to lookup different icons (eg "ToDoItem-notDone.png" and "ToDoItem-done.png").
+
+Similarly, it's possible to specify CSS hints imperatively using the `cssClass()`.
+This returns a simple string that is added as a CSS class wherever the object is rendered in the UI.
+
+In this exercise we'll use a different icon for the various species of `Pet`:
+
+image::{_imagesdir}/Pet-icons.png[width="800px",link="_images/Pet-icons.png"]
+
+Let's also use a strike-through text for all ``Visit``s that are paid when rendered within a collection:
+
+image::{_imagesdir}/Visits-paid-strikethrough.png[width="800px",link="_images/Visits-paid-strikethrough.png"]
+
+
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/380-another-digression-icons-and-css
+mvn clean package jetty:run
+----
+
+
+
+=== Exercise
+
+For the icons:
+
+* add new icons for each of the pet species: `Pet-dog.png`, `Pet-cat.png`, `Pet-hamster.dog` and `Pet-budgerigar.png`
+
+* add an `iconName()` method to `Pet`:
++
+[source,java]
+----
+public String iconName() {
+    return getPetSpecies().name().toLowerCase();
+}
+----
+
+
+For the CSS class:
+
+* add a `cssClass()` method to `Visit`:
++
+[source,java]
+----
+public String cssClass() {
+    boolean isPaid = getPaidOn() != null;
+    return isPaid ? "paid": null;
+}
+----
+
+
+* update `application.css`:
+
+[source,css]
+----
+.entityCollection .domainapp-modules-impl-visits-dom-Visit .paid {
+    text-decoration: line-through;
+    color: lightgrey;
+}
+----
+
+
+== Delete an `Owner` provided no unpaid ``Visit``s
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/390-delete-an-owner-provided-no-unpaid-visits
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+We don't want `Owner` (in the `pets` module) to check for unpaid ``Visit``s, because that would create a cyclic dependency between modules.
+Instead, we'll use a subscriber in the `visits` module which can veto any attempt to delete an owner if there are unpaid visits.
+
+For this, we arrange for the `Owner` to emit an action domain event when its `delete()` action is invoked.
+In fact, the event will be emitted by the framework up to five times: to check if the action is visible, if it is disabled, if it's valid, pre-execute and post-execute.
+The subscriber in the ``visits`` module will therefore potentially veto on the disable phase.
+
+* in the `Visits` repository, add `findNotPaidBy` method to find any unpaid ``Visit``s for an `Owner`:
++
+[source,java]
+----
+@Programmatic
+public java.util.List<Visit> findNotPaidBy(Owner owner) {
+    TypesafeQuery<Visit> q = isisJdoSupport.newTypesafeQuery(Visit.class);
+    final QVisit cand = QVisit.candidate();
+    q = q.filter(
+            cand.paidOn.eq(q.parameter("paidOn", LocalDateTime.class)
+        ).and(
+                cand.pet.owner.eq(q.parameter("owner", Owner.class))
+            )
+    );
+    return q.setParameter("paidOn", null)
+            .setParameter("owner", owner)
+            .executeList();
+}
+----
+
+* update `Owner`'s `delete()` action so that it emits an action domain event.
++
+[source,java]
+----
+import org.apache.isis.applib.services.eventbus.ActionDomainEvent;
+...
+public static class Delete extends ActionDomainEvent<Owner> {}  // <1>
+@Action(
+        domainEvent = Delete.class                              // <2>
+        semantics = SemanticsOf.NON_IDEMPOTENT                  // <3>
+)
+public void delete() {
+    final String title = titleService.titleOf(this);
+    messageService.informUser(String.format("'%s' deleted", title));
+    repositoryService.removeAndFlush(this);
+}
+----
+<1> declare the event, and
+<2> emit it
+<3> change from `NON_IDEMPOTENT_ARE_YOU_SURE` (due to a bug in the framework).
+
+* add a new integration test:
++
+[source,java]
+----
+public class Owner_delete_IntegTest extends PetClinicModuleIntegTestAbstract {
+
+    @Test
+    public void can_delete_if_there_are_no_unpaid_visits() {
+
+        // given
+        runFixtureScript(Owner_enum.FRED_HUGHES.builder());
+
+        Owner owner = Owner_enum.FRED_HUGHES.findUsing(serviceRegistry);
+        List<Visit> any = visits.findNotPaidBy(owner);
+        assertThat(any).isEmpty();
+
+        // when
+        wrap(owner).delete();
+
+        // then
+        Owner ownerAfter = Owner_enum.FRED_HUGHES.findUsing(serviceRegistry);
+        assertThat(ownerAfter).isNull();
+    }
+
+    @Test
+    public void cannot_delete_with_unpaid_visits() {
+
+        // given
+        runFixtureScript(Owner_enum.MARY_JONES.builder());
+
+        Owner owner = Owner_enum.MARY_JONES.findUsing(serviceRegistry);
+        List<Visit> any = visits.findNotPaidBy(owner);
+        assertThat(any).isNotEmpty();
+
+        // expect
+        expectedExceptions.expect(DisabledException.class);
+        expectedExceptions.expectMessage("This owner still has unpaid visit(s)");
+
+        // when
+        wrap(owner).delete();
+    }
+
+    @Inject
+    Visits visits;
+}
+----
+
+* add the subscriber to veto the action if required:
++
+[source,java]
+----
+@DomainService(nature = NatureOfService.DOMAIN)
+public class VetoDeleteOfOwnerWithUnpaidVisits
+        extends org.apache.isis.applib.AbstractSubscriber {
+
+    @org.axonframework.eventhandling.annotation.EventHandler
+    public void on(Owner.Delete ev) {
+
+        switch (ev.getEventPhase()) {
+        case DISABLE:
+            Collection<Visit> visitsForPet = visits.findNotPaidBy(ev.getSource());
+            if (!visitsForPet.isEmpty()) {
+                ev.veto("This owner still has unpaid visit(s)");
+            }
+            break;
+        }
+    }
+
+    @javax.inject.Inject
+    Visits visits;
+}
+----
+
+* finally, in `PetClinicModuleIntegTestAbstract`, we need to make a small adjustment to use the same event bus implementation as the production app:
++
+[source,java]
+----
+super(new PetClinicModule()
+    .withAdditionalServices(DeploymentCategoryProviderForTesting.class)
+    .withConfigurationProperty("isis.services.eventbus.implementation","axon")      // <1>
+    .withConfigurationProperty(TranslationServicePo.KEY_PO_MODE, "write")
+);
+----
+<1> specify Axon as the event bus implementation
+
+
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/adding-the-remaining-classes.adoc b/antora/components/tutorials/modules/petclinic/pages/adding-the-remaining-classes.adoc
new file mode 100644
index 0000000..5b061d0
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/adding-the-remaining-classes.adoc
@@ -0,0 +1,442 @@
+= Adding the remaining classes
+
+:Notice: 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 ag [...]
+
+
+Right now our domain model still only consists of the single domain class, `Owner`.
+We still have the `Pet` and `Visit` entities to add, along with the `PetSpecies`  enum.
+
+== `newPet` action and `Pet` to `Owner` association
+
+The association from `Owner` to `Pet` is bidirectional.
+Let's start by tackling one side of this, from `Pet` to `Owner`.
+
+* We'll add a new action to create a new `Pet` from an `Owner`:
++
+image::{_imagesdir}/owner-newPet.png[width="400px",link="_images/owner-newPet.png"]
++
+which will prompt for the name and species of the `Pet`:
++
+image::{_imagesdir}/owner-newPet-prompt.png[width="400px",link="_images/owner-newPet-prompt.png"]
+
+* and, when the `Pet` is returned, it will be associated with the `Owner` that created it:
++
+image::{_imagesdir}/Pet.png[width="600px",link="_images/Pet.png"]
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/150-add-pet-1-m-collections
+mvn clean package jetty:run
+----
+
+
+
+=== Exercise
+
+* declare the `PetSpecies` enum:
++
+[source,java]
+----
+public enum PetSpecies {
+    Dog,
+    Cat,
+    Hamster,
+    Budgerigar,
+}
+----
+
+* let's start with an outline of the `Pet` class
++
+[source,java]
+----
+@javax.jdo.annotations.PersistenceCapable(identityType = IdentityType.DATASTORE, schema = "pets" )
+@javax.jdo.annotations.DatastoreIdentity(strategy = IdGeneratorStrategy.IDENTITY, column = "id")
+@javax.jdo.annotations.Version(strategy= VersionStrategy.DATE_TIME, column ="version")
+@DomainObject(auditing = Auditing.ENABLED)
+@DomainObjectLayout()  // causes UI events to be triggered
+public class Pet implements Comparable<Pet> {
+
+}
+----
++
+This won't compile until we have implemented `Comparable<Pet>`.
+
+* let's add in the key fields, `owner` and `name`:
++
+[source,java]
+----
+// ...
+@javax.jdo.annotations.Unique(name="Pet_owner_name_UNQ", members = {"owner","name"})
+// ...
+public class Pet implements Comparable<Pet> {
+
+    public Pet(final Owner owner, final String name) {
+        this.owner = owner;
+        this.name = name;
+    }
+
+    public String title() {
+        return String.format(
+                "%s (%s owned by %s)",
+                getName(), getPetSpecies().name().toLowerCase(), getOwner().getName());
+    }
+
+    @javax.jdo.annotations.Column(allowsNull = "false", name = "ownerId")
+    @Property(editing = Editing.DISABLED)
+    @Getter @Setter
+    private Owner owner;
+
+    @javax.jdo.annotations.Column(allowsNull = "false", length = 40)
+    @Property(editing = Editing.ENABLED)
+    @Getter @Setter
+    private String name;
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+
+    @Override
+    public int compareTo(final Pet other) {
+        return ComparisonChain.start()
+                .compare(this.getOwner(), other.getOwner())
+                .compare(this.getName(), other.getName())
+                .result();
+    }
+}
+----
+
+* let's add in a reference to `PetSpecies`:
++
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "false")
+@Property(editing = Editing.DISABLED)
+@Getter @Setter
+private PetSpecies petSpecies;
+----
++
+Since this is mandatory, we also need to update the constructor:
++
+[source,java]
+----
+// ...
+public Pet(final Owner owner, final String name, final PetSpecies petSpecies) {
+    this.owner = owner;
+    this.name = name;
+    this.petSpecies = petSpecies;
+}
+----
+
+* finally, let's add in `notes` optional property:
++
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "true", length = 4000)
+@Property(editing = Editing.ENABLED)
+@Getter @Setter
+private String notes;
+----
+
+* We also need a `PetLayout.xml` and a `Pet.png`.
+The `.png` files should reside in the same package as the classes.
+
+
+Now we need a way to create ``Pet``s.
+
+We could create a fixture script and an `Pets` domain service. On the other hand, if we consider the use cases we are implementing  we remember that ``Pet``s are owned by ``Owner``s, and so a better design is to make the creation (and removal) of ``Pet``s a responsibility of `Owner`.
+
+Thus:
+
+* add a `newPet` action to `Owner`:
++
+[source,java]
+----
+@Action(semantics = SemanticsOf.NON_IDEMPOTENT)
+public Pet newPet(final String name, final PetSpecies petSpecies) {
+    return repositoryService.persist(new Pet(this, name, petSpecies));
+}
+----
+
+== Collection of ``Pet``s
+
+At this point in our app, although the `Pet` knows its `Owner`, the opposite isn't true.
+
+Our design says we'd like this to be a bidirectional 1-to-many association:
+
+image::{_imagesdir}/Owner-pets.png[width="800px",link="_images/Owner-pets.png"]
+
+Let's add in the `Owner#pets` collection:
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/160-collection-of-pets
+mvn clean package jetty:run
+----
+
+
+
+=== Exercise
+
+* in the `Owner` class, add the `pets` collection:
++
+[source,java]
+----
+@Persistent(
+    mappedBy = "owner",             // <1>
+    dependentElement = "true"       // <2>
+)
+@Collection()
+@Getter @Setter
+private SortedSet<Pet> pets = new TreeSet<Pet>();
+----
+<1> specifies a bidirectional property.
+(`Pet#owner` "points back to" the `Owner`).
+<2> Deleting an `Owner` will also delete any associated ``Pet``s.
++
+* update the `Owner.layout.xml` file to specify the position of the `pets` collection.
+For example:
++
+[source,xml]
+----
+<bs3:tabGroup collapseIfOne="false">
+<bs3:tab name="Details">
+    <bs3:row>
+        <bs3:col span="12">
+            <c:collection id="pets" defaultView="table"/>
+        </bs3:col>
+    </bs3:row>
+</bs3:tab>
+</bs3:tabGroup>
+----
+
+* update the `newPet` action to associate with the `pets` collection:
++
+[source,java]
+----
+@Action(
+    semantics = SemanticsOf.NON_IDEMPOTENT,
+    associateWith = "pets"
+)
+public Pet newPet(final String name, final PetSpecies petSpecies) { ... }
+----
+
+* we could also take the opportunity to add an action to remove a `Pet`:
++
+[source,java]
+----
+@Action(
+    semantics = SemanticsOf.NON_IDEMPOTENT,
+    associateWith = "pets", associateWithSequence = "2"
+)
+public Owner removePet(Pet pet) {
+    repositoryService.removeAndFlush(pet);
+    return this;
+}
+----
+
+When the `removePet` action is invoked, note how the available ``Pet``s is restricted to those in the collection.
+This is due to the `@Action#associateWith` attribute.
+
+
+== Extend our fixture
+
+Before we go any further, let's take some time out to extend our fixture so that each `Owner` also has some ``Pet``s.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/170-extend-our-fixtures
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+* update `RecreateOwners` by adding a `PetData` (Lombok) data class:
+
++
+[source,java]
+----
+@Data
+static class PetData {
+    private final String name;
+    private final PetSpecies petSpecies;
+}
+----
+
+* factor out a `createOwner` helper method:
++
+[source,java]
+----
+private Owner createOwner(
+        final String lastName,
+        final String firstName,
+        final String phoneNumber,
+        final PetData... pets) {
+    Owner owner = this.owners.create(lastName, firstName, phoneNumber);
+    for (PetData pet : pets) {
+        owner.newPet(pet.name, pet.petSpecies);
+    }
+    return owner;
+}
+----
+
+* and update `execute` to use both:
++
+[source,java]
+----
+@Override
+protected void execute(final ExecutionContext ec) {
+
+    isisJdoSupport.deleteAll(Pet.class);
+    isisJdoSupport.deleteAll(Owner.class);
+
+    ec.addResult(this,
+            createOwner("Smith", "John", null,
+                    new PetData("Rover", PetSpecies.Dog))
+    );
+    ec.addResult(this,
+            createOwner("Jones", "Mary", "+353 1 555 1234",
+                    new PetData("Tiddles", PetSpecies.Cat),
+                    new PetData("Harry", PetSpecies.Budgerigar)
+            ));
+    ec.addResult(this,
+            createOwner("Hughes", "Fred", "07777 987654",
+                    new PetData("Jemima", PetSpecies.Hamster)
+            ));
+}
+----
+
+* rename from `RecreateOwners` to `RecreateOwnersAndPets`
+
+
+
+== Adding `Visit`
+
+Our final entity is `Visit`.
+Let's extend our app to allow ``Visit``s to be booked from an ``Owner``'s ``Pet``:
+
+image::{_imagesdir}/Pet-bookVisit-prompt.png[width="800px",link="_images/Pet-bookVisit-prompt.png"]
+
+returning
+
+image::{_imagesdir}/Visit.png[width="800px",link="_images/Visit.png"]
+
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/180-adding-Visit
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+First let's create the `Visit` entity:
+
+* add the outline of `Visit`:
++
+[source,xml]
+----
+@javax.jdo.annotations.PersistenceCapable(identityType = IdentityType.DATASTORE, schema = "visits" )
+@javax.jdo.annotations.DatastoreIdentity(strategy = IdGeneratorStrategy.IDENTITY, column = "id")
+@javax.jdo.annotations.Version(strategy= VersionStrategy.DATE_TIME, column ="version")
+@DomainObject(auditing = Auditing.ENABLED)
+@DomainObjectLayout()  // causes UI events to be triggered
+public class Visit implements Comparable<Visit> {
+
+}
+----
+
+* add the three mandatory properties, `pet`, `visitAt` and `reason`:
++
+[source,xml]
+----
+@javax.jdo.annotations.Column(allowsNull = "false", name = "petId")
+@Property(editing = Editing.DISABLED)
+@Getter @Setter
+private Pet pet;
+
+@javax.jdo.annotations.Column(allowsNull = "false")
+@Property(editing = Editing.DISABLED)
+@Getter @Setter
+private LocalDateTime visitAt;
+
+@javax.jdo.annotations.Column(allowsNull = "false", length = 4000)
+@Property(editing = Editing.ENABLED)
+@PropertyLayout(multiLine = 5)
+@Getter @Setter
+private String reason;
+----
+
+* specify unique constraints and boilerplate for constructors, title, toString and compareTo:
++
+[source,xml]
+----
+@javax.jdo.annotations.Unique(name="Visit_visitAt_pet_UNQ", members = {"visitAt","pet"})
+@javax.jdo.annotations.Index(name="Visit_pet_visitAt_IDX", members = {"pet","visitAt"})
+//...
+public class Visit implements Comparable<Visit> {
+
+    public Visit(final Pet pet, final LocalDateTime visitAt, final String reason) {
+        this.pet = pet;
+        this.visitAt = visitAt;
+        this.reason = reason;
+    }
+
+    public String title() {
+        return String.format(
+                "%s: %s (%s)",
+                getVisitAt().toString("yyyy-MM-dd hh:mm"),
+                getPet().getOwner().getName(),
+                getPet().getName());
+    }
+
+    @Override
+    public String toString() {
+        return getVisitAt().toString("yyyy-MM-dd hh:mm");
+    }
+
+    @Override
+    public int compareTo(final Visit other) {
+        return ComparisonChain.start()
+                .compare(this.getVisitAt(), other.getVisitAt())
+                .compare(this.getPet(), other.getPet())
+                .result();
+    }
+}
+----
+
+* create a `Visit.layout.xml` layout file
+
+We also need the ability to book a `Visit` (ie create a new `Visit` entity instance).
+We'll make this a responsibility of `Pet` for now (we can always refactor later if we find a better place to do this):
+
+* add the following action to `Pet`:
++
+[source,java]
+----
+@Action(semantics = SemanticsOf.NON_IDEMPOTENT)
+public Visit bookVisit(
+        final LocalDateTime at,
+        @Parameter(maxLength = 4000)
+        @ParameterLayout(multiLine = 5)
+        final String reason) {
+    return repositoryService.persist(new Visit(this, at, reason));
+}
+
+@javax.jdo.annotations.NotPersistent
+@javax.inject.Inject
+RepositoryService repositoryService;
+----
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/an-example-domain.adoc b/antora/components/tutorials/modules/petclinic/pages/an-example-domain.adoc
new file mode 100644
index 0000000..2dcc942
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/an-example-domain.adoc
@@ -0,0 +1,126 @@
+:Notice: 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 ag [...]
+
+= An Example Domain
+
+By now you should understand the basics of what Apache Isis does, but there's only so much we can learn from a one domain class.
+Let's therefore evolve the app into a slightly more interesting domain and explore other features of the framework.
+
+The domain we're going to work on is a version of the venerable "Pet Clinic" app.
+Here's a sketch of (our version of) its domain:
+
+[plantuml]
+----
+hide empty members
+hide methods
+
+skinparam class {
+	BackgroundColor<<desc>> Cyan
+	BackgroundColor<<ppt>> LightGreen
+	BackgroundColor<<mi>> LightPink
+	BackgroundColor<<role>> LightYellow
+}
+
+package pets {
+
+    enum PetSpecies <<desc>> {
+        Dog
+        Cat
+        Hamster
+        Budgerigar
+    }
+
+    class Pet <<ppt>> {
+        +id
+        ..
+        #owner
+        #name
+        ..
+        -species
+        -notes
+    }
+
+
+    class Owner <<role>> {
+        +id
+        ..
+        #lastName
+        #firstName
+        ..
+        -phoneNumber
+        -emailAddress
+    }
+}
+
+
+package visits {
+
+    class Visit <<mi>> {
+        +id
+        ..
+        #pet
+        #visitAt: LocalDateTime
+        ..
+        -reason
+        ..
+        -cost
+        -paid: boolean
+        -outcome
+    }
+}
+
+
+Owner *-down-> "0..*" Pet
+Visit "   \n*" -up->  Pet
+Pet  "*" -right--> PetSpecies
+----
+
+[TIP]
+====
+The colours used are from "Java: Modeling in Color" book by Coad et al.
+====
+
+
+Some of the use cases we might want to support include:
+
+* create an `Owner`
+* add and remove ``Pet``s for said `Owner`
+* book a `Pet` in for a `Visit`
+* enter an `outcome` and `cost` of a `Visit`
+* allow an `Owner` to pay for a `Visit`
+* find ``Visit``s not yet paid and overdue
+* delete an `Owner` and its ``Pet``s and ``Visit``s, so long as there are no unpaid ``Visit``s.
+
+This tutorial has worked solutions for all of these.
+
+== Rename `HelloWorldObject` to `Owner`
+
+To start with, let's rename the `HelloWorldObject` entity to `Owner`
+You can either make the changes yourself, or just checkout the next git tag checkpoint
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/020-rename-HelloWorldObject-to-Owner
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* the changes for `HelloWorldObject` entity are:
+
+** rename class `HelloWorldObject` -> `Owner`
+** rename file `HelloWorldObject.layout.xml` -> `Owner.layout.xml`
+** rename file `HelloWorldObject.png` -> `Owner.png`
+*** or replace with another more suitable icon
+** rename class `HelloWorldObjectTest_updateName` -> `OwnerTest_updateName`
+** rename class `HelloWorldObjectTest_delete` -> `OwnerTest_delete`
+
+* for the `HelloWorldObjects` domain service:
+
+** rename class `HelloWorldObjects` -> `Owners`
+** update file `menubars.layout.xml`
+
+Run the application and make sure it still runs fine.
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/business-rules-and-unit-testing.adoc b/antora/components/tutorials/modules/petclinic/pages/business-rules-and-unit-testing.adoc
new file mode 100644
index 0000000..1813a66
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/business-rules-and-unit-testing.adoc
@@ -0,0 +1,153 @@
+= Business Rules & (Unit) Testing
+
+:Notice: 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 ag [...]
+
+Domain driven design is intended for complex business domains, and so testing is obviously important.
+In this part of the tutorial we'll cover unit testing, later on we'll look at integration testing.
+
+
+== Defaults, and `ClockService`
+
+By way of motivation, let's consider a small enhancement we could make to the app.
+For example, it would improve the usability if the app automatically suggesting a time for a new `Visit` that was in the future (say tomorrow, at 9am):
+
+image::{_imagesdir}/Pet-bookVisit-prompt-with-default.png[width="800px",link="_images/Pet-bookVisit-prompt-with-default.png"]
+
+[NOTE]
+====
+Actually, the design of this app is probably all wrong.
+Rather than choosing some arbitrary time in the future for a visit, more likely there would be a number of pre-defined "appointment slots".
+
+One of the strengths of the framework is to allow the development team to uncover these missing concepts as quickly as possible.
+It also means we are able to "let go" of bad ideas (we become less emotionally attached to them).
+====
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/190-defaults-and-clockservice
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+The default is specified using a supporting method:
+
+[source,java]
+----
+public LocalDateTime default0BookVisit() {
+    return clockService.now()
+                .plusDays(1)
+                .toDateTimeAtStartOfDay()
+                .toLocalDateTime()
+                .plusHours(9);
+}
+
+@javax.jdo.annotations.NotPersistent
+@javax.inject.Inject
+ClockService clockService;
+----
+
+The name of this supporting method is "default" + "paramNum" + "actionName".
+
+The (framework provided) `ClockService` provides the current time.
+Why do this (rather than simply instantiating `LocalDateTime`?)
+We'll see why in the next session.
+
+
+== Unit tests
+
+Let's now write a unit test to safeguard the logic to calculate the default time for the visit.
+ Now we see the reason why we use a domain service to obtain the time; it allows us to "mock the clock".
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/200-unit-tests
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+The Apache Isis framework provides some extensions to link:http://jmock.org[JMock] to make writing unit tests really simple.
+
+[source,java]
+----
+public class Pet_bookVisit_Test {
+
+    @Rule
+    public JUnitRuleMockery2 context =                                  // <1>
+        JUnitRuleMockery2.createFor(JUnitRuleMockery2.Mode.INTERFACES_AND_CLASSES);
+
+    @Mock                                                               // <2>
+    ClockService mockClockService;
+
+    @Test
+    public void default0BookVisit() {
+
+        // given
+        Pet pet = new Pet(null, null, null);
+        pet.clockService = mockClockService;                            // <3>
+
+        // expecting
+        context.checking(new Expectations() {{                          // <4>
+            allowing(mockClockService).now();
+            // 3-Mar-2018, 14:10
+            will(returnValue(new LocalDate(2018,3,3)));
+        }});
+
+        // when
+        LocalDateTime actual = pet.default0BookVisit();
+
+        // then
+        assertThat(actual).isEqualTo(new LocalDateTime(2018,3,4,9,0));  // <5>
+    }
+}
+----
+<1> to set up expectations on mocks.
+All configured expectations are also automatically verified.
+<2> automatically instantiated by JMock
+<3> inject the mock clock into the domain object
+<4> set up expectation on the mock clock
+<5> use link:http://joel-costigliola.github.io/assertj/[AssertJ] to assert the expected value
+
+
+
+== Validation
+
+It doesn't really make sense to book a visit in the past.
+Let's fix that with some validation:
+s
+image::{_imagesdir}/Pet-bookVisit-prompt-with-validate.png[width="800px",link="_images/Pet-bookVisit-prompt-with-validate.png"]
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/210-validation
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+The validation rule is implemented using a supporting method (though a specification could also have been used):
+
+[source,java]
+----
+public String validate0BookVisit(final LocalDateTime proposed) {
+    return proposed.isBefore(clockService.nowAsLocalDateTime())
+            ? "Cannot enter date in the past"
+            : null;
+}
+----
+
+The name of this supporting method is "validate" + "paramNum" + "actionName".
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/fleshing-out-the-owner-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/fleshing-out-the-owner-entity.adoc
new file mode 100644
index 0000000..12275d5
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/fleshing-out-the-owner-entity.adoc
@@ -0,0 +1,513 @@
+= Fleshing out the `Owner` entity
+
+:Notice: 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 ag [...]
+
+
+== Rework ``Owner``'s name (`firstName` and `lastName`)
+
+In the domain we are working on, `Owner` has a `firstName` and a `lastName` property, not a single `name` property.
+Let's update `Owner` accordingly.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/070-rework-Owners-name-firstName-and-lastName
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* rename property `Owner#name` -> `Owner#lastName`
+* add property `Owner#firstName`:
++
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "false", length = 40)
+@Property
+@Getter @Setter
+private String firstName;
+----
+
+* update the constructor:
++
+[source,java]
+----
+public Owner(final String lastName, final String firstName) {
+    this.lastName = lastName;
+    this.firstName = firstName;
+}
+----
+
+* remove `@Title` annotation from `lastName` property, and add a `title()` method to derive from both properties:
++
+[source,java]
+----
+public String title() {
+    return getLastName() + ", " + getFirstName().substring(0,1);
+}
+----
+
+
+* update `Owner.layout.xml`
++
+[source,xml]
+----
+<c:property id="lastName" namedEscaped="true"/>
+<c:property id="firstName" namedEscaped="true">
+    <c:action id="updateName">
+        <c:describedAs>Updates the object's name</c:describedAs>
+    </c:action>
+</c:property>
+----
++
+This will place the button for `updateName` underneath the `firstName` property.
+
+* update the corresponding JDO `@Unique` annotation for `Owner`:
++
+[source,java]
+----
+@javax.jdo.annotations.Unique(name="Owner_lastName_firstName_UNQ", members = {"lastName", "firstName"})
+----
+
+* update the implementation of `Comparable` for `Owner`:
++
+[source,java]
+----
+@Override
+public int compareTo(final Owner other) {
+    return ComparisonChain.start()
+            .compare(this.getLastName(), other.getLastName())
+            .compare(this.getFirstName(), other.getFirstName())
+            .result();
+}
+----
++
+[NOTE]
+====
+The archetype includes a dependency on guava, which is where `ComparisonChain` comes from.
+====
+
+
+* update `Owner#updateName` to also accept a new `firstName` parameter:
++
+[source,java]
+----
+@Action(semantics = SemanticsOf.IDEMPOTENT, command = CommandReification.ENABLED, publishing = Publishing.ENABLED)
+public Owner updateName(
+        @Parameter(maxLength = 40)
+        final String lastName,
+        @Parameter(maxLength = 40)
+        final String firstName) {
+    setLastName(lastName);
+    setFirstName(firstName);
+    return this;
+}
+public String default0UpdateName() {
+    return getLastName();
+}
+public String default1UpdateName() {
+    return getFirstName();
+}
+----
++
+The "default" supporting methods are called when the action prompt is rendered, providing the default for the "Nth" parameter of the corresponding action.
+
+
+* update `Orders#create`:
++
+[source,java]
+----
+@Action(semantics = SemanticsOf.NON_IDEMPOTENT)
+@MemberOrder(sequence = "1")
+public Owner create(
+        @Parameter(maxLength = 40)
+        final String lastName,
+        @Parameter(maxLength = 40)
+        final String firstName) {
+    return repositoryService.persist(new Owner(lastName, firstName));
+}
+----
+
+* update `Orders#findByName` to search across either the `firstName` or the `lastName`:
++
+[source,java]
+----
+@Action(semantics = SemanticsOf.SAFE)
+@MemberOrder(sequence = "2")
+public List<Owner> findByName(
+        final String name) {
+    TypesafeQuery<Owner> q = isisJdoSupport.newTypesafeQuery(Owner.class);
+    final QOwner cand = QOwner.candidate();
+    q = q.filter(
+            cand.lastName.indexOf(q.stringParameter("name")).ne(-1).or(
+            cand.firstName.indexOf(q.stringParameter("name")).ne(-1)
+            )
+    );
+    return q.setParameter("name", name)
+            .executeList();
+}
+----
+
+* update test class `OwnerTest_updateName`
+* update test class `OwnerTest_delete`
+
+
+
+== Derived name property
+
+The ``Owner``'s `firstName` and `lastName` properties are updated using the `updateName` action, but when the action's button is invoked, it only "replaces" the `firstName` property:
+
+image::{_imagesdir}/Owner-updateName-prompt.png[width="400px",link="_images/Owner-updateName-prompt.png"]
+
+We can improve this by introducing a derived `name` property and then hiding the `firstName` and `lastName`:
+
+image::{_imagesdir}/Owner-name.png[width="400px",link="_images/Owner-name.png"]
+
+And, when `Owner#updateName` is invoked, the prompt we'll see is:
+
+image::{_imagesdir}/Owner-name-updated.png[width="400px",link="_images/Owner-name-updated.png"]
+
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/080-derived-name-property
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* Add `getName()` as the derived `name` property:
++
+[source,java]
+----
+@Property(notPersisted = true)
+public String getName() {
+    return getFirstName() + " " + getLastName();
+}
+----
+
+* Hide the `firstName` and `lastName` properties, using `@Property(hidden=...)`:
++
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "false", length = 40)
+@Property(hidden = Where.EVERYWHERE)
+@Getter @Setter
+private String lastName;
+----
+
+
+* Update the `Owner.layout.xml` layout file:
++
+[source,xml]
+----
+<c:property id="name" namedEscaped="true">
+    <c:action id="updateName">
+        <c:describedAs>Updates the object's name</c:describedAs>
+    </c:action>
+</c:property>
+----
+
+
+== Digression: Changing the App Name
+
+Let's remove the remaining vestiges of the "hello world" archetype, and rename our application to "pet clinic".
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/090-digression-changing-the-app-name
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+Rename:
+
+* `HelloWorldModule` -> `PetClinicModule`
+
+* `HelloWorldApplication` -> `PetClinicApplication`
+
+** Also update the string literals in `newIsisWicketModule()` method
+** Also update the reference to the application class in `web.xml`.
+
+* `HelloWorldAppManifest` -> `PetClinicAppManifest`
+
+** Also update `isis.appManifest` property in the `isis.properties` file
+
+* various text references to "Hello World" or "HelloWorld" literals in `pom.xml`, `index.html` and `welcome.html` files
+
+
+== Changing the "Object Type" Class Alias
+
+The Apache Isis framework allows an optional alias to be specified for each domain class; this is called the "objectType".
+This crops up in various places, including the `menubars.layout.xml`, and the REST API.
+It can used when persisting data, eg to hold a reference to an arbitrary domain object (a "polymorphic association").
+
+It's good practice to specify an object type, because it makes refactoring easier if we subsequently move the class to another package, or rename it.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/100-changing-the-object-type-class-alias
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* Update `Owners` domain service.
+The object type alias is specified in `@DomainService(objectType=...)`:
++
+[source,java]
+----
+@DomainService(
+        nature = NatureOfService.VIEW_MENU_ONLY,
+        objectType = "pets.Owners"
+)
+public class Owners { ... }
+----
+
+* Update `Owner` domain entity.
+The object type alias is derived from the database schema and the class's (simple) name:
++
+[source,java]
+----
+@javax.jdo.annotations.PersistenceCapable(identityType = IdentityType.DATASTORE, schema = "pets" )
+...
+public class Owner implements Comparable<Owner> { ... }
+----
+
+* also, update `menubars.layout.xml`, changing "myapp.Owners" to "pets.Owners".
+
+
+== Add other properties for `Owner`
+
+Let's add the two remaining properties for `Owner`.
+
+[plantuml]
+----
+hide empty members
+hide methods
+
+class Owner {
+    +id
+    ..
+    #lastName
+    #firstName
+    ..
+    -phoneNumber
+    -emailAddress
+}
+----
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/110-add-other-properties-for-Owner
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* Add `phoneNumber` as an editable property and use a regex to limit the allowed characters:
++
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "true", length = 15)
+@Property(
+        editing = Editing.ENABLED,
+        regexPattern = "[+]?[0-9 ]+",
+        regexPatternReplacement =
+            "Specify only numbers and spaces, optionally prefixed with '+'.  " +
+            "For example, '+353 1 555 1234', or '07123 456789'"
+)
+@Getter @Setter
+private String phoneNumber;
+----
++
+Until we update `Owner.layout.xml`, then the new property will be added to the section specified `unreferencedProperties="true"`, in other words a field set called "Other".
+
+* Add `emailAddress` as an editable property and use a supporting `validate` method to verify the format:
++
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "true", length = 50)
+@Property(editing = Editing.ENABLED)
+@Getter @Setter
+private String emailAddress;
+public String validateEmailAddress(String emailAddress) {
+    return emailAddress.contains("@") ? null : "Email address must contain a '@'";
+}
+----
++
+Obviously in this previous case we could also have used a declarative approach, but using a "validate" method here shows that arbitrary logic can be used.
+For example, we could delegate to an injected domain service to actually validate the email.
+
+* update `Owner.layout.xml`.
++
+While we are at it, we could move the `notes` property to its own tab:
++
+[source,xml]
+----
+<bs3:tab name="Contact Details">
+    <bs3:row>
+        <bs3:col span="12">
+            <c:fieldSet name="Contact Details">
+                <c:property id="emailAddress"/>
+                <c:property id="phoneNumber"/>
+            </c:fieldSet>
+        </bs3:col>
+    </bs3:row>
+</bs3:tab>
+<bs3:tab name="Notes">
+    <bs3:row>
+        <bs3:col span="12">
+            <c:fieldSet name="Notes">
+                <c:property id="notes" namedEscaped="true" multiLine="10" hidden="ALL_TABLES"/>
+            </c:fieldSet>
+        </bs3:col>
+    </bs3:row>
+</bs3:tab>
+----
++
+resulting in:
+
+image::{_imagesdir}/Owner-with-contact-details.png[width="600px",link="_images/Owner-with-contact-details.png"]
+
+
+== Using specifications to encapsulate business rules
+
+When we create a new `Owner` we specify only the first and last name.
+If the owner has a phone number, then the user has to edit that property separately.
+
+Suppose we wanted to allow the user to optionally enter the phone number when the `Owner` is first created?
+That would require extending the `Owners#create(...)` action to also accept an optional "phoneNumber" parameter.
+
+However, the regex validation rule that we've specified on `Owner#phoneNmber` will need duplicating for the `phoneNumber` parameter; the framework doesn't "know" that the value is to be used to populate that particular property.
+But duplicating validation violates the single responsibility principle.
+
+Instead, we can move the validation logic into a "specification", and associate both the property and the parameter with that specification.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/120-using-specifications-to-encapsulate-business-rules
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* factor out a `PhoneNumberSpec`:
++
+[source,java]
+----
+public static class PhoneNumberSpec extends AbstractSpecification<String> {
+    @Override
+    public String satisfiesSafely(final String phoneNumber) {
+        Matcher matcher = Pattern.compile("[+]?[0-9 ]+").matcher(phoneNumber);
+        return matcher.matches() ? null :
+                "Specify only numbers and spaces, optionally prefixed with '+'.  " +
+                "For example, '+353 1 555 1234', or '07123 456789'";
+    }
+}
+----
++
+[TIP]
+====
+In this case it isn't required, but we could if we wanted inject domain services into this specification class.
+====
+
+* refactor the `phoneNumber` property to use this spec:
++
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "true", length = 15)
+@Property(editing = Editing.ENABLED,
+        mustSatisfy = PhoneNumberSpec.class
+)
+@Getter @Setter
+private String phoneNumber;
+----
+
+* extend the `Orders#create` action to also extend a `phoneNumber` parameter, and use the `PhoneNumberSpec` to implement the same business rule:
++
+image::{_imagesdir}/Owners-create-with-phoneNumber.png[width="400px",link="_images/Owners-create-with-phoneNumber.png"]
++
+using this code:
++
+[source,java]
+----
+@Action(semantics = SemanticsOf.NON_IDEMPOTENT)
+@MemberOrder(sequence = "1")
+public Owner create(
+        @Parameter(maxLength = 40)
+        final String lastName,
+        @Parameter(maxLength = 40)
+        final String firstName,
+        @Parameter(
+                mustSatisfy = Owner.PhoneNumberSpec.class,
+                maxLength = 15,
+                optionality = Optionality.OPTIONAL
+        )
+        final String phoneNumber) {
+    Owner owner = new Owner(lastName, firstName);
+    owner.setPhoneNumber(phoneNumber);
+    return repositoryService.persist(owner);
+}
+----
+
+
+[NOTE]
+====
+The above refactoring isn't perfect: there is still some repetition of the length of the property/parameter, for example.
+
+The next version of the framework will support custom meta-annotations which will address this.
+Then, you'll be able to write:
+
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "true", length = 15)
+@Property(
+    mustSatisfy = Owner.PhoneNumberSpec.class
+)
+@Parameter(
+    mustSatisfy = Owner.PhoneNumberSpec.class,
+    maxLength = 15,
+    optionality = Optionality.OPTIONAL
+)
+public @interace @PhoneNumber {}
+----
+
+and then:
+
+[source,java]
+----
+@PhoneNumber
+@Getter @Setter
+private String phoneNumber;
+----
+
+and
+
+[source,java]
+----
+public Owner create(
+        final String lastName,
+        final String firstName,
+        @PhoneNumber
+        final String phoneNumber) { ... }
+----
+
+====
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/getting-started.adoc b/antora/components/tutorials/modules/petclinic/pages/getting-started.adoc
new file mode 100644
index 0000000..a763e20
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/getting-started.adoc
@@ -0,0 +1,363 @@
+= Getting Started
+
+:Notice: 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 ag [...]
+
+== Prereqs
+
+You'll need:
+
+* Java 8
++
+Apache Isis hasn't yet been tested for later versions of Java; the next major version of the framework will target Java 11.
+
+* Maven 3.x
++
+Maven 3.5.0 or later is recommended (for its CI/CD support), but you'll get by okay with earlier versions.
+
+Also recommended:
+
+* a Java IDE with support for Maven.
++
+The Apache Isis website has detailed documentation for using IntelliJ and Eclipse.
+
+* git
++
+The tutorial includes worked example/solution, provided in a github-hosted repo.
+This has multiple tags for the various checkpoints.
+If you want to work from the solution, you'll need git to pull down the version.
+
+
+== Generate
+
+Use the "Hello World" Maven archetype to generate a running app (adjust `groupId` and `artifactId` as necessary):
+
+[source,bash]
+----
+mvn archetype:generate  \
+    -D archetypeGroupId=org.apache.isis.archetype \
+    -D archetypeArtifactId=helloworld-archetype \
+    -D archetypeVersion=1.16.2 \
+    -D groupId=com.mycompany \
+    -D artifactId=myapp \
+    -D version=1.0-SNAPSHOT \
+    -B
+
+cd myapp
+----
+
+Build and run:
+
+[source,bash]
+----
+mvn clean package jetty:run
+----
+
+and then browse to http://localhost:8080[]:
+
+image::{_imagesdir}/index.png[width="800px",link="_images/index.png"]
+
+
+
+== Explore the Generated App
+
+Although there's the archetype doesn't generated much code, there's still plenty to explore.
+
+=== Wicket viewer
+
+Follow the "wicket" link and logon using `sven/pass`.
+
+image::{_imagesdir}/home-page.png[width="800px",link="_images/home-page.png"]
+
+Use the "Hello World Objects" menu:
+
+image::{_imagesdir}/hello-world-objects-menu.png[width="300px",link="_images/hello-world-objects-menu.png"]
+
+to:
+
+
+* create new objects
+* search by name
+* list all objects
++
+Note that "list all" is in italics.
+That's because this is a so-called "prototype" action; it's only available when running in prototype (development) mode.
+
+On an object:
+
+image::{_imagesdir}/object-a.png[width="800px",link="_images/object-a.png"]
+
+perform the following:
+
+* update its name
+* delete the object
+* create two objects with the same name
+
+Use the "Prototyping" menu:
+
+image::{_imagesdir}/prototyping-menu.png[width="300px",link="_images/prototyping-menu.png"]
+
+to access various built-in functionality.
+For example, use "HSQL DB Manager" to launch a Swing UI to browse the backend database (we use HSQL DB running in-memory when prototyping).
+Note that we'll be exploring the Swagger UI/REST API) below.
+
+Use the tertiary menu (with the user's name) to view the application's configuration settings and to logout:
+
+image::{_imagesdir}/tertiary-menu.png[width="250px",link="_images/tertiary-menu.png"]
+
+
+Use the bookmarks page (top left strip or `alt-[` ) to access previously visited domain objects.
+Use the drop-down menu on the bottom footer as an alternative mechanism.
+
+
+=== Restful Objects (Swagger)
+
+Navigate back to http://localhost:8080[], and follow the "swagger-ui" link.
+Enter `sven/pass` and "prototyping", then reload:
+
+image::{_imagesdir}/swagger-ui.png[width="800px",link="_images/swagger-ui.png"]
+
+Use the Swagger UI to interact with the domain objects previously created in the Wicket viewer.
+
+For example, follow link:http://localhost:8080/swagger-ui/index.html#!/myapp/get_services_myapp_HelloWorldObjects_actions_listAll_invoke[/services/myapp.HelloWorldObjects/actions/listAll/invoke] to list all domain objects.
+
+Use the "Response Content Type" drop-down to obtain different representations of the list.
+
+
+== Pull down github example/solution
+
+This tutorial has an accompanying github repo with multiple tags for the various checkpoints; the first tag is immediately after creating the archetype.
+We recommend that you pull this down so that - even if you code up all the steps yourself - you can easily get back to a working application if needs be.
+
+To pull down the example:
+
+[source,bash]
+----
+git clone https://github.com/danhaywood/isis-petclinic-tutorial
+----
+
+and then checkout the first tag:
+
+[source,bash]
+----
+git checkout tags/010-pull-down-github-example-solution
+mvn clean package jetty:run
+----
+
+You can now run the app.
+If necessary, switch into the "myapp" directory:
+
+[source,bash]
+----
+cd myapp
+----
+
+and run using:
+
+[source,bash]
+----
+mvn clean package jetty:run
+----
+
+
+== Set up your dev env
+
+Rather than running and editing from the Maven command line, we recommend that you load the application into an IDE and run from there.
+
+[TIP]
+====
+Admittedly, there is 5 or 10 minutes of setup required here.
+
+If you are short of time then you might want to skip this and instead just run the solutions by checking out the various tags and run using "mvn clean package jetty:run".
+
+Do though use an an editor that lets you easily locate files in the filesystem.
+====
+
+
+=== Loading the project
+
+Assuming that you _are_ going to use a mainstream IDE, the first bit is to load the files into the IDE.
+All of the mainstream IDEs make this easy to do: generally open Maven projects just by navigating to the `pom.xml`.
+For example, here's the generated app loaded into IntelliJ:
+
+image::{_imagesdir}/project-loaded-into-intellij.png[width="300px",link="_images/project-loaded-into-intellij.png"]
+
+=== Compiling the app
+
+DataNucleus includes an annotation processor, so make sure that annotation processing is enabled on the Maven project.
+
+The other very important thing you do need to know is that Apache Isis leverages DataNucleus for its ORM, and this uses bytecode enhancement rather than runtime proxies (it being an implementation of the JDO API as well as JPA).
+The DataNucleus bytecode enhancer runs after the compiler but (of course) before the app runs.
+
+* When using the Maven command line, the `datanucleus-maven-plugin` is bound to the `postCompile` phase.
+So, a simple "mvn package jetty:run" suffices to build the code, run the enhancer and to run the actual app.
+
+* When compiling with IntelliJ, there's nothing specific we need to configure to ensure that domain entity clases are enhanced.
+Because IntelliJ "watches" the filesystem for external changes, we can simply leverage Maven to perform the enhancement just prior to running the app.
+More on this in the next section.
+
+* When building in Eclipse, it is necessary to hook into the compile phase to ensure that the enhancement occurs; this is done by installing an Eclipse plugin which runs the datanucleus enhancer.
++
+This is necessary because Eclipse -- unlike IntelliJ -- isn't designed to continually watch the filesystem; any changes to the class files must be made through its "internal" processes.
++
+Nevertheless, datanucleus's Eclipse plugin generally works as well as the Maven plugin.
+
+For more info, see the Apache Isis developers' guide for more detailed instructions when using http://isis.apache.org/guides/dg/dg.html#_dg_ide_intellij[IntelliJ] or
+http://localhost:4000/guides/dg/dg.html#_dg_ide_eclipse[Eclipse].
+
+=== Running the app
+
+When running from the IDE, it's easiest to run using framework-provided bootstrap class, namely `org.apache.isis.WebServer`.
+This is just a regular application class (with a `main(...)` method) that uses Jetty to run the app (similar to the way in which Spring Boot works, for example).
+
+If using IntelliJ, we've found it easiest to set up a _run configuration_ with the "Before launch, run Maven goal" property set up to run the datanucleus enhancer:
+
+image::{_imagesdir}/intellij-run-configuration.png[width="800px",link="_images/intellij-run-configuration.png"]
+
+with
+
+image::{_imagesdir}/intellij-run-configuration-before-launch-datanucleus-enhance.png[width="400px",link="_images/intellij-run-configuration-before-launch-datanucleus-enhance.png"]
+
+The command being run here (in the appropriate directory) is simply:
+
+[source,bash]
+----
+mvn datanucleus:enhance -o
+----
+
+
+
+Alternatively the app can be deployed to an app server such as Tomcat (8.x); all the usual files in `src/main/webapp` are there.
+
+
+
+== Naked Objects pattern
+
+Apache Isis is an implementation of the _naked objects pattern_.
+This means that there's a direct mapping from the domain object model into the UI.
+We can explore this by looking at the domain entity and domain service generated by the archetype.
+
+[TIP]
+====
+An ORM such as DataNucleus or Hibernate maps domain objects into an RDBMS or other datastore.
+You can think of Apache Isis (and naked objects) similarly, but it's an OIM - an object _interface_ mapper.
+It maps to the UI layer rather than the persistence layer.
+
+Common to both ORMs and OIMs is an internal metamodel; this is where much of the power comes from.
+====
+
+
+Navigate to `HelloWorldObjects`; this is a singleton domain service automatically instantiated by the framework.
+It corresponds to the "Hello World Objects" menu.
+
+The menu items correspond to the actions of this class:
+
+image::{_imagesdir}/HelloWorldObjects.png[width="800px",link="_images/HelloWorldObjects.png"]
+
+
+The actions of that service are used to create and persist instances of `HelloWorldObject`.
+The structure and behaviour of this domain entity are similarly reflected:
+
+image::{_imagesdir}/HelloWorldObject.png[width="800px",link="_images/HelloWorldObject.png"]
+
+It's common for each entity (or more precisely, aggregate root) to have a corresponding domain service, acting as its repository.
+This abstraction hides the details of interacting with the persistence data store.
+Domain services are automatically injected wherever they are required, using `@javax.inject.Inject`.
+
+Apache Isis applications therefore generally follow the _hexagonal architecture_ (aka the _ports and adapters_ architecture).
+
+As well as writing our own domain services, there are also many framework-provided domain services, including `RepositoryService` (to persist objects) and `IsisJdoSupport` (for type-safe queries against the database).
+But this is just the tip of the iceberg; see Apache Isis documentation (specifically: link:http://isis.apache.org/guides/rgsvc/rgsvc.html[Reference Guide: Domain Services]) for the full list.
+
+If there is more than one implementation available, declare a `List<SomeDomainService>` and all of the available services will be injected.
+They are sorted by `@DomainService#menuOrder` attribute.
+
+It's also possible to override/decorate any of the framework-provided services; just implement the same type with a lower `@DomainService#menuOrder`.
+
+Domain services also act as extension points for the framework.
+For example, custom auditing and event publishing can be provided by providing an implementation of the appropriate SPI interface.
+
+
+== UI Hints
+
+The framework derives as much of the UI as possible from the domain objects' intrinsic structure and behaviour, but there are some supporting structures and conventions that are there primarily to improve the UI.
+
+
+=== Titles
+
+A title is the identifier of a domain object for the end-user.
+
+For `HelloWorldObject`, this is defined declaratively:
+
+[source,java]
+----
+@Title(prepend = "Object: ")
+private String name;
+----
+
+It can also be specified imperatively using either the `title()` or `toString()` method.
+
+*Mini-Exercise*:
+
+(No solution is provided for this exercise).
+
+* replace the `@Title` annotation with a `title()` method:
++
+[source,java]
+----
+public String title() {
+    return "Object: " + getName();
+}
+----
+
+
+
+See link:http://isis.apache.org/guides/rgcms/rgcms.html#_rgcms_methods_reserved_title[Reference Guide: Classes, Methods and Schema] for more details.
+
+=== Object layout
+
+Frameworks that implement the _naked objects pattern_ automatically provide a default representation of domain objects.
+In many cases the details of that representation can be inferred directly from the domain members.
+For example the label of a field for an object's property (eg `HelloWorldObject#name`) can be derived directly from the name of the object property itself (`getName()`).
+
+In the absence of other metadata, Apache Isis will render a domain object with the properties to the left-hand side and the collections (if any) to the right.
+The order of these properties can be specified using the `@MemberOrder` annotation, and there are other annotations to group properties together and to associate action buttons with either properties or collections.
+
+The downside of using annotations is that changing the layout requires that the application be restarted, and certain more complex UIs, such as multi-columns or tab groups are difficult or impossible to express.
+
+Therefore Apache Isis also allows the layout of domain objects to be specified using a complementary layout file, eg `HelloWorldObject.layout.xml`.
+This is modelled upon bootstrap and so supports arbitrary rows and columns as well as tab groups and tabs.
+
+*Mini-Exercise*:
+
+* locate the `HelloWorldObject.layout.xml` file
+* compare the structure of the layout file to that of the rendered object
+* change the file, eg the relative widths of the columns
+* use the IDE to copy over the file to the classpath; the new version will be picked up automatically
+** for example, with IntelliJ use `Run > Reload Changed Classes`
+
+To learn more, see the link:http://isis.apache.org/guides/ugvw/ugvw.html#_ugvw_layout_file-based[Wicket viewer guide] (file-based layout).
+
+
+=== Icons
+
+Each domain object is associated with an icon.
+Typically this is static and in the same package as the class; see `HelloWorldObject.png`.
+
+
+=== menubars.layout.xml
+
+In a similar fashion, the actions of the various domain services are grouped into menus using the `menubars.layout.xml` file.
+
+*Mini-Exercise*:
+
+* locate the `menubars.layout.xml` file
+* compare the structure of the layout file to that of the rendered menu bar
+* change the file, eg reorder menu items or create new menus
+* again, use the IDE to copy over the file to the classpath
+** for example, with IntelliJ use `Run > Reload Changed Classes`
+
+
+To learn more, see the link:http://localhost:4000/guides/ugvw/ugvw.html#_ugvw_menubars-layout[Wicket viewer guide] (menu bars layout).
+
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/i18n.adoc b/antora/components/tutorials/modules/petclinic/pages/i18n.adoc
new file mode 100644
index 0000000..e289832
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/i18n.adoc
@@ -0,0 +1,6 @@
+= i18n
+
+:Notice: 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 ag [...]
+
+TODO.
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/integration-testing.adoc b/antora/components/tutorials/modules/petclinic/pages/integration-testing.adoc
new file mode 100644
index 0000000..495da1a
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/integration-testing.adoc
@@ -0,0 +1,628 @@
+= (Integration) Testing
+
+:Notice: 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 ag [...]
+
+In an earlier section of this tutorial we looked at unit testing, but arguably integration tests are at least as important, probably more so, because they exercise the entire application from an end-users perspective, rather than an individual part.
+
+We'll look at integration tests shortly, but first let's look once more to improve our fixture scripts.
+
+
+== An improved Fixture Script
+
+While fixture scripts are great for prototyping and demos, they can also be used for integration tests: they represent the "given" of some scenario.
+
+However, our `RecreateOwnersAndPets` fixture script currently has too many responsibilities, both defining _what_ data to set up and also _how to_ set up that data.
+
+If we split out these responsibilities, it'll make it easier to write integration tests in the future.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/270-an-improved-fixture-script
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+First, the "know-how-to" responsibility:
+
+* create `OwnerBuilderScript`.
+This knows "how to" create an `Owner` and their ``Pet``s using the various injected domain services.
+Using the business logic of the app to setup the data (as opposed to inserting directly in the underlying database tables) means that the fixture will remain valid even if the implementation changes:
++
+[source,java]
+----
+@lombok.experimental.Accessors(chain = true)
+public class OwnerBuilderScript extends BuilderScriptAbstract<Owner, OwnerBuilderScript> {
+
+    @Getter @Setter
+    private String lastName;                                // <1>
+    @Getter @Setter
+    private String firstName;                               // <1>
+    @Getter @Setter
+    private String phoneNumber;                             // <1>
+    @Getter @Setter
+    private String emailAddress;                            // <1>
+
+    @Setter
+    private List<PetData> petData = Lists.newArrayList();   // <1>
+
+    @Data
+    static class PetData {
+        private final String name;
+        private final PetSpecies petSpecies;
+    }
+
+    @Getter                                                 // <2>
+    private Owner object;
+
+    @Override
+    protected void execute(final ExecutionContext ec) {
+
+        checkParam("lastName", ec, String.class);           // <3>
+        checkParam("firstName", ec, String.class);
+
+        Owner owner = wrap(owners).create(lastName, firstName, phoneNumber);
+        wrap(owner).setEmailAddress(emailAddress);
+
+        for (PetData petDatum : petData) {
+            wrap(owner).newPet(petDatum.name, petDatum.petSpecies);
+        }
+
+        this.object = owner;
+    }
+
+    @Inject
+    Owners owners;
+}
+----
+<1> the inputs to the fixture script.
+<2> the output of the fixture script
+<3> the `checkParam(...)` method is used to ensure that all mandatory properties are provided.
++
+A more sophisticated script can use the `defaultParam(...)` method to provide a default value for all properties where _no_ value was provided.
+In conjunction with the `FakeDataService` (which we'll see shortly), this opens up the idea that a builder can be used to create "some" randomised object, with the test class fixing only the values that are significant to the test scenario.
+
+* the above class uses the framework's `WrapperFactory` domain service, which allows interactions to be made "as if" through the UI.
+Using this service requires an additional dependency in the `pom.xml`:
++
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.isis.core</groupId>
+    <artifactId>isis-core-wrapper</artifactId>
+</dependency>
+----
++
+This gives access to the framework's `WrapperFactory` domain service.
+
+Second, the "know-what" responsibility:
+
+* create the `Owner_enum` enum.
+The "what"s are the  enum instances, each delegating the actual creation to the `OwnerBuilderScript`:
++
+[source,java]
+----
+@AllArgsConstructor
+public enum Owner_enum
+        implements PersonaWithBuilderScript<Owner, OwnerBuilderScript> {
+
+    JOHN_SMITH("John", "Smith", null, new PetData[]{
+            new PetData("Rover", PetSpecies.Dog)
+    }),
+    MARY_JONES("Mary","Jones", "+353 1 555 1234", new PetData[] {
+            new PetData("Tiddles", PetSpecies.Cat),
+            new PetData("Harry", PetSpecies.Budgerigar)
+    }),
+    FRED_HUGHES("Fred","Hughes", "07777 987654", new PetData[] {
+            new PetData("Jemima", PetSpecies.Hamster)
+    });
+
+    private final String firstName;
+    private final String lastName;
+    private final String phoneNumber;
+    private final PetData[] petData;
+
+    @Override
+    public OwnerBuilderScript builder() {
+        return new OwnerBuilderScript()
+                .setFirstName(firstName)
+                .setLastName(lastName)
+                .setPhoneNumber(phoneNumber)
+                .setPetData(Arrays.asList(petData));
+    }
+}
+----
+
+* refactor `RecreateOwnersAndPets` to use the enum:
++
+[source,java]
+----
+public class RecreateOwnersAndPets extends FixtureScript {
+
+    public RecreateOwnersAndPets() {
+        super(null, null, Discoverability.DISCOVERABLE);
+    }
+
+    @Override
+    protected void execute(final ExecutionContext ec) {
+
+        isisJdoSupport.deleteAll(Pet.class);
+        isisJdoSupport.deleteAll(Owner.class);
+
+        ec.executeChild(this, new PersonaEnumPersistAll<>(Owner_enum.class));
+    }
+
+    @Inject
+    IsisJdoSupport isisJdoSupport;
+}
+----
+
+Before we get to our integration tests there is one further refinement we can make.
+We will want to easily "look up" existing objects, so we make the `Owner_enum` implement a further interface.
+
+* first, extend `Owners` domain service to perform an exact lookup:
++
+[source,java]
+----
+@Programmatic
+public Owner findByLastNameAndFirstName(
+        final String lastName,
+        final String firstName) {
+    TypesafeQuery<Owner> q = isisJdoSupport.newTypesafeQuery(Owner.class);
+    final QOwner cand = QOwner.candidate();
+    q = q.filter(
+            cand.lastName.eq(q.stringParameter("lastName")).and(
+            cand.firstName.eq(q.stringParameter("firstName"))
+            )
+    );
+    return q.setParameter("lastName", lastName)
+            .setParameter("firstName", firstName)
+            .executeUnique();
+}
+----
+
+* now let's extend `Owner_enum` to also implement `PersonaWithFinder`:
++
+[source,java]
+----
+public enum Owner_enum
+        implements PersonaWithBuilderScript<Owner, OwnerBuilderScript>,
+                   PersonaWithFinder<Owner> {
+    ...
+    @Override
+    public Owner findUsing(final ServiceRegistry2 serviceRegistry) {
+        return serviceRegistry.lookupService(Owners.class)
+                .findByLastNameAndFirstName(lastName, firstName);
+    }
+    ...
+}
+----
+
+
+== Writing Integration Tests
+
+Now we have a refactored our fixture scripts, let's use them in an integration test, to check that `bookVisit` works correctly.
+
+Integration tests are _not_ written using Selenium or similar, so avoid the fragility and maintenance effort that such tests often entail.
+Instead, the framework provides an implementation of the `WrapperFactory` domain service which simulates the user interface in a type-safe way.
+Our unit test code is only allowed to invoke the methods of the domain objects that are visible and modifiable.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/280-writing-integration-tests
+mvn clean package jetty:run
+----
+
+[TIP]
+====
+If running integration tests from the IDE, make sure that the DataNucleus enhancer has run first.
+For example, with IntelliJ this is just a matter of running `mvn datanucleus:enhance -o` first from the command line.
+====
+
+
+=== Exercise
+
+* let's further refactor `RecreateOwnersAndPets`, taking account of the fact that fixture scripts implement the composite pattern:
++
+[source,java]
+----
+public class RecreateOwnersAndPets extends FixtureScript {
+
+    public RecreateOwnersAndPets() {
+        super(null, null, Discoverability.DISCOVERABLE);
+    }
+
+    @Override
+    protected void execute(final ExecutionContext ec) {
+        ec.executeChild(this, new DeleteAllOwnersAndPets());
+        ec.executeChild(this, new PersonaEnumPersistAll<>(Owner_enum.class));
+    }
+}
+----
+
+* where `DeleteAllOwnersAndPets` in turn is:
++
+[source,java]
+----
+public class DeleteAllOwnersAndPets extends TeardownFixtureAbstract2 {
+    @Override
+    protected void execute(final ExecutionContext ec) {
+        deleteFrom(Pet.class);
+        deleteFrom(Owner.class);
+    }
+}
+----
+
+* let's also introduce a new `DeleteAllVisits` fixture:
++
+[source,java]
+----
+public class DeleteAllVisits extends TeardownFixtureAbstract2 {
+    @Override
+    protected void execute(final ExecutionContext ec) {
+        deleteFrom(Visit.class);
+    }
+}
+----
+
+* our integration test, `Pet_bookVisit_IntegTest`, can now use these fixtures:
++
+[source,java]
+----
+public class Pet_bookVisit_IntegTest extends IntegrationTestAbstract3 {
+
+    public Pet_bookVisit_IntegTest() {
+        super(new PetClinicModule());
+    }
+
+    @Before
+    public void setUp() {
+        runFixtureScript(
+                new DeleteAllVisits(),
+                new DeleteAllOwnersAndPets()
+        );
+    }
+}
+----
+
+* Normally it would be sufficient to bootstrap the integration tests using just the module (`PetClinicModule` in this case).
+However, since we have (for simplicity) written the integration test in the webapp module, we need to adjust the bootstrapping to disable a domain service (for i18n support) that is on the classpath:
+
++
+[source,java]
+----
+public class Pet_bookVisit_IntegTest extends IntegrationTestAbstract3 {
+
+    public Pet_bookVisit_IntegTest() {
+        super(new PetClinicModule()
+                // disable the TranslationServicePo domain service
+                .withAdditionalServices(DeploymentCategoryProviderForTesting.class)
+                .withConfigurationProperty(TranslationServicePo.KEY_PO_MODE, "write")
+        );
+    }
+
+    public static class DeploymentCategoryProviderForTesting
+            implements DeploymentCategoryProvider {
+        @Getter
+        DeploymentCategory deploymentCategory = DeploymentCategory.PROTOTYPING;
+    }
+
+    ...
+}
+----
+
+* okay, now let's write the happy case:
++
+[source,java]
+----
+@Test
+public void happy_case() {
+
+    // given
+    runFixtureScript(Owner_enum.FRED_HUGHES.builder());
+
+    Owner owner = Owner_enum.FRED_HUGHES.findUsing(serviceRegistry);
+    Pet pet = owner.getPets().first();
+    Pet_bookVisit mixin = factoryService.mixin(Pet_bookVisit.class, pet);
+
+    // when
+    LocalDateTime default0Act = mixin.default0Act();
+    String reason = "off her food";
+    Visit visit = wrap(mixin).act(default0Act, reason);
+
+    // then
+    assertThat(visit.getPet()).isEqualTo(pet);
+    assertThat(visit.getVisitAt()).isEqualTo(default0Act);
+    assertThat(visit.getReason()).isEqualTo(reason);
+}
+----
+
+* and let's also write an error scenario which checks that a reason has been provided:
++
+[source,java]
+----
+@Test
+public void reason_is_required() {
+
+    // given
+    runFixtureScript(Owner_enum.FRED_HUGHES.builder());
+
+    Owner owner = Owner_enum.FRED_HUGHES.findUsing(serviceRegistry);
+    Pet pet = owner.getPets().first();
+    Pet_bookVisit mixin = factoryService.mixin(Pet_bookVisit.class, pet);
+
+    // expect
+    expectedExceptions.expect(InvalidException.class);
+    expectedExceptions.expectMessage("Mandatory");
+
+    // when
+    LocalDateTime default0Act = mixin.default0Act();
+    String reason = null;
+    wrap(mixin).act(default0Act, reason);
+}
+----
+
+== Factor out abstract integration test
+
+In the next main section we'll be looking at extending the scope of the app, but before that we should invest further in our integration testing infrastructure.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/290-factor-out-abstract-integration-test
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+* Factor out an abstract class for integration tests:
++
+[source,java]
+----
+public abstract class PetClinicModuleIntegTestAbstract extends IntegrationTestAbstract3 {
+
+    public PetClinicModuleIntegTestAbstract() {
+        super(new PetClinicModule()
+                // disable the TranslationServicePo domain service
+                .withAdditionalServices(DeploymentCategoryProviderForTesting.class)
+                .withConfigurationProperty(TranslationServicePo.KEY_PO_MODE, "write")
+        );
+    }
+
+    public static class DeploymentCategoryProviderForTesting implements DeploymentCategoryProvider {
+        @Getter
+        DeploymentCategory deploymentCategory = DeploymentCategory.PROTOTYPING;
+    }
+}
+----
+
+* Update our existing integration test to use this new adapter:
++
+[source,java]
+----
+public class Pet_bookVisit_IntegTest extends PetClinicModuleIntegTestAbstract {
+
+    @Before
+    public void setUp() { ... }
+    @Test
+    public void happy_case() { ... }
+    @Test
+    public void reason_is_required() { ... }
+
+}
+----
+
+== Move teardowns to modules
+
+When running a suite of integration tests we need to reset the database to a known state, typically deleting all data (or at least, all non-reference data).
+Since modules are "containers" of entities (among other things), the framework allows the module to handle this responsibility.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/300-move-teardowns-to-modules
+mvn clean package jetty:run
+----
+
+
+
+=== Exercise
+
+* Update the `PetClinicModule`, adding in `getRefDataSetupFixture()` and `getTeardownFixture()`:
++
+[source,java]
+----
+public class PetClinicModule extends ModuleAbstract {
+
+    @Override
+    public FixtureScript getRefDataSetupFixture() {
+        // nothing currently
+        return null;
+    }
+
+    @Override public FixtureScript getTeardownFixture() {
+        return new FixtureScript() {
+            @Override
+            protected void execute(final ExecutionContext ec) {
+                ec.executeChild(this, new DeleteAllVisits());
+                ec.executeChild(this, new DeleteAllOwnersAndPets());
+            }
+        };
+    }
+}
+----
+
+* Update `Pet_bookVisit_IntegTest`, removing the `setpUp()` method (which deletes all data from the tables)
+
+
+== Fake Data Service
+
+When exercising some functionality, we need to provide valid arguments for the various actions being invoked.
+Sometimes the values of thosse arguments are significant (eg can't book a visit for a date in the past), but sometimes they just need to be a value (eg the reason for a visit).
+
+We should be able to understand the behaviour of an application through its tests.
+To help the reader, it would be good to distinguish between the significant values and the "any old value".
+
+The http://platform.incode.org/[Incode Platform]'s http://platform.incode.org/modules/lib/fakedata/lib-fakedata.html[Fake Data library] provides us with a `FakeDataService` domain service that helps generate such fake or random data for our tests.
+Let's integrate it.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/310-fake-data-service
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+* update the `pom.xml` to reference the Incode Platform's fake data module.
++
+Add a property:
++
+[source,xml]
+----
+<incode-platform.version>1.16.2</incode-platform.version>
+----
++
+and add a dependency:
++
+[source,xml]
+----
+<dependency>
+    <groupId>org.isisaddons.module.fakedata</groupId>
+    <artifactId>isis-module-fakedata-dom</artifactId>
+    <version>${incode-platform.version}</version>
+</dependency>
+----
+
+* Extend `PetClinicModule` to depend upon the `FakeDataModule`:
++
+[source,java]
+----
+public class PetClinicModule extends ModuleAbstract {
+
+    @Override
+    public Set<Module> getDependencies() {
+        return Sets.newHashSet(new FakeDataModule());
+    }
+    ...
+}
+----
+
+
+== Extend the Fixture script to set up visits
+
+Some of the functionality we want to test will require visits, but so far our fixture scripts only allow us to set up ``Owner``s and their ``Pet``s.
+Let's extend the fixture scripts so we can declaratively have a number of ``Visit``s for each of the ``Pet``s also.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/320-extend-the-fixture-script-to-set-up-visits
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+* in `OwnerBuilderScript`
+
+** inject two new domain services.
+We'll need these to compute the date of the ``Visit``s.
++
+[source,java]
+----
+@Inject
+FakeDataService fakeDataService;
+
+@Inject
+ClockService clockService;
+----
+
+** add some helper methods:
++
+[source,java]
+----
+private String someReason() {
+    return fakeDataService.lorem().paragraph(fakeDataService.ints().between(1, 3));
+}
+
+private LocalDateTime someRandomTimeInPast() {
+    return clockService.now()
+            .toDateTimeAtStartOfDay().minus(fakeDataService.jodaPeriods().daysBetween(5, 365))
+            .plusHours(fakeDataService.ints().between(9, 17))
+            .plusMinutes(5 * fakeDataService.ints().between(0, 12))
+            .toLocalDateTime();
+}
+
+private void setTimeTo(final ExecutionContext ec, final LocalDateTime ldt) {
+    ec.executeChild(this, new TickingClockFixture().setDate(ldt.toString("yyyyMMddhhmm")));
+}
+----
++
+Note the use of the framework-provided `TickingClockFixture` that lets the time reported by `ClockService` be changed.
+
+** extend `PetData` to specify the number of visits to setup:
++
+[source,java]
+----
+@Data
+static class PetData {
+    private final String name;
+    private final PetSpecies petSpecies;
+    private final int numberOfVisits;
+}
+----
+
+** extend the `execute(...)` method to set up the required number of visits (using the previously added helper methods):
++
+[source,java]
+----
+LocalDateTime now = clockService.nowAsLocalDateTime();
+try {
+    for (PetData petDatum : petData) {
+        Pet pet = wrap(owner).newPet(petDatum.name, petDatum.petSpecies);
+        for (int i = 0; i < petDatum.numberOfVisits; i++) {
+            LocalDateTime someTimeInPast = someRandomTimeInPast();
+            String someReason = someReason();
+            setTimeTo(ec, someTimeInPast);
+            wrap(mixin(Pet_bookVisit.class, pet)).act(someTimeInPast.plusDays(3), someReason);
+        }
+    }
+} finally {
+    setTimeTo(ec, now);
+}
+----
+
+* extend `Owner_enum` persona to use all new infrastructure:
++
+[source,java]
+----
+JOHN_SMITH("John", "Smith", null, new PetData[]{
+        new PetData("Rover", PetSpecies.Dog, 3)
+}),
+MARY_JONES("Mary","Jones", "+353 1 555 1234", new PetData[] {
+        new PetData("Tiddles", PetSpecies.Cat, 1),
+        new PetData("Harry", PetSpecies.Budgerigar, 2)
+}),
+FRED_HUGHES("Fred","Hughes", "07777 987654", new PetData[] {
+        new PetData("Jemima", PetSpecies.Hamster, 0)
+});
+----
++
+The difference is simply the last argument in the `PetData` constructor.
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/modularity.adoc b/antora/components/tutorials/modules/petclinic/pages/modularity.adoc
new file mode 100644
index 0000000..252aa12
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/modularity.adoc
@@ -0,0 +1,382 @@
+= Modularity
+
+:Notice: 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 ag [...]
+
+Keeping applications modular is key to their long-term maintainability.
+If every class potentially can depend on any other class, we'll end up with a "big ball of mud" that becomes almost impossible to change.
+
+Instead, we need to ensure that the dependency graph between packages remains acyclic.
+The Apache Isis framework provides some powerful tools.
+
+
+== Introducing Packages
+
+At the moment all of the domain services and entities are in a single package.
+Referring back to our design we see though that there are meant to be two packages, `pets` and `visits`:
+
+[plantuml]
+----
+hide empty members
+hide methods
+
+skinparam class {
+	BackgroundColor<<desc>> Cyan
+	BackgroundColor<<ppt>> LightGreen
+	BackgroundColor<<mi>> LightPink
+	BackgroundColor<<role>> LightYellow
+}
+
+package pets {
+
+    enum PetSpecies <<desc>> {
+    }
+
+    class Pet <<ppt>> {
+    }
+
+    class Owner <<role>> {
+    }
+}
+
+
+package visits {
+
+    class Visit <<mi>> {
+    }
+}
+
+
+Owner *-down-> "0..*" Pet
+Visit "   \n*" -up->  Pet
+Pet  "*" -right--> PetSpecies
+----
+
+Also, the fixture scripts should probably be kept separate from the production code.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/220-introducing-packages
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+Rename the following:
+
+[cols="2m,2m,3m", options="header"]
+|===
+
+| Class
+| From
+| To
+
+| PetClinicFixtureScript-Provider
+| domainapp.dom.impl
+| domainapp.modules.impl
+
+| Owner
+| domainapp.dom.impl
+| domainapp.modules.impl.pets.dom
+
+| OwnerTest_updateName
+| domainapp.dom.impl
+| domainapp.modules.impl.pets.dom
+
+| OwnerTest_delete
+| domainapp.dom.impl
+| domainapp.modules.impl.pets.dom
+
+| Owners
+| domainapp.dom.impl
+| domainapp.modules.impl.pets.dom
+
+| Pet
+| domainapp.dom.impl
+| domainapp.modules.impl.pets.dom
+
+| PetSpecies
+| domainapp.dom.impl
+| domainapp.modules.impl.pets.dom
+
+| Pet_bookVisit_Test
+| domainapp.dom.impl
+| domainapp.modules.impl.pets.dom
+
+| RecreatePetsAndOwners
+| domainapp.dom.impl
+| domainapp.modules.impl.pets.fixtures
+
+| Visit
+| domainapp.dom.impl
+| domainapp.modules.impl.visits.dom
+
+|===
+
+Also move the supporting `.layout.xml`, `.png` files.
+
+== Inverting responsibilities (Refactoring the ``Pet``'s visits)
+
+For long-term maintainability it's important to keep the application modular.
+In particular, that means avoiding cyclic dependencies.
+
+If we look at our original design, we see that the original idea was for `visits` package depends upon the `pets` package, but not the other way around:
+
+[plantuml]
+----
+
+hide empty members
+hide methods
+
+skinparam class {
+	BackgroundColor<<desc>> Cyan
+	BackgroundColor<<ppt>> LightGreen
+	BackgroundColor<<mi>> LightPink
+	BackgroundColor<<role>> LightYellow
+}
+
+
+package pets {
+
+    class Pet <<ppt>> {
+    }
+}
+
+package visits {
+
+    class Visit <<mi>> {
+        #pet
+    }
+}
+
+
+Visit "*" -up->  Pet
+----
+
+However, as things stand this dependency is bidirectional: `Pet` acts as the factory of `Visit`, and yet `Visit` references back to that same `Pet`.
+
+We fix the issue by moving the behaviour out of `Pet`, and into a "mixin".
+This mixin then resides in the `visits` package.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/230-inverting-responsibilities
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+* create the following "mixin" class (most of the code can be copied-n-pasted out of `Pet`):
++
+[source,java]
+----
+@Mixin(method = "act")                                              // <1>
+public class Pet_bookVisit {
+
+    private final Pet pet;
+    public Pet_bookVisit(Pet pet) {                                 // <2>
+        this.pet = pet;
+    }
+
+    @Action(semantics = SemanticsOf.NON_IDEMPOTENT)
+    public Visit act(                                               // <1>
+            final LocalDateTime at,
+            @Parameter(maxLength = 4000)
+            @ParameterLayout(multiLine = 5)
+            final String reason) {
+        return repositoryService.persist(new Visit(this.pet, at, reason));
+    }
+
+    public LocalDateTime default0Act() {                            // <1>
+        return clockService.now()
+                .plusDays(1)
+                .toDateTimeAtStartOfDay()
+                .toLocalDateTime()
+                .plusHours(9);
+    }
+
+    public String validate0Act(final LocalDateTime proposed) {      // <1>
+        return proposed.isBefore(clockService.nowAsLocalDateTime())
+                ? "Cannot enter date in the past"
+                : null;
+    }
+
+    @javax.jdo.annotations.NotPersistent
+    @javax.inject.Inject
+    RepositoryService repositoryService;
+
+    @javax.jdo.annotations.NotPersistent
+    @javax.inject.Inject
+    ClockService clockService;
+}
+----
+<1> the name of the action is derived from the class rather than the method name (by convention, called simply "act").
+<2> constructor determines the type that the mixin contributes to.
+This can be a class or an interface.
+
+* remove the corresponding code from `Pet`
+
+* refactor the `Pet_bookVisit_Test` unit test to exercise the mixin rather than the `Pet`.
+
+
+== Pet's visits (a contributed collection)
+
+We also have the issue that we can't actually access the ``Visit``s once they have been created.
+An obvious place to see them would probably be from the `Pet`.
+Similar to the "bookVisit" contributed action, we can also contribute a "visits" collection:
+
+image::{_imagesdir}/Pet-visits-collection.png[width="800px",link="_images/Pet-visits-collection.png"]
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/240-pets-visits-a-contributed-collection
+mvn clean package jetty:run
+----
+
+
+
+=== Exercise
+
+* we'll start by creating a `Visits` domain service repository:
++
+[source,java]
+----
+@DomainService(nature = NatureOfService.DOMAIN)             // <1>
+public class Visits {
+
+    @Programmatic                                           // <2>
+    public java.util.Collection<Visit> findByPet(Pet pet) {
+        TypesafeQuery<Visit> q = isisJdoSupport.newTypesafeQuery(Visit.class);
+        final QVisit cand = QVisit.candidate();
+        q = q.filter(
+                cand.pet.eq((q.parameter("pet", Pet.class))
+            )
+        );
+        return q.setParameter("pet", pet)
+                .executeList();
+    }
+
+    @javax.inject.Inject
+    IsisJdoSupport isisJdoSupport;
+}
+----
+<1> don't show in the menu
+<2> and in any case, exclude this method from the metamodel.
+
+* create the `Pet_visits` mixin and have it delegate to the `Visits` service:
++
+[source,java]
+----
+@Mixin(method = "coll")                                         // <1>
+public class Pet_visits {
+
+    private final Pet pet;
+    public Pet_visits(Pet pet) {
+        this.pet = pet;
+    }
+
+    @Action(semantics = SemanticsOf.SAFE)                       // <2>
+    @ActionLayout(contributed = Contributed.AS_ASSOCIATION)     // <3>
+    @CollectionLayout(defaultView = "table")
+    public java.util.Collection<Visit> coll() {                 // <1>
+        return visits.findByPet(pet);
+    }
+
+    @javax.inject.Inject
+    Visits visits;
+}
+----
+<1> the collection name is derived from the class name, not the method name
+<2> behind the scenes contributed collections are just a type of action.
+They must take no arguments, and have no side-effects.
+<3> this is what makes the contributed action instead be rendered as a collection
+
+* associate the `Pet_bookVisit` action with this collection (so is rendered as part of the "visits" collection):
++
+[source,java]
+----
+@Action(semantics = SemanticsOf.NON_IDEMPOTENT, associateWith = "visits")
+@ActionLayout(named = "Book")
+public Visit act(...) { ... }
+----
+
+
+== Events
+
+Mixins are a powerful technique to decouple the application, but they are only half the story.
+
+What happens if we attempt to delete an `Owner` that has associated ``Pet``s which in turn have associated ``Visit``s?
+Well, the ``Pet``s will be cascade-deleted, but the ``Visit``s are not.
+This prevents the delete from occurring.
+
+What we want to happen is for the ``Visit``s also to be deleted.
+However, this can't be a responsibility of `Owner` or `Pet`, because they are not meant to "know" about the associated visits.
+
+What we can do instead is to use domain events, and set up a subscriber domain service to do the delete of associated ``Visit``s when a `Pet` is deleted.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/250-events
+mvn clean package jetty:run
+----
+
+To try this out, book a `Visit` for a `Pet`, then navigate back to the parent `Owner` and delete it.
+All associated ``Pet``s and ``Visit``s should be deleted: the ``Pet``s because the Owner <--> Pet association is declared for cascade-delete , the ``Visit``s because of the subscriber.
+
+
+=== Exercise
+
+* update `Pet` so that events will be emitted when it is deleted:
++
+[source,java]
+----
+@DomainObject(
+    auditing = Auditing.ENABLED,
+    removingLifecycleEvent = Pet.RemovingEvent.class            // <1>
+)
+...
+public class Pet implements Comparable<Pet> {
+    public static class RemovingEvent extends ObjectRemovingEvent<Pet> {}
+    ...
+}
+----
+<1> an instance of this class will be emitted when the `Pet` instance is about to be deleted
+
+* add a new `PetVisitCascadeDelete` subscriber.
++
+[source,java]
+----
+@DomainService(nature = NatureOfService.DOMAIN)
+public class PetVisitCascadeDelete
+        extends org.apache.isis.applib.AbstractSubscriber {                 // <1>
+
+    @org.axonframework.eventhandling.annotation.EventHandler                // <2>
+    public void on(Pet.RemovingEvent ev) {                                  // <3>
+        Collection<Visit> visitsForPet = visits.findByPet(ev.getSource());
+        for (Visit visit : visitsForPet) {
+            repositoryService.removeAndFlush(visit);
+        }
+    }
+
+    @javax.inject.Inject
+    Visits visits;
+    @javax.inject.Inject
+    RepositoryService repositoryService;
+}
+----
+<1> convenience superclass that hooks up the subscriber with the internal event bus
+<2> the event bus is implemented using the link:http://www.axonframework.org/[Axon Framework] so the callback method must be annotated with the appropriate annotation.
+<3> called only when a `Pet` is about to be deleted.
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/prototyping.adoc b/antora/components/tutorials/modules/petclinic/pages/prototyping.adoc
new file mode 100644
index 0000000..cdc8ca4
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/prototyping.adoc
@@ -0,0 +1,190 @@
+= Prototyping
+
+:Notice: 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 ag [...]
+
+== Fixture Scripts (for Owner)
+
+The hello world archetype sets up an application that by default is configured to use an in-memory database.
+That means that every time we restart the application, we start with an empty database.
+After a while it gets pretty tedious continually having to create domain objects while testing.
+
+While we _could_ reconfigure the application to run with an external database (so that the data survives application restarts), it would then open up data migration issues when the data changes in an incompatible way as we continue to develop the application.
+
+A better approach is to stick with the in-memory database, but to automate the setup of data, something we can do with Apache Isis' "Fixture Scripts" library.
+
+[TIP]
+====
+There's another benefit of sticking with the in-memory database and using fixtures scripts - it means we can reuse those same fixture scripts when writing integration tests.
+The fixture script captures the "given" of the test scenario, and the test itself concentrates on the "when" and the "then".
+====
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/130-fixture-scripts-for-owner
+mvn clean package jetty:run
+----
+
+
+=== Exercise
+
+* Implement the `RecreateOwners` fixture script:
++
+[source,java]
+----
+package domainapp.dom.impl;
+// ... imports omitted
+public class RecreateOwners extends FixtureScript {
+    public RecreateOwners() {
+        super(null, null, Discoverability.DISCOVERABLE);                // <1>
+    }
+    @Override
+    protected void execute(final ExecutionContext ec) {
+        isisJdoSupport.deleteAll(Owner.class);                          // <2>
+        ec.addResult(this,                                              // <3>
+                this.owners.create("Smith", "John", null));             // <4>
+        ec.addResult(this,
+                this.owners.create("Jones", "Mary", "+353 1 555 1234"));
+        ec.addResult(this,
+                this.owners.create("Hughes", "Fred", "07777 987654"));
+    }
+    @Inject
+    Owners owners;                                                      // <4>
+    @Inject
+    IsisJdoSupport isisJdoSupport;                                      // <2>
+}
+----
+<1> make script available in the UI
+<2> use framework-provided `IsisJdoSupport` domain service to delete any existing objects (making the script re-runnable).
+(Domain services are injected into fixture scripts, same as domain objects).
+<3> makes the results available to the caller.
+When prototyping, these are rendered in the UI.
+This could be useful for Selenium E2E tests, for example.
+<4> Use the existing functionality from the injected `Owners` domain service.
+
+* Also, implement `PetClinicFixtureScriptSpecProvider`, which configures the framework to include all fixtures under the specified package:
+
+[source,java]
+----
+package domainapp.dom.impl;
+// ... imports omitted
+@DomainService(nature = NatureOfService.DOMAIN)
+public class PetClinicFixtureScriptSpecProvider
+        implements FixtureScriptsSpecificationProvider {
+    @Override
+    public FixtureScriptsSpecification getSpecification() {
+        return FixtureScriptsSpecification.builder(getClass())
+                .withRunScriptDefault(RecreateOwners.class)
+                .build();
+    }
+}
+----
+
+This now provides us with a _Run Fixture Script_ menu item under the _Prototyping_ menu:
+
+image::{_imagesdir}/run-fixture-script-menu-item.png[width="250px",link="_images/run-fixture-script-menu-item.png"]
+
+from which we can select the _Order Fixture Script_:
+
+image::{_imagesdir}/run-fixture-script-prompt.png[width="400px",link="_images/run-fixture-script-prompt.png"]
+
+When invoked this shows the three `Order` domain objects just created:
+
+image::{_imagesdir}/run-fixture-script-result.png[width="800px",link="_images/run-fixture-script-result.png"]
+
+
+== Run with a different manifest
+
+While running the fixture scripts is easy to do, we can go one better by running the fixture script automatically when the application starts.
+To do that we need to understand a little more about how the framework bootstraps our app.
+
+The key concept is that of an "app manifest", which allows us to identify the code modules that make up the class, along with various configuration properties.
+It also allows us to optionally specify a fixture script to run.
+
+The default app manifest is `PetClinicAppManifest` (we actually renamed this earlier from the name generated by the archetype):
+
+[source,java]
+----
+public class PetClinicAppManifest extends AppManifestAbstract2 {
+
+    public static final Builder BUILDER = Builder
+            .forModule(new PetClinicModule())                               // <1>
+            .withConfigurationPropertiesFile(                               // <2>
+                PetClinicAppManifest.class, "isis-non-changing.properties")
+            .withAuthMechanism("shiro");                                    // <3>
+
+    public PetClinicAppManifest() {
+        super(BUILDER);
+    }
+}
+----
+<1> load all the entities and domain services accessible under this package.
+The framework uses classpath scanning to discover these classes.
+<2> load all configuration properties in the `isis-non-changing.properties` file, relative to this manifest class.
+This is in addition to any (typically environment-specific) configuration properties loaded from the various properties files (eg `isis.properties`) to be found in `WEB-INF` directory.
+<3> use Apache Shiro for authentication.
+We'll ignore this for now; suffice to say that Apache Isis can be integrated with various authentication providers, with Shiro being a very flexible out-of-the-box implementation.
+
+The framework knows to use this app manifest because it is specified in `WEB-INF/isis.properties` file:
+
+[source,properties]
+----
+isis.appManifest=domainapp.application.PetClinicAppManifest
+----
+
+However, we can write an alternative manifest that will also run our fixture script, and then use this new manifest either by editing the `isis.properties` file or (better), run the app using a system property.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/140-run-with-a-different-manifest
+mvn clean package jetty:run
+----
+
+and run using this system property:
+
+[source,bash]
+----
+-Disis.appManifest=\
+    domainapp.application.PetClinicAppManifestWithFixture
+----
+
+When you run the application, the fixture should have run already and so there should be some ``Owner`` instances.
+
+=== Exercise
+
+* implement `PetClinicAppManifestWithFixture`:
++
+[source,java]
+----
+public class PetClinicAppManifestWithFixture
+                    extends AppManifestAbstract2 {
+    public static final Builder BUILDER =
+            PetClinicAppManifest.BUILDER                        // <1>
+                    .withFixtureScripts(RecreateOwners.class);  // <2>
+    public PetClinicAppManifestWithFixture() {
+        super(BUILDER);
+    }
+}
+----
+<1> reuses the builder of the original manifest, but \...
+<2> \... also automatically run the `RecreateOwners` fixture script on bootstrap
+
+
+* run using this system property:
++
+[source,bash]
+----
+-Disis.appManifest=\
+    domainapp.application.PetClinicAppManifestWithFixture
+----
++
+for example:
++
+image::{_imagesdir}/extended-manifest-run-configuration.png[width="800px",link="_images/extended-manifest-run-configuration.png"]
+
+When you run the application, the fixture should have run already and so there should be some ``Owner`` instances.
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/removing-boilerplatex.adoc b/antora/components/tutorials/modules/petclinic/pages/removing-boilerplatex.adoc
new file mode 100644
index 0000000..4ed2aba
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/removing-boilerplatex.adoc
@@ -0,0 +1,236 @@
+== Removing boilerplate (Lombok)
+
+:Notice: 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 ag [...]
+
+Java has a -- probably deserved -- reputation for being somewhat prone to boilerplate, obscuring the meaning of our code.
+There are however a couple of techniques we can use to reduce the boilerplate load.
+
+One of the most common areas of boilerplate is the getters and setters.
+We can use project lombok to remove the necessity of writing these methods.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/030-removing-boilerplate-lombok
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* in the `pom.xml`, add the following to `<dependencies>`:
++
+[source,xml]
+----
+<dependency>
+    <groupId>org.projectlombok</groupId>
+    <artifactId>lombok</artifactId>
+    <version>1.16.18</version>
+    <scope>provided</scope>
+</dependency>
+----
+
+* remove the getters and setters for `Owner#name` and add in its `@lombok.Getter` and `@lombok.Setter` annotations for its field instead:
+
++
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "false", length = 40)
+@Property(editing = Editing.DISABLED)
+@Title(prepend = "Object: ")
+@Getter @Setter                 // <1>
+private String name;
+----
+<1> added in, `getName()` and `setName()` removed
+
+* do likewise for the `Owner#notes` property
+
+
+== Removing boilerplate (Parameter names)
+
+Apache Isis uses Java reflection to infer the names of domain object types and members.
+However, prior to Java 8 the name of method parameters was not accessible, and thus an annotation is required to provide this information.
+
+For example, the `Owners#findByName(...)` action is:
+
+[source,java]
+----
+public class Owners {
+
+    @Action(semantics = SemanticsOf.NON_IDEMPOTENT)
+    @MemberOrder(sequence = "1")
+    public Owner create(
+            @Parameter(maxLength = 40)
+            @ParameterLayout(named = "Name")
+            final String name) {
+        return repositoryService.persist(new Owner(name));
+    }
+    ...
+}
+----
+
+The boilerplate here is the `@ParameterLayout#named` annotation.
+
+Given we're running on Java 8, though, we can remove this boilerplate.
+We do so by configuring the framework to also check for Java 8 metadata.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/040-removing-boilerplate--Paraname8
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* in the IDE, ensure that the compiler specifies the `-parameters` flag.
++
+For example, in IntelliJ:
++
+image::{_imagesdir}/intellij-java-compiler-parameters.png[width="800px",link="_images/intellij-java-compiler-parameters.png"]
+
+* in the `pom.xml`, add the following to `<dependencies>`:
++
+[source,xml]
+----
+<dependency>
+    <groupId>org.isisaddons.metamodel.paraname8</groupId>
+    <artifactId>isis-metamodel-paraname8-dom</artifactId>
+    <version>1.16.2</version>
+</dependency>
+----
+
+* in the `isis-non-changing.properties` file, add:
++
+[source,properties]
+----
+isis.reflector.facets.include=\
+    org.isisaddons.metamodel.paraname8.NamedFacetOnParameterParaname8Factory
+----
++
+This extends the metamodel processing to use the new Java 8 reflection API.
++
+There's further discussion on configuration properties in the next section.
+
+* Delete the `@ParameterLayout(named=...)` attribute wherever it is now redundant.
+
+Run the application and make sure it still runs fine.
+
+
+== Disable editing
+
+The framework is configured using various properties files.
+The archetype splits these into an `isis-non-changing.properties` file, and an `isis.properties` file (under `WEB-INF`).
+The former is configuration properties that don't vary by environment, the latter contains properties that _will_ vary by environment (eg database connection parameters).
+
+[TIP]
+====
+All of these configuration properties can be overridden using system properties.
+====
+
+The `isis-non-changing.properties` file includes this setting:
+
+[source,properties]
+----
+isis.objects.editing=false
+----
+
+This means that properties are non-editable by default, a good convention because in most cases we will want to use actions to mutate the state of the system.
+In `Order` we can therefore remove some boilerplate:
+
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "false", length = 40)
+@Property(editing = Editing.DISABLED)
+@Title(prepend = "Object: ")
+@Getter @Setter
+private String name;
+----
+
+Since editing is disabled by default, we can therefore remove the `editing = Editing.DISABLED` attribute.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/050-disable-editing
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* remove the `editing = Editing.DISABLED` attribute:
++
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "false", length = 40)
+@Property
+@Title(prepend = "Object: ")
+@Getter @Setter
+private String name;
+----
++
+[TIP]
+====
+We could in fact remove `@Property` annotation entirely because the framework searches only for getters and setters.
+It's a matter of taste.
+====
+
+
+== Font Awesome Icons
+
+While we are looking at `isis-non-changing.prooperties`, note that there is another configuration setting for font-awesome icons:
+
+[source,properties]
+----
+isis.reflector.facet.cssClassFa.patterns=\
+                        new.*:fa-plus,\
+                        add.*:fa-plus-square,\
+                        create.*:fa-plus,\
+                        update.*:fa-edit,\
+                        delete.*:fa-trash,\
+                        find.*:fa-search,\
+                        list.*:fa-list
+----
+
+This is what causes the framework to automatically include icons for specifically named action names.
+
+Similarly, bootstrap "btn" classes can also be associated with action names:
+
+[source,properties]
+----
+isis.reflector.facet.cssClass.patterns=\
+                        delete.*:btn-danger
+----
+
+This UI hint _can_ be specified using either annotations or the `.layout.xml` files, but specifying it centrally removes the boilerplate clutter and is a good way of ensuring consistent verbs are chosen for action names.
+
+
+== Implicit Action Annotations
+
+Apache Isis has two ways to recognise actions, either by those that are explicitly annotated with `@Action` annotation, or alternatively implicitly as all `public` methods that do not otherwise represent properties, collections or supporting methods.
+
+The archetype is configured to use the former, but we can switch to implicit actions and potentially save the need to add `@Action` annotation.
+Also (and probably more useful), in implicit mode the framework will tell us if we accidentally mis-spell any of the supporting methods (which we'll see later in the tutorial).
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/060-implicit-action-annotations
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* in `isis-non-changing.properties`, set this configuration property to false:
++
+[source,java]
+----
+isis.reflector.explicitAnnotations.action=false
+----
+
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/view-models.adoc b/antora/components/tutorials/modules/petclinic/pages/view-models.adoc
new file mode 100644
index 0000000..f0271e6
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/view-models.adoc
@@ -0,0 +1,97 @@
+= View models
+
+:Notice: 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 ag [...]
+
+So far the application consists only of domain entities and domain services.
+However, the framework also supports view models.
+
+A classic use case is to provide a home page or dashboard, but they are also used to represent certain specific business processes when there isn't necessarily a domain entity required to track the state of the process itself.
+Some real-world examples include importing/exporting spreadsheets periodically (eg changes to indexation rates), or generating extracts such as a payment file or summary PDF for an quarterly invoice run.
+
+
+
+== Dashboard
+
+For this application, though, we'll just focus on building a dashboard.
+Moreover, we'll make this the home page so that it is automatically shown when the user starts up the application.
+
+Our end result is:
+
+image::{_imagesdir}/dashboard.png[width="800px",link="_images/dashboard.png"]
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/260-view-models
+mvn clean package jetty:run
+----
+
+=== Exercise
+
+* write a `Dashboard` view model:
++
+[source,java]
+----
+@DomainObject(
+    nature = Nature.VIEW_MODEL,                             // <1>
+    objectType = "dashboard.Dashboard"
+)
+public class Dashboard {
+
+    public String title() { return getOwners().size() + " owners"; }
+
+    @CollectionLayout(defaultView = "table")
+    public List<Owner> getOwners() {
+        return owners.listAll();
+    }
+
+    @javax.inject.Inject
+    Owners owners;
+}
+----
+<1> Framework manages the state (though this view model happens to be stateless).
+For complex view models their state is serialized using JAXB.
+
+* provide a `Dashboard.layout.xml`:
++
+[source,xml]
+----
+<bs3:grid ...>
+    <bs3:row>
+        <bs3:col span="12">
+            <c:collection id="owners" defaultView="table"/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="0" unreferencedActions="true">
+            <c:fieldSet name="Other" unreferencedProperties="true"/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="12">
+            <bs3:tabGroup unreferencedCollections="true"/>
+        </bs3:col>
+    </bs3:row>
+</bs3:grid>
+----
++
+This example uses the trick of `<col span="0"...>` to completely suppress any unreferenced properties or actions (eg those contributed by the framework).
+
+* implement a `HomePageProvider` domain service:
++
+[source,java]
+----
+@DomainService(nature = NatureOfService.DOMAIN)
+public class HomePageProvider {
+    @HomePage                               // <1>
+    public Dashboard dashboard() {
+        return new Dashboard();
+    }
+}
+----
+<1> the action annotated with `@HomePage` is called automatically and its results rendered on the home page.
+There can only be one action with this annotation.
+
+
diff --git a/antora/components/tutorials/nav.adoc b/antora/components/tutorials/nav.adoc
new file mode 100644
index 0000000..c0acc1d
--- /dev/null
+++ b/antora/components/tutorials/nav.adoc
@@ -0,0 +1,73 @@
+
+* xref:intro.adoc[Introduction]
+
+* xref:getting-started.adoc[Getting Started]
+** xref:getting-started.adoc#_prereqs[Prereqs]
+** xref:getting-started.adoc#_generate[Generate]
+** xref:getting-started.adoc#_explore_the_generated_app[Explore the generated app]
+** xref:getting-started.adoc#_pull_down_github_example_solution[image:hand.png[] *010*: Pull down the github example/solution]
+** xref:getting-started.adoc#_set_up_your_dev_env[Set up your dev env]
+** xref:getting-started.adoc#_naked_objects_pattern[Naked Objects pattern]
+** xref:getting-started.adoc#_ui_hints[UI Hints]
+
+* xref:an-example-domain.adoc[An Example Domain]
+** xref:an-example-domain.adoc#_rename_code_helloworldobject_code_to_code_owner_code[image:hand.png[] *020*: Rename HelloWorldObject to Owner]
+
+* xref:removing-boilerplatex.adoc[Removing Boilerplate]
+** xref:removing-boilerplatex.adoc#_removing_boilerplate_lombok[image:hand.png[] *030*: Lombok]
+** xref:removing-boilerplatex.adoc#_removing_boilerplate_parameter_names[image:hand.png[] *040*: Parameter Names]
+** xref:removing-boilerplatex.adoc#_disable_editing[image:hand.png[] *050*: Disable editing]
+** xref:removing-boilerplatex.adoc#_font_awesome_icons[Font awesome icons]
+** xref:removing-boilerplatex.adoc#_implicit_action_annotations[image:hand.png[] *060*: Implicit Action Annotations]
+
+* xref:fleshing-out-the-owner-entity.adoc[Fleshing out the Owner entity]
+** xref:fleshing-out-the-owner-entity.adoc#_rework_code_owner_code_s_name_code_firstname_code_and_code_lastname_code[image:hand.png[] *070*: Rework Owner's name (firstName and lastName)]
+** xref:fleshing-out-the-owner-entity.adoc#_derived_name_property[image:hand.png[] *080*: Derived name property]
+** xref:fleshing-out-the-owner-entity.adoc#_digression_changing_the_app_name[image:hand.png[] *090*: Digression: Changing the App Name]
+** xref:fleshing-out-the-owner-entity.adoc#_changing_the_object_type_class_alias[image:hand.png[] *100*: Changing the "Object Type" Class Alias]
+** xref:fleshing-out-the-owner-entity.adoc#_add_other_properties_for_code_owner_code[image:hand.png[] *110*: Add other properties for Owner]
+** xref:fleshing-out-the-owner-entity.adoc#_using_specifications_to_encapsulate_business_rules[image:hand.png[] *120*: Using specifications to encapsulate business rules]
+
+* xref:prototyping.adoc[Prototyping]
+** xref:prototyping.adoc#_fixture_scripts_for_owner[image:hand.png[] *130*: Fixture Scripts (for Owner)]
+** xref:prototyping.adoc#_run_with_a_different_manifest[image:hand.png[] *140*: Run with a different manifest]
+
+* xref:adding-the-remaining-classes.adoc[Adding the remaining classes]
+** xref:adding-the-remaining-classes.adoc#_newpet_action_and_code_pet_code_to_code_owner_code_association[image:hand.png[] *150*: `newPet` action, `Pet` to `Owner`]
+** xref:adding-the-remaining-classes.adoc#_collection_of_code_pet_code_s[image:hand.png[] *160*: Collection of Pets)]
+** xref:adding-the-remaining-classes.adoc#_extend_our_fixture[image:hand.png[] *170*: Extend our Fixtures]
+** xref:adding-the-remaining-classes.adoc#_adding_code_visit_code[image:hand.png[] *180*: Adding Visit]
+
+* xref:business-rules-and-unit-testing.adoc[Business Rules & (Unit) Testing]
+** xref:business-rules-and-unit-testing.adoc#_defaults_and_code_clockservice_code[image:hand.png[] *190*: Defaults, and ClockService]
+** xref:business-rules-and-unit-testing.adoc#_unit_tests[image:hand.png[] *200*: Unit Tests]
+** xref:business-rules-and-unit-testing.adoc#_validation[image:hand.png[] *210*: Validation]
+
+* xref:modularity.adoc[Modularity]
+** xref:modularity.adoc#_introducing_packages[image:hand.png[] *220*: Introducing Packages]
+** xref:modularity.adoc#_inverting_responsibilities_refactoring_the_code_pet_code_s_visits[image:hand.png[] *230*: Inverting responsibilities (Refactoring the Pet's visits)]
+** xref:modularity.adoc#_pet_s_visits_a_contributed_collection[image:hand.png[] *240*: Pet’s visits (a contributed collection)]
+** xref:modularity.adoc#_events[image:hand.png[] *250*: Events]
+
+* xref:view-models.adoc[View Models]
+** xref:view-models.adoc#_dashboard[image:hand.png[] *260*: Dashboard]
+
+* xref:integration-testing.adoc[(Integration) Testing]
+** xref:integration-testing.adoc#_an_improved_fixture_script[image:hand.png[] *270*: An improved Fixture Script]
+** xref:integration-testing.adoc#_writing_integration_tests[image:hand.png[] *280*: Writing Integration Tests]
+** xref:integration-testing.adoc#_factor_out_abstract_integration_test[image:hand.png[] *290*: Factor out abstract integration test]
+** xref:integration-testing.adoc#_move_teardowns_to_modules[image:hand.png[] *300*: Move teardowns to modules]
+** xref:integration-testing.adoc#_fake_data_service[image:hand.png[] *310*: Fake Data Service]
+** xref:integration-testing.adoc#_extend_the_fixture_script_to_set_up_visits[image:hand.png[] *320*: Extend the Fixture script to set up visits]
+
+* xref:adding-further-business-logic-worked-examples.adoc[Further business logic]
+** xref:adding-further-business-logic-worked-examples.adoc#_enter_an_outcome[image:hand.png[] *330*: Enter an outcome]
+** xref:adding-further-business-logic-worked-examples.adoc#_pay_for_a_visit[image:hand.png[] *340*: Pay for a visit]
+** xref:adding-further-business-logic-worked-examples.adoc#_prevent_payment_for_a_visit_twice[image:hand.png[] *350*: Prevent payment for a visit twice ("see it? use it? do it?")]
+** xref:adding-further-business-logic-worked-examples.adoc#_find_code_visit_code_s_not_yet_paid_and_overdue[image:hand.png[] *360*: Find Visits not yet paid and overdue]
+** xref:adding-further-business-logic-worked-examples.adoc#_digression_hiding_columns_in_tables[image:hand.png[] *370*: Digression: Hiding Columns in Tables]
+** xref:adding-further-business-logic-worked-examples.adoc#_another_digression_icons_and_css[image:hand.png[] *380*: Another Digression: Icons and CSS]
+** xref:adding-further-business-logic-worked-examples.adoc#_delete_an_code_owner_code_provided_no_unpaid_code_visit_code_s[image:hand.png[] *390*: Delete an Owner provided no unpaid Visits]
+
+//* xref:incode-platform.adoc[Incode Platform]
+//* xref:i18n.adoc[i18n]
diff --git a/antora/playbooks/site-tutorials.yml b/antora/playbooks/site-tutorials.yml
new file mode 100644
index 0000000..0c7ef78
--- /dev/null
+++ b/antora/playbooks/site-tutorials.yml
@@ -0,0 +1,86 @@
+#  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.
+
+site:
+  title: Apache Isis
+  url: https://isis.apache.org/
+  start_page: docs:ROOT:about.adoc
+
+content:
+  sources:
+
+# antora
+    - url: .
+      start_path: antora/components/docs # docs
+      branches: HEAD
+    - url: .
+      start_path: antora/components/tutorials # tutorials
+      branches: HEAD
+# examples
+    - url: .
+      start_path: examples/demo/domain/src/main/adoc # docs
+      branches: HEAD
+
+# mavendeps
+    - url: .
+      start_path: mavendeps/adoc # docs
+      branches: HEAD
+
+# starters
+    - url: .
+      start_path: starters/adoc # docs
+      branches: HEAD
+
+# tooling
+    - url: .
+      start_path: tooling/adoc # toc
+      branches: HEAD
+    - url: .
+      start_path: tooling/cli/adoc # cli
+      branches: HEAD
+
+
+
+
+ui:
+  bundle:
+#    url: ../isis-antora/build/ui-bundle.zip
+    url: https://github.com/apache/isis-antora/blob/master/build/ui-bundle.zip?raw=true
+    snapshot: true
+  supplemental_files: antora/supplemental-ui
+
+asciidoc:
+  extensions:
+    - asciidoctor-kroki # requires: npm i ... @asciidoctor/core@2.2.0 asciidoctor-kroki
+#    - asciidoctor-plantuml # requires: npm i ... asciidoctor-plantuml@1.5.0
+    - ./antora/lib/lorem
+    - ./antora/lib/jira-issue
+  attributes:
+    experimental: ""
+    idprefix: ""
+    idseparator: "-"
+    # kroki
+    # for more options see https://github.com/Mogztter/asciidoctor-kroki
+    kroki-default-format: svg # (default)
+    kroki-default-options: interactive # only available for SVG
+    kroki-fetch-diagram: true
+    kroki-server-url: https://kroki.io # (default)
+    page-isisprev: "2.0.0-M6"
+    page-isisrel: "2.0.0-M7"
+
+output:
+  dir: antora/target/site
diff --git a/antora/playbooks/site.yml b/antora/playbooks/site.yml
index 51e5ca0..387cbd8 100644
--- a/antora/playbooks/site.yml
+++ b/antora/playbooks/site.yml
@@ -48,6 +48,9 @@ content:
     - url: .
       start_path: antora/components/userguide # userguide
       branches: HEAD
+    - url: .
+      start_path: antora/components/tutorials # tutorials
+      branches: HEAD
 
 # api
     - url: .