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/10/06 17:46:25 UTC

[isis] branch ISIS-2873-petclinic updated: ISIS-2873: exercises

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


The following commit(s) were added to refs/heads/ISIS-2873-petclinic by this push:
     new c5445d0  ISIS-2873: exercises
c5445d0 is described below

commit c5445d04b7d41e2ef3790218e711a755b5ae2360
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Wed Oct 6 18:46:12 2021 +0100

    ISIS-2873: exercises
---
 .../tutorials/modules/petclinic/nav.adoc           |  67 ++-
 .../modules/petclinic/pages/040-pet-entity.adoc    |   9 +
 .../modules/petclinic/pages/050-visit-entity.adoc  |   5 +-
 .../modules/petclinic/pages/060-unit-testing.adoc  | 112 ++++
 .../pages/070-business-rules-and-unit-testing.adoc | 153 -----
 .../modules/petclinic/pages/070-modularity.adoc    | 158 ++++++
 .../modules/petclinic/pages/080-modularity.adoc    | 382 -------------
 .../modules/petclinic/pages/080-view-models.adoc   | 297 ++++++++++
 .../petclinic/pages/090-integration-testing.adoc   | 206 +++++++
 .../modules/petclinic/pages/090-view-models.adoc   |  97 ----
 .../petclinic/pages/100-integration-testing.adoc   | 628 ---------------------
 .../petclinic/pages/architecture-rules.adoc        |   6 +
 12 files changed, 831 insertions(+), 1289 deletions(-)

diff --git a/antora/components/tutorials/modules/petclinic/nav.adoc b/antora/components/tutorials/modules/petclinic/nav.adoc
index b3f84f2..c027dee 100644
--- a/antora/components/tutorials/modules/petclinic/nav.adoc
+++ b/antora/components/tutorials/modules/petclinic/nav.adoc
@@ -12,7 +12,7 @@
 * xref:020-the-petclinic-domain.adoc[The PetClinic Domain]
 ** xref:020-the-petclinic-domain.adoc#exercise-2-1-refactor-simpleobject-to-petowner[image:hand.png[] *2.1*: Refactor SimpleObject to PetOwner]
 
-* xref:030-petowner-entity.adoc[Fleshing out the PetOwner entity]
+* xref:030-petowner-entity.adoc[PetOwner entity]
 ** xref:030-petowner-entity.adoc#exercise-3-1-rename-petowners-name-property[image:hand.png[] *3.1*: Rename PetOwner's name property]
 ** xref:030-petowner-entity.adoc#exercise-3-2-add-petowners-firstname-property[image:hand.png[] *3.2*: Add PetOwner's firstName property]
 ** xref:030-petowner-entity.adoc#exercise-3-3-modify-petowners-updatename-action[image:hand.png[] *3.3*: Modify PetOwner's updateName action]
@@ -26,33 +26,44 @@
 ** xref:030-petowner-entity.adoc#exercise-3-11-column-orders[image:hand.png[] *3.11*: Column Orders]
 
 
