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/03 11:52:14 UTC

[isis] branch ISIS-2873-petclinic updated: ISIS-2873: ex 4.1 and 4.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


The following commit(s) were added to refs/heads/ISIS-2873-petclinic by this push:
     new 6739960  ISIS-2873: ex 4.1 and 4.2
6739960 is described below

commit 67399600d40d1840ba9d46a1772e97c6cacbea8d
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Oct 3 12:52:06 2021 +0100

    ISIS-2873: ex 4.1 and 4.2
---
 .../tutorials/modules/petclinic/nav.adoc           |  34 +-
 .../petclinic/pages/020-the-petclinic-domain.adoc  |  66 +--
 ...-owner-entity.adoc => 030-petowner-entity.adoc} |  17 +-
 .../modules/petclinic/pages/040-pet-entity.adoc    | 506 +++++++++++++++++++++
 .../modules/petclinic/pages/050-prototyping.adoc   | 190 --------
 .../modules/petclinic/pages/050-visit-entity.adoc  | 154 +++++++
 .../pages/060-adding-the-remaining-classes.adoc    | 442 ------------------
 .../modules/petclinic/pages/080-modularity.adoc    |   6 +-
 .../petclinic/pages/100-integration-testing.adoc   |  20 +-
 ...ing-further-business-logic-worked-examples.adoc |  24 +-
 .../modules/petclinic/partials/domain.adoc         |  60 +++
 .../modules/petclinic/partials/skinparam.adoc      |  11 +
 12 files changed, 790 insertions(+), 740 deletions(-)

diff --git a/antora/components/tutorials/modules/petclinic/nav.adoc b/antora/components/tutorials/modules/petclinic/nav.adoc
index e69f132..b3f84f2 100644
--- a/antora/components/tutorials/modules/petclinic/nav.adoc
+++ b/antora/components/tutorials/modules/petclinic/nav.adoc
@@ -10,25 +10,27 @@
 ** xref:010-getting-started.adoc#exercise-1-5-ui-hints[1.5: UI Hints]
 
 * 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:020-the-petclinic-domain.adoc#exercise-2-1-refactor-simpleobject-to-petowner[image:hand.png[] *2.1*: Refactor SimpleObject to PetOwner]
 
-* xref:030-fleshing-out-the-owner-entity.adoc[Fleshing out the PetOwner entity]
-** xref:030-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:030-fleshing-out-the-owner-entity.adoc#_derived_name_property[image:hand.png[] *080*: Derived name property]
-** xref:030-fleshing-out-the-owner-entity.adoc#_digression_changing_the_app_name[image:hand.png[] *090*: Digression: Changing the App Name]
-** xref:030-fleshing-out-the-owner-entity.adoc#_changing_the_object_type_class_alias[image:hand.png[] *100*: Changing the "Object Type" Class Alias]
-** xref:030-fleshing-out-the-owner-entity.adoc#_add_other_properties_for_code_owner_code[image:hand.png[] *110*: Add other properties for Owner]
-** xref:030-fleshing-out-the-owner-entity.adoc#_using_specifications_to_encapsulate_business_rules[image:hand.png[] *120*: Using specifications to encapsulate business rules]
+* xref:030-petowner-entity.adoc[Fleshing out the 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]
+** xref:030-petowner-entity.adoc#exercise-3-4-modify-the-menu-action-to-create-petowners[image:hand.png[] *3.4*: Modify the menu action to create PetOwners]
+** xref:030-petowner-entity.adoc#exercise-3-5-initial-fixture-script[image:hand.png[] *3.5*: Initial Fixture Script]
+** xref:030-petowner-entity.adoc#exercise-3-6-prompt-styles[image:hand.png[] *3.6*: Prompt styles]
+** xref:030-petowner-entity.adoc#exercise-3-7-derived-name-property[image:hand.png[] *3.7*: Derived name property]
+** xref:030-petowner-entity.adoc#exercise-3-8-add-other-properties-for-petowner[image:hand.png[] *3.8*: Add other properties for PetOwner]
+** xref:030-petowner-entity.adoc#exercise-3-9-validation[image:hand.png[] *3.9*: Validation]
+** xref:030-petowner-entity.adoc#exercise-3-10-field-layout[image:hand.png[] *3.10*: Field layout]
+** xref:030-petowner-entity.adoc#exercise-3-11-column-orders[image:hand.png[] *3.11*: Column Orders]
 
