You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2024/05/26 13:49:13 UTC

(causeway) branch CAUSEWAY-2873 updated (09c8b4fe3d -> 0dadd1bca2)

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

danhaywood pushed a change to branch CAUSEWAY-2873
in repository https://gitbox.apache.org/repos/asf/causeway.git


    from 09c8b4fe3d CAUSEWAY-2873: 04-01
     new cca9ba4d0f CAUSEWAY-2873: 04-04
     new 0dadd1bca2 CAUSEWAY-2873: 04-08

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../petclinic/pages/030-petowner-entity.adoc       |  35 +
 .../modules/petclinic/pages/040-pet-entity.adoc    | 985 ++++++++-------------
 .../modules/petclinic/pages/100-todo.adoc          | 105 +++
 3 files changed, 488 insertions(+), 637 deletions(-)


(causeway) 01/02: CAUSEWAY-2873: 04-04

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit cca9ba4d0f4eb56b5b5bd2dbdb2cc6d26f465dfe
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun May 26 13:15:52 2024 +0100

    CAUSEWAY-2873: 04-04
---
 .../petclinic/pages/030-petowner-entity.adoc       |  35 ++
 .../modules/petclinic/pages/040-pet-entity.adoc    | 497 ++++++++++-----------
 .../modules/petclinic/pages/100-todo.adoc          | 103 +++++
 3 files changed, 367 insertions(+), 268 deletions(-)

diff --git a/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
index 179a312512..4b899f7b39 100644
--- a/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
@@ -1253,3 +1253,38 @@ As you might expect, these tags indicate where to render properties, collections
 IMPORTANT::
 This is an important principle of the _naked objects pattern_ ; the domain object should always be rendered in some way or another.
 The presence of UI semantics (`@XxxLayout` annotations or the `.layout.xml` files) merely influence how that rendering is performed.