-* xref:040-pet-entity.adoc[Adding the remaining classes]
-** xref:040-pet-entity.adoc#_newpet_action_and_code_pet_code_to_code_owner_code_association[image:hand.png[] *150*: `newPet` action, `Pet` to `PetOwner`]
-** xref:040-pet-entity.adoc#_collection_of_code_pet_code_s[image:hand.png[] *160*: Collection of Pets)]
-** xref:040-pet-entity.adoc#_extend_our_fixture[image:hand.png[] *170*: Extend our Fixtures]
-** xref:040-pet-entity.adoc#_adding_code_visit_code[image:hand.png[] *180*: Adding Visit]
-
-* xref:070-business-rules-and-unit-testing.adoc[Business Rules & (Unit) Testing]
-** xref:070-business-rules-and-unit-testing.adoc#_defaults_and_code_clockservice_code[image:hand.png[] *190*: Defaults, and ClockService]
-** xref:070-business-rules-and-unit-testing.adoc#_unit_tests[image:hand.png[] *200*: Unit Tests]
-** xref:070-business-rules-and-unit-testing.adoc#_validation[image:hand.png[] *210*: Validation]
-
-* xref:080-modularity.adoc[Modularity]
-** xref:080-modularity.adoc#_introducing_packages[image:hand.png[] *220*: Introducing Packages]
-** xref:080-modularity.adoc#_inverting_responsibilities_refactoring_the_code_pet_code_s_visits[image:hand.png[] *230*: Inverting responsibilities (Refactoring the Pet's visits)]
-** xref:080-modularity.adoc#_pet_s_visits_a_contributed_collection[image:hand.png[] *240*: Pet’s visits (a contributed collection)]
-** xref:080-modularity.adoc#_events[image:hand.png[] *250*: Events]
-
-* xref:090-view-models.adoc[View Models]
-** xref:090-view-models.adoc#_dashboard[image:hand.png[] *260*: Dashboard]
-
-* xref:100-integration-testing.adoc[(Integration) Testing]
-** xref:100-integration-testing.adoc#_an_improved_fixture_script[image:hand.png[] *270*: An improved Fixture Script]
-** xref:100-integration-testing.adoc#_writing_integration_tests[image:hand.png[] *280*: Writing Integration Tests]
-** xref:100-integration-testing.adoc#_factor_out_abstract_integration_test[image:hand.png[] *290*: Factor out abstract integration test]
-** xref:100-integration-testing.adoc#_move_teardowns_to_modules[image:hand.png[] *300*: Move teardowns to modules]
-** xref:100-integration-testing.adoc#_fake_data_service[image:hand.png[] *310*: Fake Data Service]
-** xref:100-integration-testing.adoc#_extend_the_fixture_script_to_set_up_visits[image:hand.png[] *320*: Extend the Fixture script to set up visits]
+* xref:040-pet-entity.adoc[Pet entity]
+** xref:040-pet-entity.adoc#exercise-4-1-pet-entitys-key-properties[image:hand.png[] *4.1*: Pet entity's key properties]
+** xref:040-pet-entity.adoc#exercise-4-2-add-petrepository[image:hand.png[] *4.2*: Add PetRepository]
+** xref:040-pet-entity.adoc#exercise-4-3-add-petowners-collection-of-pets[image:hand.png[] *4.3*: Add PetOwner's collection of Pets]
+** xref:040-pet-entity.adoc#exercise-4-4-add-pets-remaining-properties[image:hand.png[] *4.4*: Add Pet's remaining properties]
+** xref:040-pet-entity.adoc#exercise-4-5-digression-clean-up-casing-of-database-schema[image:hand.png[] *4.5*: Digression: clean-up casing of database schema]
+** xref:040-pet-entity.adoc#exercise-4-6-add-petowner-action-to-add-pets[image:hand.png[] *4.6*: Add PetOwner action to add Pets]
+** xref:040-pet-entity.adoc#exercise-4-7-add-pets-ui-customisation[image:hand.png[] *4.7*: Add Pet's UI customisation]
+** xref:040-pet-entity.adoc#exercise-4-8-update-fixture-script-using-pet-personas[image:hand.png[] *4.8*: Update fixture script using Pet personas]
+** xref:040-pet-entity.adoc#exercise-4-9-add-petowner-action-to-delete-a-pet[image:hand.png[] *4.9*: Add PetOwner action to delete a Pet]
+** xref:040-pet-entity.adoc#exercise-4-10-cleanup[image:hand.png[] *4.10*: Cleanup]
+
+* xref:050-visit-entity.adoc[Visit module and entity]
+** xref:050-visit-entity.adoc#exercise-5-1-the-visits-module[image:hand.png[] *5.1*: The visits module]
+** xref:050-visit-entity.adoc#exercise-5-2-visit-entitys-key-properties[image:hand.png[] *5.2*: Visit entity's key properties]
+** xref:050-visit-entity.adoc#exercise-5-3-book-visit-action[image:hand.png[] *5.3*: "Book Visit" action]
+
+
+* xref:060-unit-testing.adoc[Unit Testing]
+** xref:060-unit-testing.adoc#exercise-6-1-unit-test-the-default-time-when-booking-visits[image:hand.png[] *6.1*: Unit test the default time when booking visits]
+
+* xref:070-modularity.adoc[Modularity (domain events)]
+** xref:070-modularity.adoc#exercise-7-1-refactor-petowners-delete-action[image:hand.png[] *7.1*: refactor PetOwner's delete action]
+
+
+* xref:080-view-models.adoc[View Models]
+** xref:080-view-models.adoc#exercise-8-1-extend-the-home-page[image:hand.png[] *8.1*: Extend the Home Page.]
+** xref:080-view-models.adoc#exercise-8-2-add-a-convenience-action[image:hand.png[] *8.2*: Add a convenience action]
+** xref:080-view-models.adoc#exercise-8-3-using-a-view-model-as-a-projection-of-an-entity[image:hand.png[] *8.3*: Using a view model as a projection of an entity]
+
+
+* xref:090-integration-testing.adoc[(Integration) Testing]
+** xref:090-integration-testing.adoc#_an_improved_fixture_script[image:hand.png[] *270*: An improved Fixture Script]
+** xref:090-integration-testing.adoc#_writing_integration_tests[image:hand.png[] *280*: Writing Integration Tests]
+** xref:090-integration-testing.adoc#_factor_out_abstract_integration_test[image:hand.png[] *290*: Factor out abstract integration test]
+** xref:090-integration-testing.adoc#_move_teardowns_to_modules[image:hand.png[] *300*: Move teardowns to modules]
+** xref:090-integration-testing.adoc#_fake_data_service[image:hand.png[] *310*: Fake Data Service]
+** xref:090-integration-testing.adoc#_extend_the_fixture_script_to_set_up_visits[image:hand.png[] *320*: Extend the Fixture script to set up visits]
 
 * xref:110-adding-further-business-logic-worked-examples.adoc[Further business logic]
 ** xref:110-adding-further-business-logic-worked-examples.adoc#_enter_an_outcome[image:hand.png[] *330*: Enter an outcome]
diff --git a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
index 35fdf5c..bcb09ad 100644
--- a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
@@ -11,6 +11,7 @@ include::partial$domain.adoc[]
 In this set of exercises we'll focus on the `Pet` entity and its relationship with `PetOwner`.
 Each `PetOwner` will hold a collection of their ``Pet``s, with actions to add or remove `Pet` instances for that collection.
 