-* xref:050-prototyping.adoc[Prototyping]
-** xref:050-prototyping.adoc#_fixture_scripts_for_owner[image:hand.png[] *130*: Fixture Scripts (for Owner)]
-** xref:050-prototyping.adoc#_run_with_a_different_manifest[image:hand.png[] *140*: Run with a different manifest]
 
-* xref:060-adding-the-remaining-classes.adoc[Adding the remaining classes]
-** xref:060-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:060-adding-the-remaining-classes.adoc#_collection_of_code_pet_code_s[image:hand.png[] *160*: Collection of Pets)]
-** xref:060-adding-the-remaining-classes.adoc#_extend_our_fixture[image:hand.png[] *170*: Extend our Fixtures]
-** xref:060-adding-the-remaining-classes.adoc#_adding_code_visit_code[image:hand.png[] *180*: Adding Visit]
+* 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]
diff --git a/antora/components/tutorials/modules/petclinic/pages/020-the-petclinic-domain.adoc b/antora/components/tutorials/modules/petclinic/pages/020-the-petclinic-domain.adoc
index 5f6e7bb..388a185 100644
--- a/antora/components/tutorials/modules/petclinic/pages/020-the-petclinic-domain.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/020-the-petclinic-domain.adoc
@@ -8,71 +8,7 @@ Let's therefore evolve the app into a slightly more interesting domain and explo
 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 PetOwner <<role>> {
-        +id
-        ..
-        #lastName
-        #firstName
-        ..
-        -phoneNumber
-        -emailAddress
-    }
-}
-
-
-package visits {
-
-    class Visit <<mi>> {
-        +id
-        ..
-        #pet
-        #visitAt: LocalDateTime
-        ..
-        -reason
-        ..
-        -cost
-        -paid: boolean
-        -outcome
-    }
-}
-
-
-PetOwner *-r--> "0..*" Pet
-Visit "   \n*" -r->  Pet
-Pet  "*" -u-> PetSpecies
-----
+include::partial$domain.adoc[]
 
 [TIP]
 ====