+
+
+[#exercise-3-15-update-icon-for-pet-owner]
+== Ex 3.15: Update icon for Pet Owner
+
+As we've learnt in previous exercises, every domain object has a title which allows the end-user to distinguish one object from another.
+Every domain object also has an icon, which helps distinguish one domain object _type_ from another.
+
+The icon acts as the hyperlink from one domain object to another, for example from the home page to the `PetOwner` entity.
+But choosing a good icon also improves the feedback cycle with your domain experts; similar to personas, it helps to create a connection between the domain experts concept of the "thing" and its representation in the app's user interface.
+
+In this exercise, we'll replace the icon for `PetOwner`.
+
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/03-15-change-pet-owner-icon-png
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+=== Task
+
+The icon for `PetOwner` is defined in the `PetOwner.png`.
+All we need to do is replace it with some other icon.
+
+* Download a `.png` icon (it will be auto-scaled, but 32, 64 or 80 pixels is a good size).
++
+There are lots of good resources, for example https://icon8.com.
+Remember to provide appropriate accreditation if required.
+
+NOTE: the icon in the solution does indeed use a free icon from icons8.com, namely https://icons8.com/icons/set/person--icons8.
+
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 b8fa0c870c..1bb6b01974 100644
--- a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
@@ -68,12 +68,11 @@ public class Pet implements Comparable<Pet> {
     @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")
+    @PropertyLayout(fieldSetId = "metadata", sequence = "999")  // <.>
     @Getter @Setter
     private long version;
 
@@ -86,14 +85,14 @@ public class Pet implements Comparable<Pet> {
 
     @ManyToOne(optional = false)
     @JoinColumn(name = "owner_id")
-    @PropertyLayout(fieldSetId = "name", sequence = "1")
+    @PropertyLayout(fieldSetId = "identity", sequence = "1")    // <1>
     @Getter @Setter
     private PetOwner petOwner;
 
     @PetName
     @Column(name = "name", length = PetName.MAX_LEN, nullable = false)
     @Getter @Setter
-    @PropertyLayout(fieldSetId = "name", sequence = "2")
+    @PropertyLayout(fieldSetId = "identity", sequence = "2")    // <1>
     private String name;
 
 
@@ -106,61 +105,161 @@ public class Pet implements Comparable<Pet> {
     }
 }
 ----
+<.> we'll look at the layout in the next exercise.
+
+* locate the owning module, `PetOwnerModule`.
+Add in a line to delete all ``Pet`` entities on teardown:
++
+[source,java]
+.PetOwnerModule.java
+----
+@Override
+public FixtureScript getTeardownFixture() {
+    return new TeardownFixtureJpaAbstract() {
+        @Override
+        protected void execute(ExecutionContext executionContext) {
+            deleteFrom(Pet.class);          // <.>
+            deleteFrom(PetOwner.class);
+        }
+    };
+}
+----
+<.> This lne must come before the deletion of ``PetOwner``s, because (in a later exercise) the `PetOwner` entity will have child ``Pet`` entities; we always delete from the leaf level up.
++
+This is used during integration tests, to reset the database after each test.
+
 
 Run the application, and confirm that the application starts correctly.
 
-Login as `secman-admin` login (password: `pass`) to access the menu:Prototyping[H2 Console]:
+Let's also confirm that JPA created the corresponding table automatically:
 
-image::04-01/h2-console-prompt.png[]
+* Login as `secman-admin` login (password: `pass`)
 
-There is no password.
+* Access the menu:Prototyping[H2 Console]:
++
+image::04-01/h2-console-prompt.png[]
 
-Confirm the `Pet` table is created correctly:
+* Connect.
+There is no password for `sa`.
 
+* Confirm the `Pet` table is created correctly:
++
 image::04-01/pet-and-petowner-tables-created.png[]
 
 
 
-[#exercise-4-2-add-petrepository]
-== Ex 4.2: Add PetRepository
+[#exercise-4-2-add-pet-ui-and-layout-semantics]
+== Ex 4.2: Add Pet's UI and layout semantics
+
+Next, let's add in the UI and layout semantics for `Pet`.
+At the moment we're "flying blind" because we don't have any demo `Pet` instances to see, but we can refine these files later; it's good to have some scaffolding.
 
-We will need to find the ``Pet``s belonging to a `PetOwner`.
-We do this by introducing a `PetRepository`, implemented as a link:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.definition[Spring Data repository].
 
 === Solution
 
 [source,bash]
 ----
-git checkout tags/04-02-PetRepository
+git checkout tags/04-02-pet-ui-and-layout-semantics
 mvn clean install
 mvn -pl spring-boot:run
 ----
 
-
-
 === Tasks
 
-* create the `PetRepository`, extending Spring Data's `org.springframework.data.repository.Repository` interface:
+* annotate the `Pet#name` property with xref:refguide:applib:index/annotation/Title.adoc[@Title]:
 +
 [source,java]
-.PetRepository.java
+.Pet.java
 ----
-import org.springframework.data.repository.Repository;
-
-public interface PetRepository extends Repository<Pet, Long> {
-
-    List<Pet> findByPetOwner(PetOwner petOwner);
-}
+@PetName
+@Title                                                              // <.>
+@Column(name = "name", length = PetName.MAX_LEN, nullable = false)
+@Getter @Setter
+@PropertyLayout(fieldSetId = "identity", sequence = "2")
+private String name;
 ----
+<.> added
 
-Confirm the application still runs
-
+* create a `.layout.xml` file for `Pet` (easiest is to copy an existing one and adapt)
++
+[source,xml]
+.Pet.layout.xml
+----
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<bs3:grid
+        xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/grid/bootstrap3 https://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd"
+        xmlns:cpt="https://causeway.apache.org/applib/layout/component"
+        xmlns:bs3="https://causeway.apache.org/applib/layout/grid/bootstrap3"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <bs3:row>
+        <bs3:col span="12" unreferencedActions="true">
+            <cpt:domainObject bookmarking="AS_ROOT"/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="6">
+            <bs3:row>
+                <bs3:col span="12">
+                    <bs3:tabGroup>
+                        <bs3:tab name="Identity">
+                            <bs3:row>
+                                <bs3:col span="12">
+                                    <cpt:fieldSet name="Identity" id="identity"/>
+                                </bs3:col>
+                            </bs3:row>
+                        </bs3:tab>
+                        <bs3:tab name="Other">
+                            <bs3:row>
+                                <bs3:col span="12">
+                                    <cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/>
+                                </bs3:col>
+                            </bs3:row>
+                        </bs3:tab>
+                        <bs3:tab name="Metadata">
+                            <bs3:row>
+                                <bs3:col span="12">
+                                    <cpt:fieldSet name="Metadata" id="metadata"/>
+                                </bs3:col>
+                            </bs3:row>
+                        </bs3:tab>
+                    </bs3:tabGroup>
+                </bs3:col>
+                <bs3:col span="12">
+                    <cpt:fieldSet name="Details" id="details"/>
+                </bs3:col>
+            </bs3:row>
+        </bs3:col>
+        <bs3:col span="6">
+            <bs3:row>
+                <bs3:col span="12">
+                </bs3:col>
+            </bs3:row>
+            <bs3:tabGroup  unreferencedCollections="true">
+            </bs3:tabGroup>
+        </bs3:col>
+    </bs3:row>
+</bs3:grid>
+----
+
+* next, download a suitable icon to represent the pet; name it a `Pet.png`
+
+* create column order file, used to determine the order of columns of any actions that might be written that return a list of ``Pet``s
++
+[source,text]
+.Pet.columnOrder.txt
+----
+petOwner
+name
+#id
+#version
+----
 
 
 [#exercise-4-3-add-petowners-collection-of-pets]
 == Ex 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:ROOT:mixins.adoc[mixin].
+According to our model, ``Pet``s are owned by ``PetOwner``s:
+
 
 [plantuml]
 ----
@@ -192,98 +291,138 @@ package pets {
 PetOwner *-r--> "0..*" Pet
 ----
 
+In this next exercise we'll add the ``PetOwner``'s collection of ``Pet``s.
+
+
 === Solution
 
 [source,bash]
 ----
-git checkout tags/04-03-PetOwner-pets-mixin-collection
+git checkout tags/04-03-PetOwner-pets-collection
 mvn clean install
 mvn -pl spring-boot:run
 ----
 
 === Tasks
 
-* create the `PetOwner_pets` mixin class:
+* update the `PetOwner` class, add a `pets` collection:
 +
 [source,java]
 ----
-import org.apache.causeway.applib.annotation.Collection;
-import org.apache.causeway.applib.annotation.CollectionLayout;
-
-import lombok.RequiredArgsConstructor;
-
-@Collection                                             // <.>
-@CollectionLayout(defaultView = "table")
-@RequiredArgsConstructor                                // <.>
-public class PetOwner_pets {                            // <.>
-
-    private final PetOwner petOwner;                    // <.>
-
-    public List<Pet> coll() {
-        return petRepository.findByPetOwner(petOwner);  // <.>
-    }
-
-    @Inject PetRepository petRepository;                // <5>
-}
+@org.apache.causeway.applib.annotation.Collection
+@Getter
+@OneToMany(mappedBy = "petOwner", cascade = CascadeType.ALL, orphanRemoval = true)
+private Set<Pet> pets = new TreeSet<>();
 ----
-<.> indicates that this is a collection mixin
-<.> lombok annotation to avoid some boilerplate
-<.> collection name is derived from the mixin class name, being the name after the '_'.
-<.> the "mixee" that is being contributed to, in other words `PetOwner`.
-<.> inject the `PetRepository` as defined in previous exercise, in order to find the ``Pet``s owned by the `PetOwner`.
 
-* Run the application to confirm that the `pets` collection is visible (it won't have any `Pet` instances in it just yet).
 
-
-* update the `PetOwner.layout.xml` file to specify the position of the `pets` collection.
-For example:
+* update `PetOwner.layout.xml` file to position the `pets` collection on the right-hand side, above the `attachment` property:
 +
 [source,xml]
 .PetOwner.layout.xml
 ----
-<bs:grid>
-    <bs:row>
-        <!--...-->
-    </bs:row>
-    <bs:row>
-        <bs:col span="6">
-            <!--...-->
-        </bs:col>
-        <bs:col span="6">
-            <bs:tabGroup  unreferencedCollections="true" collapseIfOne="false">
-                <bs:tab name="Pets">                   <!--.-->
-                    <bs:row>
-                        <bs:col span="12">
-                            <c:collection id="pets"/>
-                        </bs:col>
-                    </bs:row>
-                </bs:tab>
-            </bs:tabGroup>
-        </bs:col>
-    </bs:row>
-</bs:grid>
-----
-<.> define a tab on the right hand side to hold the `pets` collection.
-+
-Run the application (or just reload the changed classes) and confirm the positioning the `pets` collection.
-
-
-* Create a column order file to define the order of columns in the ``PetOwner``'s `pets` collection:
+<bs3:col span="6">
+    <bs3:row>
+        <bs3:col span="12">
+            <bs3:row>
+                <bs3:col span="12">
+                    <cpt:collection id="pets"/>
+                </bs3:col>
+            </bs3:row>
+            <cpt:fieldSet name="Content" id="content">
+                <cpt:property id="attachment">
+                    <cpt:action id="updateAttachment" position="PANEL"/>
+                </cpt:property>
+            </cpt:fieldSet>
+        </bs3:col>
+    </bs3:row>
+    <bs3:tabGroup  unreferencedCollections="true">
+    </bs3:tabGroup>
+</bs3:col>
+----
+
+
+* Create a column order file to define the order of columns in the ``PetOwner``'s `pets` collection.
+It should be in the same package as `PetOwner`:
 +
 [source,xml]
 .PetOwner#pets.columnOrder.txt
 ----
 name
-id
+#id
+#version
+#petOwner
+----
+
+Run the application to confirm that the `pets` collection is visible and that the column order in the `pets` collection is correct.
+(It won't have any `Pet` instances in it just yet, of course).
+
+
+
+[#exercise-4-4-add-actions-to-add-or-remove-pets]
+== Ex 4.4: Add actions to add or remove Pets
+
+Given that a `PetOwner` knows the ``Pet``(s) that they own, it seems a reasonable responsibility to maintain this collection using behaviour on the `PetOwner`.
+
+So in this exercise we'll add two actions on `PetOwner`: one to add a `Pet` and one to remove.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/04-04-add-remove-pets
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+=== Tasks
+
+* within `PetOwner`, create the `addPet` action:
++
+[source,java]
+.Pet.java
+----
+@Action
+@ActionLayout(associateWith = "pets", sequence = "1")   // <.>
+public PetOwner addPet(@PetName final String name) {
+    final var pet = new Pet();
+    pet.setName(name);
+    pet.setPetOwner(this);
+    pets.add(pet);
+    return this;
+}
 ----
+<.> UI hint to render button near the `pets` collection
+
+* and create the `removePet` action:
 +
-Run the application (or just reload the changed classes) and confirm the columns of the `pets` collection are correct.
+[source,java]
+.Pet.java
+----
+@Action(choicesFrom = "pets")                           // <.>
+@ActionLayout(associateWith = "pets", sequence = "2")   // <.>
+public PetOwner removePet(@PetName final Pet pet) {     // <1>
+    pets.remove(pet);                                   // <.>
+    return this;
+}
+----
+<.> Indicates that a drop-down list of choices for the parameter should be taken from the `pets` collection
+<.> UI hint to render button near the `pets` collection
+<.> To delete the object, it's sufficient to simply remove from the ``PetOwner#pets`` collection, because `orphanRemoval` was set to `true`
+
+Run the application and confirm that you can now add and remove pets for a pet owner.
 
+[#exercise-4-5-extend-the-fixture-data-to-add-in-Pets]
+== Ex 4.5: Extend the fixture data to add in Pets
 
+UP TO HERE.
 
+Recall that our
 
-[#exercise-4-4-add-pets-remaining-properties]
-== Ex 4.4: Add Pet's remaining properties
+
+
+[#exercise-4-5-add-pets-remaining-properties]
+== Ex 4.5: Add Pet's remaining properties
 
 In this exercise we'll add the remaining properties for `Pet`.
 
@@ -322,7 +461,7 @@ Pet  "*" -u-> PetSpecies
 
 [source,bash]
 ----
-git checkout tags/04-04-pet-remaining-properties
+git checkout tags/04-03-pet-remaining-properties
 mvn clean install
 mvn -pl spring-boot:run
 ----
@@ -384,131 +523,15 @@ 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]
-== Ex 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.
-In this exercise we'll take a timeout to make everything consistent.
 
-=== Solution
 
-[source,bash]
-----
-git checkout tags/04-05-db-schema-consistent-casings
-mvn clean install
-mvn -pl spring-boot:run
-----
-
-=== Tasks
-
-* check out the tag and inspect the changes:
-
-** `Pet` entity table name
-** `PetOwner` entity table name and column names
-** JDBC URL
-
-* run the application to check the database schema.
-
-
-
-[#exercise-4-6-add-petowner-action-to-add-pets]
-== Ex 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`.
-We'll use an mixin action to implement this.
-
-=== Solution
-
-[source,bash]
-----
-git checkout tags/04-06-PetOwner-addPet-action
-mvn clean install
-mvn -pl spring-boot:run
-----
 
-=== Tasks
 
-* create the `PetOwner_addPet` action mixin:
-+
-[source,java]
-.PetOwner_addPet.java
-----
-@Action(                                                // <.>
-        semantics = SemanticsOf.IDEMPOTENT,
-        commandPublishing = Publishing.ENABLED,
-        executionPublishing = Publishing.ENABLED
-)
-@ActionLayout(associateWith = "pets")                   // <.>
-@RequiredArgsConstructor
-public class PetOwner_addPet {                          // <.>
-
-    private final PetOwner petOwner;                    // <.>
 
-    public PetOwner act(
-            @PetName final String name,
-            final PetSpecies petSpecies
-            ) {
-        repositoryService.persist(new Pet(petOwner, name, petSpecies));
-        return petOwner;
-    }
+[#exercise-4-7-pet-title-and-dynamic-icons]
+== Ex 4.7: Pet title and dynamic icons
 
-    @Inject RepositoryService repositoryService;
-}
-----
-<.> indicates that this class is a mixin action.
-<.> the action is associated with the "pets" collection (defined earlier).
-This means that in the UI, the button representing the action will be rendered close to the table representing the "pets" collection.
-<.> the action name "addPet" is derived from the mixin class name.
-+
-Run the application and verify that ``Pet``s can now be added to ``PetOwner``s.
-
-Let's now add some validation to ensure that two pets with the same name cannot be added.
-
-* first, we need a new method in `PetRepository`:
-+
-[source,java]
-.PetRepository.java
-----
-Optional<Pet> findByPetOwnerAndName(PetOwner petOwner, String name);
-----
-
-* Now use a supporting xref:userguide:ROOT:business-rules.adoc#validity[validate] method to prevent two pets with the same name from being added:
-+
-[source,java]
-.PetOwner_addPet.java
-----
-public String validate0Act(final String name) {
-    return petRepository.findByPetOwnerAndName(petOwner, name).isPresent()
-            ? String.format("Pet with name '%s' already defined for this owner", name)
-            : null;
-}
-
-@Inject PetRepository petRepository;
-----
-+
-NOTE: we could also just rely on the database, but adding a check here will make for better UX.
-+
-Run the application and check the validation message is fired when you attempt to add two ``Pet``s with the same name for the same `PetOwner` (but two different ``PetOwner``s should be able to have a ``Pet`` with the same name).
-
-
-* Let's suppose that owners own dogs for this particular clinic.
-Use a xref:refguide:applib-methods:prefixes.adoc#default[default] supporting method to default the petSpecies parameter:
-+
-[source,java]
-.PetOwner_addPet.java
-----
-public PetSpecies default1Act() {
-    return PetSpecies.Dog;
-}
-----
-+
-Run the application once more to test.
-
-
-
-
-[#exercise-4-7-add-pets-ui-customisation]
-== Ex 4.7: Add Pet's UI customisation
 
 If we run the application and create a `Pet`, then the framework will render a page but the layout could be improved.
 So in this exercise we'll add a layout file for `Pet` and other UI files.
@@ -526,68 +549,6 @@ mvn -pl spring-boot:run
 
 === Tasks
 
-* Create a `Pet.layout.xml` file as follows:
-+
-[source,xml]
-.Pet.layout.xml
-----
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<bs:grid xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/links https://causeway.apache.org/applib/layout/links/links.xsd https://causeway.apache.org/applib/layout/grid/bootstrap3 https://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd" xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3" xmlns:cpt="https://causeway.apache.org [...]
-    <bs:row>
-        <bs:col span="12" unreferencedActions="true">
-            <cpt:domainObject bookmarking="AS_ROOT"/>
-        </bs:col>
-    </bs:row>
-    <bs:row>
-        <bs:col span="6">
-            <bs:row>
-                <bs:col span="12">
-                    <bs:tabGroup>
-                        <bs:tab name="General">
-                            <bs:row>
-                                <bs:col span="12">
-                                    <cpt:fieldSet id="name"/>
-                                </bs:col>
-                            </bs:row>
-                        </bs:tab>
-                        <bs:tab name="Metadata">
-                            <bs:row>
-                                <bs:col span="12">
-                                    <cpt:fieldSet name="Metadata" id="metadata"/>
-                                </bs:col>
-                            </bs:row>
-                        </bs:tab>
-                        <bs:tab name="Other">
-                            <bs:row>
-                                <bs:col span="12">
-                                    <cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/>
-                                </bs:col>
-                            </bs:row>
-                        </bs:tab>
-                    </bs:tabGroup>
-                    <cpt:fieldSet id="details" name="Details"/>
-                    <cpt:fieldSet id="notes" name="Notes"/>
-                </bs:col>
-            </bs:row>
-            <bs:row>
-                <bs:col span="12">
-                </bs:col>
-            </bs:row>
-        </bs:col>
-        <bs:col span="6">
-            <bs:tabGroup unreferencedCollections="true"/>
-        </bs:col>
-    </bs:row>
-</bs:grid>
-----
-
-* reload changed classes (or run the application), and check the layout.
-+
-TIP: if the layout isn't quite as you expect, try using menu:Metadata[Rebuild metamodel] to force the domain object metamodel to be recreated.
-
-* add a `Pet.png` file to act as the icon, in the same package.
-+
-This might be a good point to find a better icon for `PetOwner`, too.
 
 * we also need a title for each `Pet`, which we can provide using a
 xref:refguide:applib-methods:ui-hints.adoc#title[title()] method:
diff --git a/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc b/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc
index 760a44e164..a3377a3e3a 100644
--- a/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc
@@ -3,9 +3,112 @@ TODO: ideas for future steps:
 * more user-friendly error message if hit duplicate (enter two pet owners with same name)
 
 
+validate pet name is unique within Pet
+
+
+
+visit
+- need some words about adding VisitRepository
+
+cf : we no longer have a PetRepository, but adapt...
+
+
+[#exercise-4-2-add-petrepository]
+== Ex 4.2: Add PetRepository
+
+We will need to find the ``Pet``s belonging to a `PetOwner`.
+We do this by introducing a `PetRepository`, implemented as a link:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.definition[Spring Data repository].
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/04-02-PetRepository
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+
+=== Tasks
+
+* create the `PetRepository`, extending Spring Data's `org.springframework.data.repository.Repository` interface:
++
+[source,java]
+.PetRepository.java
+----
+import org.springframework.data.repository.Repository;
+
+public interface PetRepository extends Repository<Pet, Long> {
+
+    List<Pet> findByPetOwner(PetOwner petOwner);
+}
+----
+
+Confirm the application still runs.
+
+
+
+
 
 PetOwner#daysSinceLastVisit could be made into a mixin - eg if these marketing analytics were the responsibility of some other module.
 
 
 hidden properties
 
+
+mixins
+
+[source,java]
+.PetOwner_pets.java
+----
+import lombok.RequiredArgsConstructor;
+
+@Collection                                             // <.>
+@CollectionLayout(defaultView = "table")
+@RequiredArgsConstructor                                // <.>
+public class PetOwner_pets {                            // <.>
+
+    private final PetOwner petOwner;                    // <.>
+
+    public List<Pet> coll() {
+        return petRepository.findByPetOwner(petOwner);  // <.>
+    }
+
+    @Inject PetRepository petRepository;                // <5>
+}
+----
+<.> indicates that this is a collection mixin
+<.> lombok annotation to avoid some boilerplate
+<.> collection name is derived from the mixin class name, being the name after the '_'.
+<.> the "mixee" that is being contributed to, in other words `PetOwner`.
+<.> inject the `PetRepository` as defined in previous exercise, in order to find the ``Pet``s owned by the `PetOwner`.
+
+
+
+
+[#exercise-4-5-digression-clean-up-casing-of-database-schema]
+== Ex 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.
+In this exercise we'll take a timeout to make everything consistent.
+
+=== Solution
+
+[source,bash]
+----
+git checkout tags/04-05-db-schema-consistent-casings
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+=== Tasks
+
+* check out the tag and inspect the changes:
+
+** `Pet` entity table name
+** `PetOwner` entity table name and column names
+** JDBC URL
+
+* run the application to check the database schema.
+


(causeway) 02/02: CAUSEWAY-2873: 04-08

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 0dadd1bca209ff320591b38d71bf60f79e66dcb2
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun May 26 14:49:06 2024 +0100

    CAUSEWAY-2873: 04-08
---
 .../modules/petclinic/pages/040-pet-entity.adoc    | 568 ++++++---------------
 .../modules/petclinic/pages/100-todo.adoc          |   2 +
 2 files changed, 161 insertions(+), 409 deletions(-)

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 1bb6b01974..efe36851e5 100644
--- a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
@@ -240,6 +240,9 @@ private String name;
     </bs3:row>
 </bs3:grid>
 ----
++
+TIP: An alternative way to create the layout file is to run the application, obtain/create an instance of the domain object in question (eg `Pet`) and then download the inferred layout XML from the metadata menu.
+
 
 * next, download a suitable icon to represent the pet; name it a `Pet.png`
 
@@ -412,381 +415,146 @@ public PetOwner removePet(@PetName final Pet pet) {     // <1>
 
 Run the application and confirm that you can now add and remove pets for a pet owner.
 
-[#exercise-4-5-extend-the-fixture-data-to-add-in-Pets]
-== Ex 4.5: Extend the fixture data to add in Pets
-
-UP TO HERE.
-
-Recall that our
-
-
-
-[#exercise-4-5-add-pets-remaining-properties]
-== Ex 4.5: Add Pet's remaining properties
-
-In this exercise we'll add the remaining properties for `Pet`.
-
-[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
-----
+[#exercise-4-5-extend-the-fixture-data-to-add-in-Pets]
+== Ex 4.5: Extend the fixture data to add in Pets
 
+In this exercise we'll extend the fixture data so that each of our pet owners have one or several pets.
 
 === Solution
 
 [source,bash]
 ----
-git checkout tags/04-03-pet-remaining-properties
+git checkout tags/04-05-extend-fixture-data-to-add-in-pets
 mvn clean install
 mvn -pl spring-boot:run
 ----
 
-
 === Tasks
 
-* declare the `PetSpecies` enum:
+* update the enum constants of `PetOwner_persona`, adding a fourth parameter of pet names:
 +
 [source,java]
-.PetSpecies.java
-----
-public enum PetSpecies {
-    Dog,
-    Cat,
-    Hamster,
-    Budgerigar,
-}
-----
+.PetOwner_persona.java
+----
+JAMAL("Jamal Washington","jamal.pdf","J",new String[] {"Max"}),
+CAMILA("Camila González","camila.pdf",null,new String[] {"Mia", "Coco", "Bella"}),
+ARJUN("Arjun Patel","arjun.pdf",null,new String[] {"Rocky", "Charlie", "Buddy"}),
+NIA("Nia Robinson","nia.pdf",null,new String[] {"Luna"}),
+OLIVIA("Olivia Hartman","olivia.pdf",null,new String[] {"Molly", "Lucy", "Daisy"}),
+LEILA("Leila Hassan","leila.pdf",null,new String[] {"Bruno"}),
+MATT("Matthew Miller","matt.pdf","Matt",new String[] {"Simba"}),
+BENJAMIN("Benjamin Thatcher","benjamin.pdf","Ben",new String[] {"Oliver"}),
+JESSICA("Jessica Raynor","jessica.pdf","Jess",new String[] {"Milo", "Lucky"}),
+DANIEL("Daniel Keating","daniel.pdf","Dan",new String[] {"Sam", "Roxy", "Smokey"});
 
-* add in a reference to `PetSpecies`:
-+
-[source,java]
-.Pet.java
-----
-@Enumerated(EnumType.STRING)                                // <.>
-@Column(nullable = false)
-@Getter @Setter
-@PropertyLayout(fieldSetId = "details", sequence = "1")     // <.>
-private PetSpecies petSpecies;
-----
-<.> mapped to a string rather than an integer value in the database
-<.> anticipates adding a 'details' fieldSet in the layout xml (see xref:#exercise-4-7-add-pets-ui-customisation[ex 4.7])
-
-* As the `petSpecies` property is mandatory, also update the constructor:
-+
-[source,java]
-.Pet.java
-----
-Pet(PetOwner petOwner, String name, PetSpecies petSpecies) {
-    this.petOwner = petOwner;
-    this.name = name;
-    this.petSpecies = petSpecies;
-}
-----
-
-* add in an optional `notes` property:
-+
-[source,java]
-----
-@Notes
-@Column(length = Notes.MAX_LEN, nullable = true)
-@Getter @Setter
-@Property(commandPublishing = Publishing.ENABLED, executionPublishing = Publishing.ENABLED)
-@PropertyLayout(fieldSetId = "notes", sequence = "1")
-private String notes;
-----
-
-Run the application and use menu:Prototyping[H2 Console] to confirm the database schema for `Pet` is as expected.
-
-
-
-
-
-
-
-
-[#exercise-4-7-pet-title-and-dynamic-icons]
-== Ex 4.7: Pet title and dynamic icons
-
-
-If we run the application and create a `Pet`, then the framework will render a page but the layout could be improved.
-So in this exercise we'll add a layout file for `Pet` and other UI files.
-
-
-=== Solution
-
-[source,bash]
-----
-git checkout tags/04-07-Pet-ui-customisation
-mvn clean install
-mvn -pl spring-boot:run
-----
-
-
-=== Tasks
-
-
-* we also need a title for each `Pet`, which we can provide using a
-xref:refguide:applib-methods:ui-hints.adoc#title[title()] method:
-+
-[source,java]
-.Pet.java
-----
-public String title() {
-    return getName() + " " + getPetOwner().getLastName();
-}
+// ...
+private final String[] petNames;
 ----
 
-In the same way that titles are specific an object instance, we can also customise the icon:
-
-* download additional icons for each of the `PetSpecies` (dog, cat, hamster, budgie)
-
-* save these icons as `Pet-dog.png`, `Pet-cat.png` and so on, ie the pet species as suffix.
-
-* implement the xref:refguide:applib-methods:ui-hints.adoc#iconName[iconName()] method as follows:
+* in the `PetOwner_persona.Builder` class, use the `petNames` to add pets to each owner:
 +
 [source,java]
-.Pet.java
-----
-public String iconName() {
-    return getPetSpecies().name().toLowerCase();
-}
+.PetOwner_persona.java
 ----
+@Override
+protected PetOwner buildResult(final ExecutionContext ec) {
 
-* Run the application.
-You should find that the appropriate icon is selected based upon the species of the `Pet`.
+    // ...
 
+    Arrays.stream(persona.petNames).forEach(petOwner::addPet);
 
-* One further tweak is to show both the title and icon for objects in tables.
-This can be done by changing some configuration properties:
-+
-[source,yaml]
-.application-custom.yml
-----
-causeway:
-  viewer:
-    wicket:
-      max-title-length-in-standalone-tables: 10
-      max-title-length-in-parented-tables: 10
-----
-+
-also update the `application.css` file, otherwise the icon and title will be centred:
-+
-[source,css]
-.application.css
-----
-td.title-column > div > div > div {
-    text-align: left;
-}
-.collectionContentsAsAjaxTablePanel table.contents thead th.title-column,
-.collectionContentsAsAjaxTablePanel table.contents tbody td.title-column {
-    width: 10%;
+    return petOwner;
 }
 ----
 
+Run the application and confirm that each pet owner has one or several pets associated with them.
 
-=== Optional exercise
-
-An alternative way to create the layout file is to run the application, obtain/create an instance of the domain object in question (eg `Pet`) and then download the inferred layout XML from the metadata menu:
-
-image::04-07/download-layout-xml.png[width=400]
 
+[#exercise-4-6-add-action-validation]
+== Ex 4.6: Unique pet names (action validation)
 
+So far our pet clinic app is mostly a CRUD (create/read/update/delete) application.
+Nothing wrong with that, and of course there are lots of tools and frameworks out there aside from Apache Causeway that can do this.
 
+Where Causeway really shines though is the ease in which more complicated business logic can be implemented.
 
-[#exercise-4-8-update-fixture-script-using-pet-personas]
-== Ex 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.
-So let's take some time out to extend our fixture so that each `PetOwner` also has some ``Pet``s.
-
+We'll see several examples of this as we flesh out the pet clinic.
+In this exercise we'll introduce a simple business rule: every owner's pet must have a different name.
 
 === Solution
 
 [source,bash]
 ----
-git checkout tags/04-08-Pet-personas
+git checkout tags/04-06-unique-pet-names-validation
 mvn clean install
 mvn -pl spring-boot:run
 ----
 
-
 === Tasks
 
-* First we need to modify the `PetOwnerBuilder` to make it idempotent:
-+
 [source,java]
-.PetOwnerBuilder.java
+.PetOwner.java
 ----
-@Accessors(chain = true)
-public class PetOwnerBuilder extends BuilderScriptWithResult<PetOwner> {
-
-    @Getter @Setter
-    private String name;
-
-    @Override
-    protected PetOwner buildResult(final ExecutionContext ec) {
-
-        checkParam("name", ec, String.class);
-
-        PetOwner petOwner = petOwners.findByLastNameExact(name);
-        if(petOwner == null) {
-            petOwner = wrap(petOwners).create(name, null);
-        }
-        return this.object = petOwner;
+@MemberSupport                                                          // <.>
+public String validate0AddPet(final String name) {                      // <.>
+    if (getPets().stream().anyMatch(x -> Objects.equals(x.getName(), name))) {
+        return "This owner already has a pet called '" + name + "'";    // <.>
     }
-
-    @Inject PetOwners petOwners;
+    return null;                                                        // <.>
 }
 ----
+<.> Indicates that this method is part of the Causeway metamodel
+<.> Naming convention indicates that this is the validation of the 0^th^ parameter of `addPet`
+<.> A non-null value indicates is used as the reason the action cannot be invoked
+<.> If null is returned then the validation has succeeded.
 
-* Now we create a similar `PetBuilder` fixture script to add ``Pet``s through a `PetOwner`:
-+
-[source,java]
-.PetBuilder.java
-----
-@Accessors(chain = true)
-public class PetBuilder extends BuilderScriptWithResult<Pet> {
-
-    @Getter @Setter String name;
-    @Getter @Setter PetSpecies petSpecies;
-    @Getter @Setter PetOwner_persona petOwner_persona;
+Run the application and confirm that the validation is working as you expect.
 
-    @Override
-    protected Pet buildResult(final ExecutionContext ec) {
-
-        checkParam("name", ec, String.class);
-        checkParam("petSpecies", ec, PetSpecies.class);
-        checkParam("petOwner_persona", ec, PetOwner_persona.class);
-
-        PetOwner petOwner = ec.executeChildT(this, petOwner_persona.builder()).getObject(); // <.>
-
-        Pet pet = petRepository.findByPetOwnerAndName(petOwner, name).orElse(null);
-        if(pet == null) {
-            wrapMixin(PetOwner_addPet.class, petOwner).act(name, petSpecies);       // <.>
-            pet = petRepository.findByPetOwnerAndName(petOwner, name).orElseThrow();
-        }
+[#exercise-4-7-add-pets-remaining-properties]
+== Ex 4.7: Add Pet's remaining properties
 
-        return this.object = pet;
-    }
-
-    @Inject PetRepository petRepository;
-}
-----
-<.> Transitively sets up its prereqs (`PetOwner`).
-This relies on thefact that `PetOwnerBuilder` is idempotent.
-<.> calls domain logic to add a `Pet` if required
+In this exercise we'll add the remaining properties for `Pet`.
+Let's remind ourselves of the domain:
 
-* Now we create a "persona" enum for ``Pet``s:
-+
-[source,java]
-.Pet_persona.java
+[plantuml]
 ----
-@AllArgsConstructor
-public enum Pet_persona
-implements PersonaWithBuilderScript<PetBuilder>, PersonaWithFinder<Pet> {
-
-    TIDDLES_JONES("Tiddles", PetSpecies.Cat, PetOwner_persona.JONES),
-    ROVER_JONES("Rover", PetSpecies.Dog, PetOwner_persona.JONES),
-    HARRY_JONES("Harry", PetSpecies.Hamster, PetOwner_persona.JONES),
-    BURT_JONES("Burt", PetSpecies.Budgerigar, PetOwner_persona.JONES),
-    TIDDLES_FARRELL("Tiddles", PetSpecies.Cat, PetOwner_persona.FARRELL),
-    SPIKE_FORD("Spike", PetSpecies.Dog, PetOwner_persona.FORD),
-    BARRY_ITOJE("Barry", PetSpecies.Budgerigar, PetOwner_persona.ITOJE);
+include::partial$skinparam.adoc[]
 
-    @Getter private final String name;
-    @Getter private final PetSpecies petSpecies;
-    @Getter private final PetOwner_persona petOwner_persona;
+package pets {
 
-    @Override
-    public PetBuilder builder() {
-        return new PetBuilder()                                     // <.>
-                        .setName(name)                              // <.>
-                        .setPetSpecies(petSpecies)
-                        .setPetOwner_persona(petOwner_persona);
+    enum PetSpecies <<desc>> {
+        Dog
+        Cat
+        Hamster
+        Budgerigar
     }
 
-    @Override
-    public Pet findUsing(final ServiceRegistry serviceRegistry) {   // <.>
-        PetOwner petOwner = petOwner_persona.findUsing(serviceRegistry);
-        PetRepository petRepository = serviceRegistry.lookupService(PetRepository.class).orElseThrow();
-        return petRepository.findByPetOwnerAndName(petOwner, name).orElse(null);
+    class Pet <<ppt>> {
+        +id
+        ..
+        #petOwner
+        #name
+        ..
+        -species
+        -notes
+        ..
+        -version
     }
 
-    public static class PersistAll
-    extends PersonaEnumPersistAll<Pet_persona, Pet> {
-        public PersistAll() {
-            super(Pet_persona.class);
-        }
-    }
 }
-----
-<.> Returns the `PetBuilder` added earlier
-<.> Copies over the state of the enum to the builder
-<.> Personas can also be used to lookup domain entities.
-The xref:refguide:applib:index/services/registry/ServiceRegistry.adoc[ServiceRegistry] can be used as a service locator of any domain service (usually a repository).
 
-* Finally, update the top-level `PetClinicDemo` to create both ``Pet``s and also ``PetOwner``s.
-+
-[source,java]
-.PetClinicDemo.java
-----
-public class PetClinicDemo extends FixtureScript {
-
-    @Override
-    protected void execute(final ExecutionContext ec) {
-        ec.executeChildren(this, moduleWithFixturesService.getTeardownFixture());
-        ec.executeChild(this, new Pet_persona.PersistAll());
-        ec.executeChild(this, new PetOwner_persona.PersistAll());
-    }
-
-    @Inject ModuleWithFixturesService moduleWithFixturesService;
-}
+Pet  "*" -u-> PetSpecies
 ----
 
-
-
-
-
-
-
-
-[#exercise-4-9-add-petowner-action-to-delete-a-pet]
-== Ex 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).
-
-
+So, we need to add a `species`, and some `notes`.
 
 === Solution
 
 [source,bash]
 ----
-git checkout tags/04-09-PetOwner-deletePet-action
+git checkout tags/04-07-pet-remaining-properties
 mvn clean install
 mvn -pl spring-boot:run
 ----
@@ -794,145 +562,123 @@ mvn -pl spring-boot:run
 
 === Tasks
 
-+ create a new action mixins, `PetOwner_removePet`:
+* declare the `PetSpecies` enum:
 +
 [source,java]
-.PetOwner_removePet.java
+.PetSpecies.java
 ----
-@Action(
-        semantics = SemanticsOf.IDEMPOTENT,
-        commandPublishing = Publishing.ENABLED,
-        executionPublishing = Publishing.ENABLED
-)
-@ActionLayout(associateWith = "pets", sequence = "2")
-@RequiredArgsConstructor
-public class PetOwner_removePet {
-
-    private final PetOwner petOwner;
-
-    public PetOwner act(@PetName final String name) {
-        petRepository.findByPetOwnerAndName(petOwner, name)
-                .ifPresent(pet -> repositoryService.remove(pet));
-        return petOwner;
-    }
-
-    @Inject PetRepository petRepository;
-    @Inject RepositoryService repositoryService;
+public enum PetSpecies {
+    Dog,
+    Cat,
+    Hamster,
+    Budgerigar,
 }
 ----
 
-* To be explicit, add in an xref:refguide:applib:index/annotation/ActionLayout.adoc#sequence[@ActionLayout#sequence] for "addPet" also:
+* add in a reference to `PetSpecies`:
 +
 [source,java]
-.PetOwner_addPet.java
+.Pet.java
 ----
-// ...
-@ActionLayout(associateWith = "pets", sequence = "1")
-// ...
-public class PetOwner_addPet {
-    // ...
-}
+@Enumerated(EnumType.STRING)                                // <.>
+@Column(nullable = false)
+@Getter @Setter
+@PropertyLayout(fieldSetId = "details", sequence = "1")
+private PetSpecies species;
 ----
+<.> mapped to a string rather than an integer value in the database
 
-* Run the application and test the action; it should work, but requires the ``Pet``'s `name` to be spelt exactly correctly.
-
-* Use a xref:refguide:applib-methods:prefixes.adoc#choices[choices] supporting method to restrict the list of `Pet` ``name``s:
+* As the `petSpecies` property is mandatory, also update the `PetOwner#addPet` action:
 +
 [source,java]
-.PetOwner_removePet.java
+.PetOwner.java
 ----
-public List<String> choices0Act() {
-    return petRepository.findByPetOwner(petOwner)
-            .stream()
-            .map(Pet::getName)
-            .collect(Collectors.toList());
+@Action
+@ActionLayout(associateWith = "pets", sequence = "1")
+public PetOwner addPet(@PetName final String name, final PetSpecies species) {
+    final var pet = new Pet();
+    pet.setName(name);
+    pet.setSpecies(species);
+    pet.setPetOwner(this);
+    pets.add(pet);
+    return this;
 }
 ----
 
-* We also should xref:refguide:applib-methods:prefixes.adoc#disable[disable] (grey out) the `removePet` action if the `PetOwner` has no ``Pet``s:
+* we also need to update the `PetOwner_persona.Builder`, because that uses this domain logic.
+We'll use the xref:refguide:testing:index/fakedata/applib/services/FakeDataService.adoc[FakeDataService] to select a species at random:
 +
 [source,java]
-.PetOwner_removePet.java
+.PetOwner_persona.java
 ----
-public String disableAct() {
-    return petRepository.findByPetOwner(petOwner).isEmpty() ? "No pets" : null;
-}
+Arrays.stream(persona.petNames).forEach(petName -> {
+    PetSpecies randomSpecies = fakeDataService.enums().anyOf(PetSpecies.class);
+    petOwner.addPet(petName, randomSpecies);
+});
 ----
 
-* As a final refinement, if there is exactly one `Pet` then that could be the xref:refguide:applib-methods:prefixes.adoc#default[default]:
+* add in an optional `notes` property:
 +
 [source,java]
-.PetOwner_removePet.java
 ----
-public String default0Act() {
-    List<String> names = choices0Act();
-    return names.size() == 1 ? names.get(0) : null;
-}
+@Notes
+@Column(length = Notes.MAX_LEN, nullable = true)
+@Getter @Setter
+@Property(commandPublishing = Publishing.ENABLED, executionPublishing = Publishing.ENABLED)
+@PropertyLayout(fieldSetId = "details", sequence = "2")
+private String notes;
 ----
 
+Let's also update the column order files:
 
-=== Optional exercise
-
-NOTE: If you decide to do this optional exercise, make the changes on a git branch so that you can resume with the main flow of exercises later.
-
-If we wanted to work with multiple instances of the `pets` collection, we can use the xref:refguide:applib-methods:prefixes.adoc#choices[choices] method using the xref:refguide:applib:index/annotation/Action.adoc#choicesFrom[@Action#choicesFrom] attribute.
-
-Add this mixin to allow multiple ``Pet``s to be removed at the same time:
-
-[source,java]
-.PetOwner_removePets.java
+* update the column order file for `PetOwner` (used to render standalone lists of objects returned by an action):
++
+[source,text]
+.Pet.columnOrder.txt
+----
+petOwner
+name
+species
+#notes
+#id
+#version
 ----
-@Action(
-        semantics = SemanticsOf.IDEMPOTENT,
-        commandPublishing = Publishing.ENABLED,
-        executionPublishing = Publishing.ENABLED,
-        choicesFrom = "pets"                            // <.>
-)
-@ActionLayout(associateWith = "pets", sequence = "2")
-@RequiredArgsConstructor
-public class PetOwner_removePets {                      // <.>
-
-    private final PetOwner petOwner;
 
-    public PetOwner act(final List<Pet> pets) {         // <.>
-        pets.forEach(repositoryService::remove);
-        return petOwner;
-    }
-    public String disableAct() {
-        return petRepository.findByPetOwner(petOwner).isEmpty() ? "No pets" : null;
-    }
-                                                        // <.>
-    @Inject PetRepository petRepository;
-    @Inject RepositoryService repositoryService;
-}
+* update the column order file for `Pet#pets` collection:
++
+[source,text]
+.PetOwner#pets.columnOrder.txt
+----
+name
+species
+#notes
+#id
+#version
+#petOwner
 ----
-<.> Results in checkboxes in the table, allowing the user to optionally check one or more instances before invoking the action.
-<.> Renamed as the action now works with a list of ``Pet``s
-<.> Signature changed.
-<.> The `choices` method is removed.
 
+Run the application, confirm it runs up ok and that the new properties of `Pet` are present and correct.
 
 
 
 
-[#exercise-4-10-cleanup]
-== Ex 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:
+[#exercise-4-8-dynamic-icons-for-pet]
+== Ex 4.8: Add dynamic icons for Pet
 
-* the classes and files for `Pet` are in the same package as for `PetOwner`; they probably should live in their own package
-* the "delete" action for `PetOwner` is not present in the UI, because its "associateWith" relates to a non-visible property
-* the "delete" action for `PetOwner` fails if there are ``Pet``s, due to a referential integrity issue.
+Now for a bit of UI candy.
 
-In this exercise we clean up these oversights.
+Currently, our icon for ``Pet``s is fixed.
+But we now have different species of `Pet`, so it would be nice if the icon could reflect this for each `Pet` instance as it is rendered.
 
+This is what we'll quickly tackle in this exercise.
 
 
 === Solution
 
 [source,bash]
 ----
-git checkout tags/04-10-pets-module-cleanup
+git checkout tags/04-08-dynamic-icons
 mvn clean install
 mvn -pl spring-boot:run
 ----
@@ -940,19 +686,23 @@ mvn -pl spring-boot:run
 
 === Tasks
 
-Just check out the tag above and inspect the fixes:
 
-* the `Pet` entity, `PetRepository` and related UI files have been moved to `petclinic.modules.pets.dom.pet` package
+* download additional icons for each of the `PetSpecies` (dog, cat, hamster, budgie)
 
-* the `PetOwner_pet`, `PetOwner_addPet` and `PetOwner_removePet` mixins have also been moved.
-+
-This means that `PetOwner` is actually unaware of the fact that there are associated ``Pet``s.
-This abliity to control the direction of dependencies is very useful for ensuring modularity.
+* save these icons as `Pet-dog.png`, `Pet-cat.png` and so on, ie the pet species as suffix.
 
-* the ``PetOwner``'s `delete` action has been refactored into a mixin, and also moved to the `pets` package so that it will delete the child ``Pet``s first.
+* implement the xref:refguide:applib-methods:ui-hints.adoc#iconName[iconName()] method as follows:
 +
-Also fixes tests.
+[source,java]
+.Pet.java
+----
+@ObjectSupport
+public String iconName() {
+    return getSpecies().name().toLowerCase();
+}
+----
+
+Run the application.
+You should find that the appropriate icon is selected based upon the species of the `Pet`.
 
-* the fixtures for `PetOwner` and `Pet` have also been moved into their own packages.
 
-* the tear down fixture for `PetsModule` has been updated to also delete from the `Pet` entity.
diff --git a/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc b/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc
index a3377a3e3a..25edb2f568 100644
--- a/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc
@@ -6,6 +6,8 @@ TODO: ideas for future steps:
 validate pet name is unique within Pet
 
 
+refactor addPet to be an inline-mixin.
+
 
 visit
 - need some words about adding VisitRepository