+[#exercise-4-1-pet-entitys-key-properties]
 == Exercise 4.1: Pet entity's key properties
 
 In this exercise we'll just create the outline of the `Pet` entity, and ensure it is mapped to the database correctly.
@@ -112,6 +113,7 @@ Run the application, and confirm that the table is created correctly using menu:
 
 
 
+[#exercise-4-2-add-petrepository]
 == Exercise 4.2: Add PetRepository
 
 We will need to find the ``Pet``s belonging to a `PetOwner`.
@@ -147,6 +149,7 @@ Confirm the application still runs
 
 
 
+[#exercise-4-3-add-petowners-collection-of-pets]
 == Exercise 4.3: Add PetOwner's collection of Pets
 
 In this next exercise we'll add the ``PetOwner``'s collection of ``Pet``s, using a xref:userguide:fun:mixins.adoc[mixin].
@@ -271,6 +274,7 @@ Run the application (or just reload the changed classes) and confirm the columns
 
 
 
+[#exercise-4-4-add-pets-remaining-properties]
 == Exercise 4.4: Add Pet's remaining properties
 
 In this exercise we'll add the remaining properties for `Pet`.
@@ -372,6 +376,7 @@ private String notes;
 Run the application and use menu:Prototyping[H2 Console] to confirm the database schema for `Pet` is as expected.
 
 
+[#exercise-4-5-digression-clean-up-casing-of-database-schema]
 == Exercise 4.5: Digression: clean-up casing of database schema
 
 Reviewing the tables in the database we can see that we have a mix between lower- and upper-case table and column names.
@@ -398,6 +403,7 @@ mvn -pl spring-boot:run
 
 
 
+[#exercise-4-6-add-petowner-action-to-add-pets]
 == Exercise 4.6: Add PetOwner action to add Pets
 
 In this exercise we'll bring in the capability to add ``Pet``s, as a responsibility of `PetOwner`.
@@ -643,6 +649,7 @@ image::04-07/download-layout-xml.png[width=400]
 
 
 
+[#exercise-4-8-update-fixture-script-using-pet-personas]
 == Exercise 4.8: Update fixture script using Pet personas
 
 By now you are probably tiring of continually creating a Pet in order to perform your tests.
@@ -799,6 +806,7 @@ public class PetClinicDemo extends FixtureScript {
 
 
 
+[#exercise-4-9-add-petowner-action-to-delete-a-pet]
 == Exercise 4.9: Add PetOwner action to delete a Pet
 
 We will probably also need to delete an action to delete a `Pet` (though once there are associated ``Visit``s for a `Pet`, we'll need to disable this action).
@@ -938,6 +946,7 @@ public class PetOwner_removePets {                      // <.>
 
 
 
+[#exercise-4-10-cleanup]
 == Exercise 4.10: Cleanup
 
 Reviewing the contents of the `pets` module, we can see (in the solutions provided at least) that there are a few thing that still need some attention:
diff --git a/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
index 7951c88..06687d1a 100644
--- a/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
@@ -12,6 +12,7 @@ Also, note that the `Visit` entity is in its own module.
 We'll look at the important topic of modularity in later exercises.
 
 
+[#exercise-5-1-the-visits-module]
 == Exercise 5.1: The visits module
 
 In this exercise we'll just create an empty visits module.
@@ -70,6 +71,7 @@ It still depends upon `PetsModule`, but now as a transitive dependency.
 
 
 
+[#exercise-5-2-visit-entitys-key-properties]
 == Exercise 5.2: Visit entity's key properties
 
 Now we have a visits module, we can now add in the `Visit` entity.
@@ -165,7 +167,8 @@ Run the application, and confirm that the table is created correctly using menu:
 
 
 
-== Exercise 5.2: "Book Visit" action
+[#exercise-5-3-book-visit-action]
+== Exercise 5.3: "Book Visit" action
 
 In addition to the key properties, the `Visit` has one further mandatory property, `reason`.
 This is required to be specified when a `Visit` is created ("what is the purpose of this visit?")
diff --git a/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc b/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc
new file mode 100644
index 0000000..96f4c6c
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc
@@ -0,0 +1,112 @@
+= 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.
+
+
+
+[#exercise-6-1-unit-test-the-default-time-when-booking-visits]
+== Exercise 6.1: Unit test the default time when booking visits
+
+The xref:050-visit-entity.adoc#exercise-5-3-book-visit-action["Book Visit"] action has a default time of 9am the next morning.
+In this section we'll write a unit test to verify this logic, using Mockito to "mock the clock".
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/06-01-unit-test-bookVisit-default-time
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+=== Tasks
+
+* add test dependencies to the `petclinic-module-visits` maven module:
++
+[source,xml]
+.module-visits/pom.xml
+----
+<dependencies>
+    <!-- ... -->
+
+    <dependency>
+        <groupId>org.apache.isis.mavendeps</groupId>
+        <artifactId>isis-mavendeps-unittests</artifactId>
+        <type>pom</type>
+        <scope>test</scope>
+        <exclusions>
+            <exclusion>
+                <groupId>org.jmock</groupId>
+                <artifactId>jmock-junit4</artifactId>
+            </exclusion>
+        </exclusions>
+    </dependency>
+
+</dependencies>
+----
+
+* add the test:
+
+[source,java]
+----
+@ExtendWith(MockitoExtension.class)                                             // <.>
+class Pet_bookVisit_Test {
+
+    @Mock ClockService mockClockService;                                        // <.>
+    @Mock VirtualClock mockVirtualClock;                                        // <2>
+
+    @BeforeEach
+    void setup() {
+        Mockito.when(mockClockService.getClock()).thenReturn(mockVirtualClock); // <.>
+    }
+
+    @Nested
+    class default0 {
+
+        @Test
+        void defaults_to_9am_tomorrow_morning() {
+
+            // given
+            Pet_bookVisit mixin = new Pet_bookVisit(null);
+            mixin.clockService = mockClockService;                              // <.>
+
+            LocalDateTime now = LocalDateTime.of(2021, 10, 21, 16, 37, 45);
+
+            // expecting
+            Mockito.when(mockVirtualClock.nowAsLocalDateTime()).thenReturn(now);// <.>
+
+            // when
+            LocalDateTime localDateTime = mixin.default0Act();
+
+            // then
+            Assertions.assertThat(localDateTime)                                // <.>
+                    .isEqualTo(LocalDateTime.of(2021,10,22,9,0,0));
+        }
+    }
+}
+----
+
+<.> Instructs JUnit to use Mockito for mocking.
+<.> mocks the `ClockService`, and mocks the `VirtualClock` returned by the `ClockService`.
+Automatically provisioned by Mockito.
+<.> makes the mock `ClockService` return the mock `VirtualClock`.
+<.> inject the mock clock into the domain object
+<.> sets up expectations for this scenario on the mock `VirtualClock`
+<.> use link:http://joel-costigliola.github.io/assertj/[AssertJ] to assert the expected value
+
+IMPORTANT: Unit tests should have a suffix "_Test", to distinguish them from integration tests.
+The top-level pom configures Maven surefire to run the unit tests first and then integration tests as a separate execution.
+
+=== Optional Exercises
+
+NOTE: If you decide to do these optional exercises, make the changes on a git branch so that you can resume with the main flow of exercises later.
+
+. Write a similar unit test to verify the validation logic that visits cannot be in the past.
+
+. Introduce a meta-annotation `@VisitedAt`, and move the validation logic into a xref:refguide:applib-classes:spec.adoc#specification[Specification].
+Verify that the app still works, and write a unit test to check your specification.
diff --git a/antora/components/tutorials/modules/petclinic/pages/070-business-rules-and-unit-testing.adoc b/antora/components/tutorials/modules/petclinic/pages/070-business-rules-and-unit-testing.adoc
deleted file mode 100644
index 5914c4e..0000000
--- a/antora/components/tutorials/modules/petclinic/pages/070-business-rules-and-unit-testing.adoc
+++ /dev/null
@@ -1,153 +0,0 @@
-= 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::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::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/070-modularity.adoc b/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc
new file mode 100644
index 0000000..932d6e0
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc
@@ -0,0 +1,158 @@
+= 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 framework provides two main tools:
+
+* the first we've already seen is mixins.
++
+These allow us to locate busines logic in one module that "appears" to reside in another module.
+Examples are the `visits` mixin collection and `bookVisit` mixin action that are both contributed by the `visits` module to the `Pet` entity in the `pets` module.
+
+* the second is domain events.
++
+These we haven't yet seen, but provide a way for one module to react to (or to veto) actions performed in logic in another module.
+
+In this part of the tutorial we'll look at domain events.
+
+
+
+[#exercise-7-1-refactor-petowners-delete-action]
+== Exercise 7.1: refactor PetOwner's delete action
+
+Currently the `delete` action for `PetOwner` is implemented as a mixin within the `Pet` package.
+That's a nice place for that functionality, because it can delete any `Pet`s for the `PetOwner` if any exist.
+
+However, we also have added `Visit`, which has the same issue: we cannot delete a `Pet` if there are associated ``Visit``s.
+And, in fact, we don't want to allow a `PetOwner` and their ``Pet``s from being deleted if there are ``Visit``s in the database; they might not have paid!
+
+In this exercise we will move the responsibility to delete an action back to `PetOwner`, and then use subscribers for both `Pet` and `Visit` to cascade delete or to veto the action respectively if there are related objects.
+
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/07-01-delete-action-events
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+To test this out:
+
+* try deleting a `PetOwner` where none of their ``Pet``s have any ``Visit``s; the action should succeed, and the `PetOwner` and the ``Pet``s should all be deleted.
+
+* now book a `Visit` for a `Pet`, then navigate back to the parent `PetOwner` and attempt to delete it.
+This time the action should be vetoed, because of that `Visit`.
+
+
+=== Tasks
+
+* in `PetOwner_delete` remove the code that deletes the ``Pet``s.
+In its place, define a subclass of xref:refguide:applib-classes:events.adoc#domain-event-classes[ActionDomainEvent] as a nested class of the mixin, and reference in the xref:refguide:applib:index/annotation/Action.adoc#domainEvent[@Action#domainEvent] attribute.
++
+[source,java]
+.PetOwner_delete.java
+----
+@Action(
+        domainEvent = PetOwner_delete.ActionEvent.class,            // <.>
+        semantics = SemanticsOf.NON_IDEMPOTENT_ARE_YOU_SURE,
+        commandPublishing = Publishing.ENABLED,
+        executionPublishing = Publishing.ENABLED
+)
+@ActionLayout(
+        associateWith = "name", position = ActionLayout.Position.PANEL,
+        describedAs = "Deletes this object from the persistent datastore")
+@RequiredArgsConstructor
+public class PetOwner_delete {
+
+    public static class ActionEvent                                 // <.>
+            extends ActionDomainEvent<PetOwner_delete>{}
+
+    private final PetOwner petOwner;
+
+    public void act() {
+        repositoryService.remove(petOwner);
+        return;
+    }
+
+    @Inject RepositoryService repositoryService;
+}
+----
+<.> specifies the domain event to emit when the action is called
+<.> declares the action event (as a subclass of the framework's xref:refguide:applib-classes:events.adoc#domain-event-classes[ActionDomainEvent]).
+
+* create a subscriber in the `pets` package to delete all ``Pet``s when the `PetOwner_delete` action is invoked:
++
+[source,java]
+.PetOwnerForPetsSubscriber.java
+----
+@Service
+public class PetOwnerForPetsSubscriber {
+
+    @EventListener(PetOwner_delete.ActionEvent.class)
+    public void on(PetOwner_delete.ActionEvent ev) {
+        switch(ev.getEventPhase()) {
+            case EXECUTING:                                             // <.>
+                PetOwner petOwner = ev.getSubject();                    // <.>
+                List<Pet> pets = petRepository.findByPetOwner(petOwner);
+                pets.forEach(repositoryService::remove);
+                break;
+        }
+    }
+
+    @Inject PetRepository petRepository;
+    @Inject RepositoryService repositoryService;
+}
+----
+<.> events are emitted at different phases.
+The `EXECUTING` phase is fired before the delete action itself is fired, so is the ideal place for us to perform the cascade delete.
+<.> is the mixee of the mixin that is emitting the event.
+
+* create a subscriber in the `visits` module to veto the `PetOwner_delete` if there are any `Pet`s of the `PetOwner` with at least one `Visit`:
++
+[source,java]
+.PetOwnerForVisitsSubscriber.java
+----
+@Service
+public class PetOwnerForVisitsSubscriber {
+
+    @EventListener(PetOwner_delete.ActionEvent.class)
+    public void on(PetOwner_delete.ActionEvent ev) {
+        switch(ev.getEventPhase()) {
+            case DISABLE:
+                PetOwner petOwner = ev.getSubject();
+                List<Pet> pets = petRepository.findByPetOwner(petOwner);
+                for (Pet pet : pets) {
+                    List<Visit> visits = visitRepository.findByPetOrderByVisitAtDesc(pet);
+                    int numVisits = visits.size();
+                    if(numVisits > 0) {
+                        ev.disable(String.format("%s has %d visit%s",
+                                titleService.titleOf(pet),
+                                numVisits,
+                                numVisits != 1 ? "s" : ""));
+                    }
+                }
+                break;
+        }
+    }
+
+    @Inject TitleService titleService;
+    @Inject VisitRepository visitRepository;
+    @Inject PetRepository petRepository;
+}
+----
+
+
+=== Optional Exercise
+
+
+Improve the implementation of `PetOwnerForVisitsSubscriber` so that it performs only a single database query to find if there are any ``Visit`` for the `PetOwner`.
+
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/080-modularity.adoc b/antora/components/tutorials/modules/petclinic/pages/080-modularity.adoc
deleted file mode 100644
index d69955f..0000000
--- a/antora/components/tutorials/modules/petclinic/pages/080-modularity.adoc
+++ /dev/null
@@ -1,382 +0,0 @@
-= 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::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 `PetOwner` 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 `PetOwner` 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 `PetOwner` 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/080-view-models.adoc b/antora/components/tutorials/modules/petclinic/pages/080-view-models.adoc
new file mode 100644
index 0000000..4c0bd8f
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/080-view-models.adoc
@@ -0,0 +1,297 @@
+= 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.
+
+
+
+[#exercise-8-1-extend-the-home-page]
+== Exercise 8.1: Extend the Home Page.
+
+In this exercise we'll extend the home page by displaying additional data in new collections.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/08-01-home-page-additional-collections
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+=== Tasks
+
+* update `PetRepository` and `VisitRepository` to extend `JpaRepository` (rather than simply `Repository`)
++
+This will provide additional finders "for free".
+
+* modify `HomePageViewModel` to show the current ``PetOwner``s, ``Pet``s and ``Visit``s in three separate columns:
++
+[source,java]
+----
+@DomainObject(
+        nature = Nature.VIEW_MODEL,                             // <.>
+        logicalTypeName = "petclinic.HomePageViewModel"
+        )
+@HomePage                                                       // <.>
+@DomainObjectLayout()
+public class HomePageViewModel {
+
+    public String title() {
+        return getPetOwners().size() + " owners";
+    }
+
+    public List<PetOwner> getPetOwners() {                      // <.>
+        return petOwnerRepository.findAll();
+    }
+    public List<Pet> getPets() {                                // <.>
+        return petRepository.findAll();
+    }
+    public List<Visit> getVisits() {                            // <.>
+        return visitRepository.findAll();
+    }
+
+    @Inject PetOwnerRepository petOwnerRepository;
+    @Inject PetRepository petRepository;
+    @Inject VisitRepository visitRepository;
+}
+----
+<.> indicates that this is a view model.
+<.> exactly one view model can be annotated as the xref:refguide:applib:index/annotation/HomePage.adoc[@HomePage]
+<.> renamed derived collection, returns ``PetOwner``s.
+<.> new derived collection returning all ``Pet``s.
+<.> new derived collection returning all ``Visits``s.
+
+* update the `HomePageViewModel.layout.xml`:
++
+[source,xml]
+.HomePageViewModel.layout.xml
+----
+<!-- ... -->
+    <bs3:row>
+        <bs3:col span="12" unreferencedCollections="true">
+            <bs3:row>
+                <bs3:col span="4">
+                    <collection id="petOwners" defaultView="table"/>
+                </bs3:col>
+                <bs3:col span="4">
+                    <collection id="pets" defaultView="table"/>
+                </bs3:col>
+                <bs3:col span="4">
+                    <collection id="visits" defaultView="table"/>
+                </bs3:col>
+            </bs3:row>
+        </bs3:col>
+    </bs3:row>
+<!-- ... -->
+----
+
+* update or add columnOrder.txt files for the 3 collections.
+
+
+
+[#exercise-8-2-add-a-convenience-action]
+== Exercise 8.2: Add a convenience action
+
+View models can have behaviour (actions), the same as entities.
+In this exercise we'll extend the home page by providing a convenience action to book a `Visit` for any `Pet` of any `PetOwner`.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/08-02-home-page-bookVisit-convenience-action
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+=== Tasks
+
+* create a bookVisit action for HomePageViewModel, as a mixin:
++
+[source,java]
+.HomePageViewModel_bookVisit.java
+----
+@Action                                                                                 // <.>
+@RequiredArgsConstructor
+public class HomePageViewModel_bookVisit {                                              // <.>
+
+    final HomePageViewModel homePageViewModel;
+
+    public Object act(
+            PetOwner petOwner, Pet pet, LocalDateTime visitAt, String reason,
+            boolean showVisit) {                                                               // <.>
+        Visit visit = wrapperFactory.wrapMixin(Pet_bookVisit.class, pet).act(visitAt, reason); // <.>
+        return showVisit ? visit : homePageViewModel;
+    }
+    public List<PetOwner> autoComplete0Act(final String lastName) {                     // <.>
+        return petOwnerRepository.findByLastNameContaining(lastName);
+    }
+    public List<Pet> choices1Act(PetOwner petOwner) {                                   // <.>
+        if(petOwner == null) return Collections.emptyList();
+        return petRepository.findByPetOwner(petOwner);
+    }
+    public LocalDateTime default2Act(PetOwner petOwner, Pet pet) {                      // <.>
+        if(pet == null) return null;
+        return factoryService.mixin(Pet_bookVisit.class, pet).default0Act();
+    }
+    public String validate2Act(PetOwner petOwner, Pet pet, LocalDateTime visitAt) {     // <.>
+         return factoryService.mixin(Pet_bookVisit.class, pet).validate0Act(visitAt);
+    }
+
+    @Inject PetRepository petRepository;
+    @Inject PetOwnerRepository petOwnerRepository;
+    @Inject WrapperFactory wrapperFactory;
+    @Inject FactoryService factoryService;
+}
+----
+<.> declares this class as a mixin action.
+<.> The action name is derived from the mixin's class ("bookVisit").
+<.> cosmetic flag to control the UI; either remain at the home page or navigate to the newly created `Visit
+<.> use the xref:refguide:applib:index/services/wrapper/WrapperFactory.adoc[WrapperFactory] to delegate to the original behaviour "as if" through the UI.
+If additional business rules were added to that delegate, then the mistake would be detected.
+<.> Uses an xref:refguide:applib-methods:prefixes.adoc#autoComplete[autoComplete] supporting method to look up matching ``PetOwner``s based upon their name.
+<.> Finds the ``Pet``s owned by the `PetOwner`, once selected.
+<.> Computes a default for the 2^nd^ parameter, once the first two are selected.
+<.> surfaces (some of) the business rules of the delegate mixin.
+
+* update the layout file to position:
++
+[source,xml]
+.HomePageViewModel.layout.xml
+----
+<!-- ... -->
+    <bs3:row>
+        <bs3:col span="12" unreferencedActions="true">
+            <domainObject/>
+            <action id="bookVisit"/>
+            <!-- ... -->
+        </bs3:col>
+    </bs3:row>
+<!-- ... -->
+----
+
+
+
+[#exercise-8-3-using-a-view-model-as-a-projection-of-an-entity]
+== Exercise 8.3: Using a view model as a projection of an entity
+
+In the home page, the ``Visit`` instances show the `Pet` but they do not show the `PetOwner`.
+One option (probably the correct one in this case) would be to extend `Visit` itself and show this derived information:
+
+[source,java]
+.Visit.java
+----
+public PetOwner getPetOwner() {
+    return getPet().getOwner();
+}
+----
+
+Alternatively, if we didn't want to "pollute" the entity with this derived property, we could use a mixin:
+
+[source,java]
+.Visit_petOwner.java
+----
+@Property
+@RequiredArgsConstructor
+public class Visit_petOwner {
+
+    final Visit visit;
+
+    public PetOwner prop() {
+        return visit.getPet().getOwner();
+    }
+}
+----
+
+Even so, this would still make the "petOwner" property visible everywhere that a `Visit` is displayed.
+
+If we instead want to be more targetted and _only_ show this "petOwner" property when displayed on the HomePage, yet another option is to implement the xref:refguide:applib:index/services/tablecol/TableColumnVisibilityService.adoc[TableColumnVisibilityService] SPI.
+This provides the context for where an object is being rendered, so this could be used to suppress the collection everywhere except the home page.
+
+A final option though, which we'll use in this exercise, is to display not the entity itself but instead a view model that "wraps" the entity and supplements with the additional data required.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/08-03-view-model-projecting-an-entity
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+=== Tasks
+
+* create a JAXB style view model `VisitPlusPetOwner`, wrapping the `Visit` entity:
++
+[source,java]
+.VisitPlusPetOwner.java
+----
+@DomainObject(nature=Nature.VIEW_MODEL, logicalTypeName = "petclinic.VisitPlusPetOwner")
+@DomainObjectLayout(named = "Visit")
+@XmlRootElement                                                     // <.>
+@XmlType                                                            // <1>
+@XmlAccessorType(XmlAccessType.FIELD)                               // <1>
+@NoArgsConstructor
+public class VisitPlusPetOwner {
+
+    @Property(
+            projecting = Projecting.PROJECTED,                      // <.>
+            hidden = Where.EVERYWHERE                               // <.>
+    )
+    @Getter
+    private Visit visit;
+
+    VisitPlusPetOwner(Visit visit) {this.visit = visit;}
+
+    public Pet getPet() {return visit.getPet();}                    // <.>
+    public String getReason() {return visit.getReason();}           // <4>
+    public LocalDateTime getVisitAt() {return visit.getVisitAt();}  // <4>
+
+    public PetOwner getPetOwner() {                                 // <.>
+        return getPet().getPetOwner();
+    }
+}
+----
+<.> Boilerplate for JAXB view models
+<.> if the icon/title is clicked, then traverse to this object rather than the view model.
+(The view model is a "projection" of the underlying `Visit`).
+<.> Nevertheless, hide this property from the UI.
+<.> expose properties from the underlying `Visit` entity
+<.> add in additional derived properties, in this case the ``Pet``'s owner.
+
+* Refactor the `getVisits` collection of `HomePageViewModel` to use the new view model:
++
+[source,java]
+.VisitPlusPetOwner.java
+----
+public List<VisitPlusPetOwner> getVisits() {
+    return visitRepository.findAll()
+            .stream()
+            .map(VisitPlusPetOwner::new)
+            .collect(Collectors.toList());
+}
+----
+
+* update the columnOrder file for this collection to display the new property:
++
+[source,java]
+.HomePageViewModel#visits.columnOrder.txt
+----
+petOwner
+pet
+visitAt
+----
+
+Run the application; the `visits` collection on the home page should now show the `PetOwner` as an additional column, but otherwise behaves the same as previously.
diff --git a/antora/components/tutorials/modules/petclinic/pages/090-integration-testing.adoc b/antora/components/tutorials/modules/petclinic/pages/090-integration-testing.adoc
new file mode 100644
index 0000000..aad77d1
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/090-integration-testing.adoc
@@ -0,0 +1,206 @@
+= 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 integration tests are at least as important, probably more so, as they exercise the entire application from an end-users perspective, rather than an individual part.
+
+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 the xref:refguide:applib:index/services/wrapper/WrapperFactory.adoc[WrapperFactory] domain service which simulates the user interface in a type-safe way.
+
+
+== Exercuse 9.1: Testing bookVisit using an integtest
+
+In this exercise we'll test the `bookVisit` mixin action (on `Pet` entity).
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/09-01-bookVisit-integ-test
+mvn clean package jetty:run
+----
+
+=== Tasks
+
+* in the `pom.xml` of the visits module, add the following dependency:
++
+[source,xml]
+.module-visits/pom.xml
+----
+<dependency>
+    <groupId>org.apache.isis.mavendeps</groupId>
+    <artifactId>isis-mavendeps-integtests</artifactId>
+    <type>pom</type>
+    <scope>test</scope>
+</dependency>
+----
+
+* add an abstract class `VisitsModuleIntegTestAbstract` for the `visits` module, for other integ tests to subclass:
++
+[source,java]
+.VisitsModuleIntegTestAbstract.java
+----
+@SpringBootTest(
+        classes = VisitsModuleIntegTestAbstract.TestApp.class
+)
+@ActiveProfiles("test")
+public abstract class VisitsModuleIntegTestAbstract
+        extends IsisIntegrationTestAbstractWithFixtures {
+
+    @SpringBootConfiguration
+    @EnableAutoConfiguration
+    @Import({
+
+            IsisModuleCoreRuntimeServices.class,
+            IsisModuleSecurityBypass.class,
+            IsisModulePersistenceJpaEclipselink.class,
+            IsisModuleTestingFixturesApplib.class,
+
+            VisitsModule.class
+    })
+    @PropertySources({
+            @PropertySource(IsisPresets.H2InMemory_withUniqueSchema),
+            @PropertySource(IsisPresets.UseLog4j2Test),
+    })
+    public static class TestApp {
+    }
+}
+----
+
+* also update the `application-test.yml` file for the `visits` module, to ensure that the database schemas for both modules are created:
++
+[source,yaml]
+.module-visits/src/test/resources/application-test.yml
+----
+isis:
+  persistence:
+    schema:
+      auto-create-schemas: pets,visits
+----
+
+* add a class `Bootstrap_IntegTest` integration test, inheriting from the `VisitsModuleIntegTestAbstract:
++
+[source,java]
+.Bootstrap_IntegTest.java
+----
+public class Bootstrap_IntegTest extends VisitsModuleIntegTestAbstract {
+
+    @Test
+    public void checks_can_bootstrap() {}
+}
+----
++
+Make sure this test runs and passes in both the IDE and using "mvn clean install".
+
+
+Now we can write our actual test:
+
+* Now add a class `Pet_bookVisit_IntegTest` integration test, also inheriting from the `VisitsModuleIntegTestAbstract:
++
+[source,java]
+.Pet_bookVisit_IntegTest.java
+----
+public class Pet_bookVisit_IntegTest extends VisitsModuleIntegTestAbstract {
+
+    @BeforeEach
+    void setup() {
+        fixtureScripts.run(new Pet_persona.PersistAll());                       // <.>
+    }
+
+    @Test
+    public void happy_case() {
+
+        // given
+        Pet somePet = fakeDataService.enums().anyOf(Pet_persona.class)          // <.>
+                        .findUsing(serviceRegistry);
+        List<Visit> before = visitRepository.findByPetOrderByVisitAtDesc(somePet);
+
+        // when
+        LocalDateTime visitAt = clockService.getClock().nowAsLocalDateTime()    // <.>
+                                    .plusDays(fakeDataService.ints().between(1, 3));
+        String reason = fakeDataService.strings().upper(40);                    // <3>
+
+        wrapMixin(Pet_bookVisit.class, somePet).act(visitAt, reason);           // <.>
+
+        // then
+        List<Visit> after = visitRepository.findByPetOrderByVisitAtDesc(somePet);
+        after.removeAll(before);
+        assertThat(after).hasSize(1);                                           // <.>
+        Visit visit = after.get(0);
+
+        assertThat(visit.getPet()).isSameAs(somePet);                           // <.>
+        assertThat(visit.getVisitAt()).isEqualTo(visitAt);                      // <6>
+        assertThat(visit.getReason()).isEqualTo(reason);                        // <6>
+    }
+
+    @Inject FakeDataService fakeDataService;
+    @Inject VisitRepository visitRepository;
+    @Inject ClockService clockService;
+
+}
+----
+<.> uses same fixture script used for prototyping to set up ``Pet``s and their ``PetOwner``s.
+<.> uses the xref:refguide:testing:index/fakedata/applib/services/FakeDataService.adoc[FakeDataService] to select a `Pet` persona at random and uses that person to look up the corresponding domain object.
+<.> sets up some randomised but valid argument values
+<.> invokes the action, using the xref:refguide:applib:index/services/wrapper/WrapperFactory.adoc[WrapperFactory] to simulate the UI
+<.> asserts that one new `Visit` has been created for the `Pet`.
+<.> asserts that the state of this new `Visit` is correct
++
+Run the test and check that it passes.
+
+* write an error scenario which checks that a reason has been provided:
++
+[source,java]
+.Pet_bookVisit_IntegTest.java
+----
+@Test
+public void reason_is_required() {
+
+    // given
+    Pet somePet = fakeDataService.enums().anyOf(Pet_persona.class)
+                    .findUsing(serviceRegistry);
+    List<Visit> before = visitRepository.findByPetOrderByVisitAtDesc(somePet);
+
+    // when, then
+    LocalDateTime visitAt = clockService.getClock().nowAsLocalDateTime()
+                                .plusDays(fakeDataService.ints().between(1, 3));
+
+    assertThatThrownBy(() ->
+        wrapMixin(Pet_bookVisit.class, somePet).act(visitAt, null)
+    )
+    .isInstanceOf(InvalidException.class)
+    .hasMessage("'Reason' is mandatory");
+}
+----
+
+* write an error scenario which checks that the `visitAt` date cannot be in the past:
++
+[source,java]
+.Pet_bookVisit_IntegTest.java
+----
+@Test
+public void cannot_book_in_the_past() {
+
+    // given
+    Pet somePet = fakeDataService.enums().anyOf(Pet_persona.class)
+            .findUsing(serviceRegistry);
+    List<Visit> before = visitRepository.findByPetOrderByVisitAtDesc(somePet);
+
+    // when, then
+    LocalDateTime visitAt = clockService.getClock().nowAsLocalDateTime();
+    String reason = fakeDataService.strings().upper(40);
+
+    assertThatThrownBy(() ->
+            wrapMixin(Pet_bookVisit.class, somePet).act(visitAt, reason)
+    )
+            .isInstanceOf(InvalidException.class)
+            .hasMessage("Must be in the future");
+}
+----
+
+*
+
+
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/090-view-models.adoc b/antora/components/tutorials/modules/petclinic/pages/090-view-models.adoc
deleted file mode 100644
index 06a718c..0000000
--- a/antora/components/tutorials/modules/petclinic/pages/090-view-models.adoc
+++ /dev/null
@@ -1,97 +0,0 @@
-= 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::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/modules/petclinic/pages/100-integration-testing.adoc b/antora/components/tutorials/modules/petclinic/pages/100-integration-testing.adoc
deleted file mode 100644
index b69467b..0000000
--- a/antora/components/tutorials/modules/petclinic/pages/100-integration-testing.adoc
+++ /dev/null
@@ -1,628 +0,0 @@
-= (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 `PetOwnerBuilderScript`.
-This knows "how to" create an `PetOwner` 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 `PetOwner_enum` enum.
-The "what"s are the  enum instances, each delegating the actual creation to the `PetOwnerBuilderScript`:
-+
-[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 `PetOwner_enum` implement a further interface.
-
-* first, extend `PetOwners` 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 `PetOwner_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 ``PetOwner``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 `PetOwnerBuilderScript`
-
-** 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 `PetOwner_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/architecture-rules.adoc b/antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc
new file mode 100644
index 0000000..257a961
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc
@@ -0,0 +1,6 @@
+= architecture rules
+
+: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.
+