diff --git a/antora/components/tutorials/modules/petclinic/pages/030-fleshing-out-the-owner-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
similarity index 97%
rename from antora/components/tutorials/modules/petclinic/pages/030-fleshing-out-the-owner-entity.adoc
rename to antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
index bbd9719..06e438c 100644
--- a/antora/components/tutorials/modules/petclinic/pages/030-fleshing-out-the-owner-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
@@ -1,11 +1,14 @@
-= Fleshing out the `Owner` entity
+= PetOwner 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 [...]
 
 
+In this set of exercises we'll just focus on the `PetOwner` entity.
+
+[#exercise-3-1-rename-petowners-name-property]
 == Exercise 3.1: Rename PetOwner's name property
 
-In the domain we are working on, `Owner` has a `firstName` and a `lastName` property, not a single `name` property.
+In the domain we are working on, `PetOwner` has a `firstName` and a `lastName` property, not a single `name` property.
 
 In this exercise, we'll rename ``PetOwner``'s `name` property to be `lastName`, and change the fixture script that sets up data to something more realistic.
 
@@ -53,6 +56,7 @@ Build and run the application to make sure it still runs fine.
 
 
 
+[#exercise-3-2-add-petowners-firstname-property]
 == Exercise 3.2: Add PetOwner's firstName property
 
 Now that `PetOwner` has a `lastName` property, let's also add a `firstName` property.
@@ -136,6 +140,7 @@ Learn more about fixture scripts xref:testing:fixtures:about.adoc[here].
 
 
 
+[#exercise-3-3-modify-petowners-updatename-action]
 == Exercise 3.3: Modify PetOwner's updateName action
 
 Although we've added a `firstName` property, currently it can't be edited.
@@ -191,6 +196,7 @@ The "default" supporting methods are called when the action prompt is rendered,
 
 
 
+[#exercise-3-4-modify-the-menu-action-to-create-petowners]
 == Exercise 3.4: Modify the menu action to create PetOwners
 
 If we want to create a new `PetOwner` and provide their `firstName`, at the moment it's a two stage process: create the `PetOwner` (using `PetOwners#create` action from the menu), then update their name (using the `updateName` action that we just looked at).
@@ -233,6 +239,7 @@ Or, even better would be to introduce some sort of "customerNumber" and use this
 
 
 
+[#exercise-3-5-initial-fixture-script]
 == Exercise 3.5: Initial Fixture Script
 
 As we prototype with an in-memory database, it means that we need to setup the database each time we restart the application.
@@ -277,6 +284,7 @@ When you run the application you should now find that there are 10 `PetOwner` ob
 
 
 
+[#exercise-3-6-prompt-styles]
 == Exercise 3.6: Prompt styles
 
 The framework provides many ways to customise the UI, either through the layout files or using the `@XxxLayout` annotations.
@@ -358,6 +366,7 @@ isis:
 
 
 
+[#exercise-3-7-derived-name-property]
 == Exercise 3.7: Derived name property
 
 The ``PetOwner``'s `firstName` and `lastName` properties are updated using the `updateName` action, but when the action's button is invoked, it only "replaces" the `lastName` property:
@@ -543,6 +552,7 @@ assertThat(wrap(fred).getName()).isEqualTo("Freddy"); // <.>
 
 
 
+[#exercise-3-8-add-other-properties-for-petowner]
 == Exercise 3.8: Add other properties for PetOwner
 
 Let's add the two remaining properties for `PetOwner`:
@@ -640,6 +650,7 @@ private String emailAddress;
 
 
 
+[#exercise-3-9-validation]
 == Exercise 3.9: Validation
 
 At the moment there are no constraints for the format of `phoneNumber` or `emailAddress` properties.
@@ -771,6 +782,7 @@ As the logic is shared, create a new meta-(meta-)annotation called `@Name`, move
 
 
 
+[#exercise-3-10-field-layout]
 == Exercise 3.10: Field layout
 
 At the moment all the properties of `PetOwner` are grouped into a single fieldset.
@@ -874,6 +886,7 @@ It really is a matter of personal preference which approach you use.
 
 
 
+[#exercise-3-11-column-orders]
 == Exercise 3.11: Column Orders
 
 The home page of the webapp shows a list of all `PetOwner`s (inherited from the original simple app).
diff --git a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
new file mode 100644
index 0000000..b6ff17c
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
@@ -0,0 +1,506 @@
+= Pet 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 [...]
+
+
+Right now our domain model still only consists of the single domain class, `PetOwner`.
+We still have the `Pet` and `Visit` entities to add, along with the `PetSpecies`  enum.
+
+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 domain class
+
+In this exercise we'll just create the outline of the `Pet` entity, and ensure it is mapped to the database correctly.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/04-01-pet-entity-key-properties
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+
+=== Tasks
+
+* create a meta-annotation `@PetName` for the Pet's name:
++
+[source,java]
+.PetName.java
+----
+@Property(maxLength = PetName.MAX_LEN, optionality = Optionality.MANDATORY)
+@Parameter(maxLength = PetName.MAX_LEN, optionality = Optionality.MANDATORY)
+@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PetName {
+
+    int MAX_LEN = 60;
+}
+----
+
+* create the `Pet` entity, using the `@PetName` meta-annotation for the `name` property:
++
+[source,java]
+----
+@Entity
+@Table(
+    schema="pets",
+    uniqueConstraints = {
+        @UniqueConstraint(name = "Pet__owner_name__UNQ", columnNames = {"owner_id, name"})
+    }
+)
+@EntityListeners(IsisEntityListener.class)
+@DomainObject(logicalTypeName = "pets.Pet", entityChangePublishing = Publishing.ENABLED)
+@DomainObjectLayout()
+@NoArgsConstructor(access = AccessLevel.PUBLIC)
+@XmlJavaTypeAdapter(PersistentEntityAdapter.class)
+@ToString(onlyExplicitlyIncluded = true)
+public class Pet implements Comparable<Pet> {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    @Column(name = "id", nullable = false)
+    @Getter @Setter
+    @PropertyLayout(fieldSetId = "metadata", sequence = "1")
+    private Long id;
+
+    @Version
+    @Column(name = "version", nullable = false)
+    @PropertyLayout(fieldSetId = "metadata", sequence = "999")
+    @Getter @Setter
+    private long version;
+
+
+    Pet(PetOwner petOwner, String name) {
+        this.petOwner = petOwner;
+        this.name = name;
+    }
+
+
+    @JoinColumn(name = "owner_id", nullable = false)
+    @PropertyLayout(fieldSetId = "name", sequence = "1")
+    @Getter @Setter
+    private PetOwner petOwner;
+
+    @PetName
+    @Column(name = "name", length = FirstName.MAX_LEN, nullable = false)
+    @Getter @Setter
+    @PropertyLayout(fieldSetId = "name", sequence = "2")
+    private String name;
+
+
+    private final static Comparator<Pet> comparator =
+            Comparator.comparing(Pet::getPetOwner).thenComparing(Pet::getName);
+
+    @Override
+    public int compareTo(final Pet other) {
+        return comparator.compare(this, other);
+    }
+
+}
+----
+
+Run the application, and confirm that the table is created correctly using menu:Prototyping[H2 Console].
+
+
+
+== Exercise 4.2: Add PetOwner's collection of Pets
+
+At this point in our app, although the `Pet` knows its `PetOwner`, the opposite isn't true.
+In this exercise we'll add that collection:
+
+[plantuml]
+----
+include::partial$skinparam.adoc[]
+
+package pets {
+
+    class Pet <<ppt>> {
+        +id
+        ..
+        #petOwner
+        #name
+        ..
+        version
+    }
+
+    class PetOwner <<role>> {
+        +id
+        ..
+        #lastName
+        #firstName
+        ..
+        -phoneNumber
+        -emailAddress
+    }
+}
+
+
+PetOwner *-r--> "0..*" Pet
+----
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/04-02-PetOwner-pets-collection
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+=== Tasks
+
+* in the `PetOwner` class, add the `pets` collection:
++
+[source,java]
+----
+@OneToMany(
+        mappedBy = "petOwner",      // <.>
+        cascade = CascadeType.ALL   // <.>
+)
+@Getter
+@CollectionLayout(defaultView = "table")
+private Set<Pet> pets = new TreeSet<>();
+----
+<.> specifies a bidirectional property.
+(`Pet#petOwner` "points back to" the `PetOwner`).
+<.> Deleting an `PetOwner` will also delete any associated ``Pet``s.
++
+Run the application to check the mapping is correct
+
+* update the `PetOwner.layout.xml` file to specify the position of the `pets` collection.
+For example:
++
+[source,xml]
+.PetOwner.layout.xml
+----
+<bs3:grid>
+    <bs3:row>
+        <!--...-->
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="6">
+            <!--...-->
+        </bs3:col>
+        <bs3:col span="6">
+            <bs3:tabGroup  unreferencedCollections="true" collapseIfOne="false">
+                <bs3:tab name="Pets">                   <!--.-->
+                    <bs3:row>
+                        <bs3:col span="12">
+                            <c:collection id="pets"/>
+                        </bs3:col>
+                    </bs3:row>
+                </bs3:tab>
+            </bs3:tabGroup>
+        </bs3:col>
+    </bs3:row>
+</bs3:grid>
+----
+<.> define a tab on the right hand side to hold the `pets` collection.
++
+Run the application to confirm that the `pets` collection is visible (it won't have any `Pet` instances in it just yet).
+
+
+* Create a column order file to define the order of columns in the ``PetOwner``'s `pets` collection:
++
+[source,xml]
+.PetOwner#pets.columnOrder.txt
+----
+name
+id
+----
++
+Reload the changed classes and confirm the columns of the `pets` collection are correct.
+
+
+
+
+== Exercise 4.3: Add Pet's remaining properties
+
+[plantuml]
+----
+include::partial$skinparam.adoc[]
+
+package pets {
+
+    enum PetSpecies <<desc>> {
+        Dog
+        Cat
+        Hamster
+        Budgerigar
+    }
+
+    class Pet <<ppt>> {
+        +id
+        ..
+        #petOwner
+        #name
+        ..
+        -species
+        -notes
+        ..
+        -version
+    }
+
+}
+
+Pet  "*" -u-> PetSpecies
+----
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/04-03-pet-remaining-properties
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+=== Tasks
+
+* TODO
+
+* declare the `PetSpecies` enum:
++
+[source,java]
+----
+public enum PetSpecies {
+    Dog,
+    Cat,
+    Hamster,
+    Budgerigar,
+}
+----
+
+
+* add in a reference to `PetSpecies`:
++
+[source,java]
+----
+@javax.jdo.annotations.Column(allowsNull = "false")
+@Property(editing = Editing.DISABLED)
+@Getter @Setter
+private PetSpecies petSpecies;
+----
+
+* As this is mandatory, we also need to update the constructor:
++
+[source,java]
+----
+// ...
+public Pet(final PetOwner petOwner, final String name, final PetSpecies petSpecies) {
+    this.petOwner = petOwner;
+    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;
+----
+
+
+
+== Exercise 4.4: Add PetOwner action to add Pets
+
+We'll make the addition (and removal) of ``Pet``s a responsibility of `PetOwner`.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/04-04-PetOOwner-addPet-action
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+=== Tasks
+
+* TODO
+
+* add an `addPet` action to `PetOwner`:
++
+[source,java]
+----
+@Action(semantics = SemanticsOf.NON_IDEMPOTENT)
+public Pet addPet(final String name, final PetSpecies petSpecies) {
+    return repositoryService.persist(new Pet(this, name, petSpecies));
+}
+----
+
+
+* update the `addPet` 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) { ... }
+----
+
+
+
+
+== Exercise 4.5: Add Pet's UI files
+
+
+TODO
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/04-05-Pet-ui-files
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+=== Tasks
+
+* TODO
+
+* Add a `Pet.layout.xml` file, in the same package as `Pet`.
+
+
+* Add a `Pet.png`, in the same package as `Pet`.
+
+
+
+
+== Exercise 4.6: Update fixture script to create Pets with PetOwners
+
+Before we go any further, let's take some time out to extend our fixture so that each `PetOwner` also has some ``Pet``s.
+
+TODO
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/04-06-Pet-fixture-script
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+=== Tasks
+
+* TODO
+
+
+* 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`
+
+
+
+
+
+
+== Exercise 4.7: 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).
+
+TODO
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/04-06-PetOwner-deletePet-action
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+
++
+[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.
+
+
+
diff --git a/antora/components/tutorials/modules/petclinic/pages/050-prototyping.adoc b/antora/components/tutorials/modules/petclinic/pages/050-prototyping.adoc
deleted file mode 100644
index 2652840..0000000
--- a/antora/components/tutorials/modules/petclinic/pages/050-prototyping.adoc
+++ /dev/null
@@ -1,190 +0,0 @@
-= 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::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::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::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::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/050-visit-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
new file mode 100644
index 0000000..4bdb3f4
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
@@ -0,0 +1,154 @@
+= 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 [...]
+
+
+ur domain model now consists of the domain class, `PetOwner` and `Pet` entities (along with the `PetSpecies`  enum).
+We still have the `Visit` entities to add:
+
+include::partial$domain.adoc[]
+
+In this set of exercises we'll focus on this final `Visit` entity.
+
+
+== Exercise 5.1: The visits module
+
+[source,bash]
+----
+git checkout tags/05-01-visits-module
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+=== Tasks
+
+* TODO
+
+
+== Exercise 5.2: Adding the Visit entity
+
+Our final entity is `Visit`.
+Let's extend our app to allow ``Visit``s to be booked from an ``PetOwner``'s ``Pet``:
+
+image::Pet-bookVisit-prompt.png[width="800px",link="_images/Pet-bookVisit-prompt.png"]
+
+returning
+
+image::Visit.png[width="800px",link="_images/Visit.png"]
+
+
+
+[source,bash]
+----
+git checkout tags/05-02-visit-entity
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+
+=== Tasks
+
+* TODO
+
+
+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/060-adding-the-remaining-classes.adoc b/antora/components/tutorials/modules/petclinic/pages/060-adding-the-remaining-classes.adoc
deleted file mode 100644
index e091cbd..0000000
--- a/antora/components/tutorials/modules/petclinic/pages/060-adding-the-remaining-classes.adoc
+++ /dev/null
@@ -1,442 +0,0 @@
-= 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::owner-newPet.png[width="400px",link="_images/owner-newPet.png"]
-+
-which will prompt for the name and species of the `Pet`:
-+
-image::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::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::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::Pet-bookVisit-prompt.png[width="800px",link="_images/Pet-bookVisit-prompt.png"]
-
-returning
-
-image::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/080-modularity.adoc b/antora/components/tutorials/modules/petclinic/pages/080-modularity.adoc
index ba29582..d69955f 100644
--- a/antora/components/tutorials/modules/petclinic/pages/080-modularity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/080-modularity.adoc
@@ -313,12 +313,12 @@ public Visit act(...) { ... }
 
 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?
+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 `Owner` or `Pet`, because they are not meant to "know" about the associated visits.
+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.
 
@@ -331,7 +331,7 @@ 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.
+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.
 
 
diff --git a/antora/components/tutorials/modules/petclinic/pages/100-integration-testing.adoc b/antora/components/tutorials/modules/petclinic/pages/100-integration-testing.adoc
index 495da1a..b69467b 100644
--- a/antora/components/tutorials/modules/petclinic/pages/100-integration-testing.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/100-integration-testing.adoc
@@ -28,8 +28,8 @@ mvn clean package jetty:run
 
 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.
+* 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]
@@ -100,8 +100,8 @@ 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`:
+* create the `PetOwner_enum` enum.
+The "what"s are the  enum instances, each delegating the actual creation to the `PetOwnerBuilderScript`:
 +
 [source,java]
 ----
@@ -161,9 +161,9 @@ public class RecreateOwnersAndPets extends FixtureScript {
 ----
 
 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.
+We will want to easily "look up" existing objects, so we make the `PetOwner_enum` implement a further interface.
 
-* first, extend `Owners` domain service to perform an exact lookup:
+* first, extend `PetOwners` domain service to perform an exact lookup:
 +
 [source,java]
 ----
@@ -184,7 +184,7 @@ public Owner findByLastNameAndFirstName(
 }
 ----
 
-* now let's extend `Owner_enum` to also implement `PersonaWithFinder`:
+* now let's extend `PetOwner_enum` to also implement `PersonaWithFinder`:
 +
 [source,java]
 ----
@@ -524,7 +524,7 @@ public class PetClinicModule extends ModuleAbstract {
 
 == 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.
+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
@@ -538,7 +538,7 @@ mvn clean package jetty:run
 
 === Exercise
 
-* in `OwnerBuilderScript`
+* in `PetOwnerBuilderScript`
 
 ** inject two new domain services.
 We'll need these to compute the date of the ``Visit``s.
@@ -607,7 +607,7 @@ try {
 }
 ----
 
-* extend `Owner_enum` persona to use all new infrastructure:
+* extend `PetOwner_enum` persona to use all new infrastructure:
 +
 [source,java]
 ----
diff --git a/antora/components/tutorials/modules/petclinic/pages/110-adding-further-business-logic-worked-examples.adoc b/antora/components/tutorials/modules/petclinic/pages/110-adding-further-business-logic-worked-examples.adoc
index e2e0271..2065caa 100644
--- a/antora/components/tutorials/modules/petclinic/pages/110-adding-further-business-logic-worked-examples.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/110-adding-further-business-logic-worked-examples.adoc
@@ -5,19 +5,19 @@
 
 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
+* create an `PetOwner` : yes, implemented
 
-* add and remove ``Pet``s for said `Owner` : yes, implemented.
+* add and remove ``Pet``s for said `PetOwner` : 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
+* allow an `PetOwner` 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.
+* delete an `PetOwner` 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.
@@ -25,7 +25,7 @@ In this section we'll implement the missing functionality, along with unit or in
 
 == Enter an outcome
 
-An outcome for a `Visit` consists of a diagnosis, and also the cost to be paid by the ``Pet``'s `Owner`.
+An outcome for a `Visit` consists of a diagnosis, and also the cost to be paid by the ``Pet``'s `PetOwner`.
 
 image::Visit-enterOutcome.png[width="800px",link="_images/Visit-enterOutcome.png"]
 
@@ -202,7 +202,7 @@ public java.util.List<Visit> findNotPaid() {
 }
 ----
 
-* Extend `OwnerBuilderScript` so that all but the last `Visit` for each ``Owner``'s ``Pet``s has been paid.
+* Extend `PetOwnerBuilderScript` so that all but the last `Visit` for each ``PetOwner``'s ``Pet``s has been paid.
 +
 Add some further supporting methods:
 +
@@ -217,7 +217,7 @@ private BigDecimal someCost() {
 }
 ----
 +
-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:
+In the `execute(...)`, update the `for` loop so that all ``Visit``s have an outcome and all but the last (for each ``PetOwner``) has been paid:
 +
 [source,java]
 ----
@@ -556,7 +556,7 @@ public String cssClass() {
 ----
 
 
-== Delete an `Owner` provided no unpaid ``Visit``s
+== Delete an `PetOwner` provided no unpaid ``Visit``s
 
 === Solution
 
@@ -569,14 +569,14 @@ 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.
+We don't want `PetOwner` (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.
+For this, we arrange for the `PetOwner` 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`:
+* in the `Visits` repository, add `findNotPaidBy` method to find any unpaid ``Visit``s for an `PetOwner`:
 +
 [source,java]
 ----
@@ -596,7 +596,7 @@ public java.util.List<Visit> findNotPaidBy(Owner owner) {
 }
 ----
 
-* update `Owner`'s `delete()` action so that it emits an action domain event.
+* update `PetOwner`'s `delete()` action so that it emits an action domain event.
 +
 [source,java]
 ----
diff --git a/antora/components/tutorials/modules/petclinic/partials/domain.adoc b/antora/components/tutorials/modules/petclinic/partials/domain.adoc
new file mode 100644
index 0000000..87641da
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/partials/domain.adoc
@@ -0,0 +1,60 @@
+
+[plantuml]
+----
+include::partial$skinparam.adoc[]
+
+package pets {
+
+    enum PetSpecies <<desc>> {
+        Dog
+        Cat
+        Hamster
+        Budgerigar
+    }
+
+    class Pet <<ppt>> {
+        +id
+        ..
+        #petOwner
+        #name
+        ..
+        -species
+        -notes
+        ..
+        -version
+    }
+
+
+    class PetOwner <<role>> {
+        +id
+        ..
+        #lastName
+        #firstName
+        ..
+        -phoneNumber
+        -emailAddress
+    }
+}
+
+
+package visits {
+
+    class Visit <<mi>> {
+        +id
+        ..
+        #pet
+        #visitAt: LocalDateTime
+        ..
+        -reason
+        ..
+        -cost
+        -paid: boolean
+        -outcome
+    }
+}
+
+
+PetOwner *-r--> "0..*" Pet
+Visit "   \n*" -r->  Pet
+Pet  "*" -u-> PetSpecies
+----
diff --git a/antora/components/tutorials/modules/petclinic/partials/skinparam.adoc b/antora/components/tutorials/modules/petclinic/partials/skinparam.adoc
new file mode 100644
index 0000000..cc0b9dd
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/partials/skinparam.adoc
@@ -0,0 +1,11 @@
+
+hide empty members
+hide methods
+
+skinparam class {
+	BackgroundColor<<desc>> Cyan
+	BackgroundColor<<ppt>> LightGreen
+	BackgroundColor<<mi>> LightPink
+	BackgroundColor<<role>> LightYellow
+}
+