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


The following commit(s) were added to refs/heads/CAUSEWAY-2873 by this push:
     new ca0901e91e CAUSEWAY-2873: 06-01
ca0901e91e is described below

commit ca0901e91ed6e8e0802c36ea725c9e177000a99f
Author: Dan Haywood <[email protected]>
AuthorDate: Sun May 26 22:16:39 2024 +0100

    CAUSEWAY-2873: 06-01
---
 .../petclinic/pages/030-petowner-entity.adoc       |   2 +-
 .../modules/petclinic/pages/050-visit-entity.adoc  | 247 +++++++++++----------
 .../modules/petclinic/pages/060-unit-testing.adoc  |  40 ++--
 .../modules/petclinic/pages/070-modularity.adoc    |   2 +-
 .../modules/petclinic/partials/domain.adoc         |   7 +-
 5 files changed, 150 insertions(+), 148 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 4b899f7b39..73cc8c874d 100644
--- 
a/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
+++ 
b/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
@@ -807,7 +807,7 @@ public String validate0UpdateName(String newName) {         
    // <.>
     return null;
 }
 ----
-<.> validates the "0^th^" parameter of `updateName`.
+<.> The xref:refguide:applib-methods:prefixes.adoc#validate[validate...()] 
supporting method is used to validate parameters; in this case the "0^th^" 
parameter of `updateName`.
 More details on the validate supporting method can be found 
xref:refguide:applib-methods:prefixes.adoc#validate[here].
 
 In this exercise we'll move this constraint onto the `@Name` meta-annotation 
instead, using a xref:refguide:applib:index/spec/Specification.adoc[].
diff --git 
a/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc 
b/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
index ff93eec71c..380039fa4a 100644
--- a/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
@@ -74,6 +74,37 @@ causeway.persistence.schema.auto-create-schemas=\
     petowner,visit,simple,...
 ----
 
+* add permissions to the new "visit" namespace.
+We could do this by adding a new security role, but for simplicity we'll just 
add to the existing role (`PetOwnerModuleSuperuserRole), renaming it as we do:
++
+[source,java]
+.CustomRolesAndUsers.java
+----
+private static class PetClinicSuperuserRole                         // <.>
+    extends AbstractRoleAndPermissionsFixtureScript {
+
+    public static final String ROLE_NAME = "petclinic-superuser";   // <.>
+
+    public PetClinicSuperuserRole() {
+        super(ROLE_NAME, "Permission to use everything in the 'petowner' and 
'visit' modules");
+    }
+
+    @Override
+    protected void execute(ExecutionContext executionContext) {
+        newPermissions(
+                ApplicationPermissionRule.ALLOW,
+                ApplicationPermissionMode.CHANGING,
+                Can.of(ApplicationFeatureId.newNamespace("petowner"),
+                       ApplicationFeatureId.newNamespace("visit")       // <.>
+                )
+        );
+    }
+}
+----
+<.> renamed
+<.> renamed
+<.> added
+
 
 [#exercise-5-2-visit-module-dependencies]
 == Ex 5.2: Visit Module Dependencies
@@ -193,7 +224,7 @@ mvn -pl spring-boot:run
 @Named(VisitModule.NAMESPACE + ".Visit")
 @DomainObject(entityChangePublishing = Publishing.ENABLED)
 @DomainObjectLayout()
-@NoArgsConstructor(access = AccessLevel.PUBLIC)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
 @XmlJavaTypeAdapter(PersistentEntityAdapter.class)
 @ToString(onlyExplicitlyIncluded = true)
 public class Visit implements Comparable<Visit> {
@@ -210,7 +241,7 @@ public class Visit implements Comparable<Visit> {
     @Getter @Setter
     private long version;
 
-    Visit(Pet pet, LocalDateTime visitAt) {
+    public Visit(Pet pet, LocalDateTime visitAt) {
         this.pet = pet;
         this.visitAt = visitAt;
     }
@@ -330,8 +361,8 @@ image::05-02/Visit-entity.png[]
 
 
 
-[#exercise-5-3-book-visit-action]
-== Ex 5.3: "Book Visit" action
+[#exercise-5-4-book-visit-action]
+== Ex 5.4: "Book Visit" action
 
 We now want to extend our domain model so that ``Visit``s to be created.
 
@@ -347,34 +378,17 @@ include::partial$domain.adoc[]
 Causeway's solution to this is to allow the visit module to define behaviour, 
but have the behaviour seem to belong to the `Pet` entity, at least so far as 
the user interface is concerned.
 This is done using a xref:userguide::mixins.adoc[].
 
+Because `Visit` is its own root entity, we're also going to need a repository 
to be able to look them up for a given `Pet`.
 
-=== Solution
-
-[source,bash]
-----
-git checkout tags/05-03-book-visit-action
-mvn clean install
-mvn -pl spring-boot:run
-----
-
-
-
-=== Tasks
-
-
-
-[#exercise-5-4-capture-visit-reason]
-== Ex 5.3: Capture visit reason
-
-In addition to the key properties, the `Visit` has one further mandatory 
property, `reason`.
-This is required to be specified when a `Visit` is created ("what is the 
purpose of this visit?")
+In this exercise we'll define the repository, and create the "book visit" 
mixin action (also sometimes called a contributed action.
+We'll also create a mixin _collection_ to be able to view the visits from a 
``PetOwner``'s UI, too.
 
-In this exercise we'll extend the "book visit" action to also capture that 
reason.
 
+=== Solution
 
 [source,bash]
 ----
-git checkout tags/05-03-schedule-visit-action
+git checkout tags/05-04-book-visit-action
 mvn clean install
 mvn -pl spring-boot:run
 ----
@@ -382,128 +396,133 @@ mvn -pl spring-boot:run
 
 === Tasks
 
-* add the `@Reason` meta-annotation
+* create the `VisitRepository`, using Spring Data:
 +
 [source,java]
-.Reason.java
+.VisitRepository.java
 ----
-@Property(maxLength = Reason.MAX_LEN)
-@PropertyLayout(named = "Reason")
-@Parameter(maxLength = Reason.MAX_LEN)
-@ParameterLayout(named = "Reason")
-@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, 
ElementType.ANNOTATION_TYPE })
-@Retention(RetentionPolicy.RUNTIME)
-public @interface Reason {
+import org.springframework.data.repository.Repository;
 
-    int MAX_LEN = 255;
-}
-----
+// ...
 
-* add the `reason` mandatory property:
-+
-[source,java]
-.Visit.java
-----
-@Reason
-@Column(name = "reason", length = FirstName.MAX_LEN, nullable = false)
-@Getter @Setter
-@PropertyLayout(fieldSetId = "details", sequence = "1")
-private String reason;
-----
+public interface VisitRepository extends Repository<Visit, Long> {
 
+    @Query("select v from Visit v where v.pet.petOwner = :petOwner")
+    List<Visit> findByPetOwner(PetOwner petOwner);
 
-* update constructor (as this is a mandatory property)
-+
-[source,java]
-.Visit.java
-----
-Visit(Pet pet, LocalDateTime visitAt, String reason) {
-    this.pet = pet;
-    this.visitAt = visitAt;
-    this.reason = reason;
 }
 ----
 
-* create a "visits" mixin collection as a mixin of `Pet`, so we can see the 
``Visit``s that have been booked:
+* define the "book visit" mixin action on `PetOwner`, in the _visit_ module.
++
+NOTE: we simply use a datetime to capture when the visit occurs.
+This isn't particularly realistic, we know - there would probably be a domain 
concept such as `AppointmentSlot`.
 +
 [source,java]
-.Pet_visits.java
+.PetOwner_bookVisit.java
 ----
-@Collection
-@CollectionLayout(defaultView = "table")
+@Action                                                     // <.>
+@ActionLayout(associateWith = "visits")                     // <.>
 @RequiredArgsConstructor
-public class Pet_visits {
+public class PetOwner_bookVisit {                           // <.>
 
-    private final Pet pet;
+    private final PetOwner petOwner;                        // <.>
 
-    public List<Visit> coll() {
-        return visitRepository.findByPetOrderByVisitAtDesc(pet);
+    @MemberSupport
+    public PetOwner act(Pet pet, LocalDateTime visitAt) {
+        Visit visit = new Visit(pet, visitAt);
+        repositoryService.persistAndFlush(visit);           // <.>
+        return petOwner;
+    }
+    @MemberSupport
+    public Set<Pet> choices0Act() {                         // <.>
+        return petOwner.getPets();
+    }
+    @MemberSupport
+    public Pet default0Act() {                              // <.>
+        Set<Pet> pets = petOwner.getPets();
+        return pets.size() == 1 ? pets.iterator().next() : null;
+    }
+    @MemberSupport
+    public LocalDateTime default1Act() {                    // <7>
+        return officeHoursTomorrow();
+    }
+    @MemberSupport
+    public String validate1Act(LocalDateTime visitAt) {
+        if (visitAt.isBefore(officeHoursTomorrow())) {
+            return "Must book in the future";
+        }
+        return null;
     }
 
-    @Inject VisitRepository visitRepository;
+    private LocalDateTime officeHoursTomorrow() {
+        return 
clockService.getClock().nowAsLocalDate().atStartOfDay().plusDays(1).plusHours(9);
+    }
+
+
+    @Inject ClockService clockService;
+    @Inject RepositoryService repositoryService;            // <5>
 }
 ----
+<.> indicates that this class is a mixin action
+<.> anticipates there being a "visits" collection also
+<.> the name of the contributed action is inferred from the mixin's class name
+<.> the type to which this mixin is being contributed, that is, `PetOwner`
+<.> injected 
xref:refguide:applib:index/services/repository/RepositoryService.adoc[] acts as 
a facade to the database for all entities.
+For querying it's usually worth defining a custom repository.
+<.> the xref:refguide:applib-methods:prefixes.adoc#choices[choices...()] 
supporting method provides programmatic set of choices for a parameter, in this 
case for the 0^th^ parameter `Pet`, rendered as a drop-down list.
+<.> the xref:refguide:applib-methods:prefixes.adoc#default[default...()] 
supporting method returns a default value for a parameter.
 
-* create a "bookVisit" mixin action (in the visits module), as a mixin of 
`Pet`.
-+
-We can use 
xref:refguide:applib:index/services/clock/ClockService.adoc[ClockService] to 
ensure that the date/time specified is in the future, and to set a default 
date/time for "tomorrow"
+* define the "visits" mixin collection on `PetOwner`, in the _visit_ module.
 +
 [source,java]
-.Pet_bookVisit.java
+.PetOwner_visits.java
 ----
-@Action(
-        semantics = SemanticsOf.IDEMPOTENT,
-        commandPublishing = Publishing.ENABLED,
-        executionPublishing = Publishing.ENABLED
-)
-@ActionLayout(associateWith = "visits", sequence = "1")
+@Collection                                     // <.>
 @RequiredArgsConstructor
-public class Pet_bookVisit {
+public class PetOwner_visits {
 
-    private final Pet pet;
+    private final PetOwner petOwner;            // <.>
 
-    public Visit act(
-            LocalDateTime visitAt,
-            @Reason final String reason
-            ) {
-        return repositoryService.persist(new Visit(pet, visitAt, reason));
-    }
-    public String validate0Act(LocalDateTime visitAt) {
-        return clockService.getClock().nowAsLocalDateTime().isBefore(visitAt)  
 // <.>
-                ? null
-                : "Must be in the future";
-    }
-    public LocalDateTime default0Act() {
-        return clockService.getClock().nowAsLocalDateTime()                    
 // <.>
-                .toLocalDate()
-                .plusDays(1)
-                .atTime(LocalTime.of(9, 0));
+    @MemberSupport
+    public List<Visit> coll() {
+        return visitRepository.findByPetOwner(petOwner);
     }
 
-    @Inject ClockService clockService;
-    @Inject RepositoryService repositoryService;
+    @Inject VisitRepository visitRepository;
 }
 ----
-<.> ensures that the date/time specified is in the future.
-<.> defaults to 9am tomorrow morning.
-
-Also add in the UI files:
+<.> indicates that this class is a mixin collection
+<.> the type to which this mixin is being contributed, that is, `PetOwner`
 
-* add a `Pet#visits.columnOrder.txt` file
+* update ``PetOwner``'s `.layout.xml`, to indicate where the contributed 
`visits` collection should be placed:
 +
-to define which properties of Visit are visible as columns in ``Pet``'s 
`visits` collection.
-
-
-
-=== Optional exercises
-
-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.
+[source,xml]
+.PetOwner.layout.xml
+----
+<bs3:col span="12">
+    <bs3:row>
+        <bs3:col span="12">
+            <cpt:collection id="pets"/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="12">
+            <cpt:collection id="visits"/>
+        </bs3:col>
+    </bs3:row>
+    <cpt:fieldSet name="Content" id="content">
+        ...
+    </cpt:fieldSet>
+</bs3:col>
+----
++
+[NOTE]
+====
+Strictly speaking, updating the `.layout.xml` _does_ make the `petowner` 
module aware of `visit` module, albeit it in a very soft way.
 
-. Download a separate `Visit-NN.png` for each of the days of the month (1 to 
31), and then use `iconName()` to show a more useful icon based on the 
`visitAt` date.
+Alternatively, the `.layout.xml` can be left untouched in which case the 
contributed `visits` collection will be rendered in the same place as any other 
"unreferencedCollections".
+====
 
-. Use choices to provide a set of available date/times, in 15 minutes slots, 
say.
 
-. Refine the list of slots to filter out any visits that already exist
-+
-Assume that visits take 15 minutes, and that only on visit can happen at a 
time.
 
diff --git 
a/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc 
b/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc
index e66530224a..c1a6041706 100644
--- a/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc
@@ -10,7 +10,7 @@ In this part of the tutorial we'll cover unit testing, later 
on we'll look at in
 [#exercise-6-1-unit-test-the-default-time-when-booking-visits]
 == Ex 6.1: Unit test the default time when booking visits
 
-The xref:050-visit-entity.adoc#exercise-5-3-book-visit-action["Book Visit"] 
action has a default time of 9am the next morning.
+The xref:050-visit-entity.adoc#exercise-5-4-book-visit-action["Book Visit"] 
action has a default time of 9am the next morning.
 In this section we'll write a unit test to verify this logic, using Mockito to 
"mock the clock".
 
 
@@ -31,23 +31,11 @@ mvn -pl spring-boot:run
 [source,xml]
 .module-visits/pom.xml
 ----
-<dependencies>
-    <!-- ... -->
-
-    <dependency>
-        <groupId>org.apache.causeway.mavendeps</groupId>
-        <artifactId>causeway-mavendeps-unittests</artifactId>
-        <type>pom</type>
-        <scope>test</scope>
-        <exclusions>
-            <exclusion>
-                <groupId>org.jmock</groupId>
-                <artifactId>jmock-junit4</artifactId>
-            </exclusion>
-        </exclusions>
-    </dependency>
-
-</dependencies>
+<dependency>
+    <groupId>org.apache.causeway.testing</groupId>
+    <artifactId>causeway-testing-unittestsupport-applib</artifactId>
+    <scope>test</scope>
+</dependency>
 ----
 
 * add the test:
@@ -55,7 +43,7 @@ mvn -pl spring-boot:run
 [source,java]
 ----
 @ExtendWith(MockitoExtension.class)                                            
 // <.>
-class Pet_bookVisit_Test {
+public class PetOwner_bookVisit_Test {
 
     @Mock ClockService mockClockService;                                       
 // <.>
     @Mock VirtualClock mockVirtualClock;                                       
 // <2>
@@ -66,31 +54,31 @@ class Pet_bookVisit_Test {
     }
 
     @Nested
-    class default0 {
+    class default1 {
 
         @Test
         void defaults_to_9am_tomorrow_morning() {
 
             // given
-            Pet_bookVisit mixin = new Pet_bookVisit(null);
+            PetOwner_bookVisit mixin = new PetOwner_bookVisit(null);
             mixin.clockService = mockClockService;                             
 // <.>
 
-            LocalDateTime now = LocalDateTime.of(2021, 10, 21, 16, 37, 45);
+            LocalDateTime now = LocalDateTime.of(2024, 5, 26, 16, 37, 45);
 
             // expecting
-            
Mockito.when(mockVirtualClock.nowAsLocalDateTime()).thenReturn(now);// <.>
+            Mockito.when(mockVirtualClock.nowAsLocalDate())                    
 // <.>
+                   .thenReturn(now.toLocalDate());
 
             // when
-            LocalDateTime localDateTime = mixin.default0Act();
+            LocalDateTime localDateTime = mixin.default1Act();
 
             // then
             Assertions.assertThat(localDateTime)                               
 // <.>
-                    .isEqualTo(LocalDateTime.of(2021,10,22,9,0,0));
+                    .isEqualTo(LocalDateTime.of(2024,5,27,9,0,0));
         }
     }
 }
 ----
-
 <.> Instructs JUnit to use Mockito for mocking.
 <.> mocks the `ClockService`, and mocks the `VirtualClock` returned by the 
`ClockService`.
 Automatically provisioned by Mockito.
diff --git 
a/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc 
b/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc
index a62595fa5b..793a6aad36 100644
--- a/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc
@@ -10,7 +10,7 @@ The framework provides two main tools:
 
 * the first we've already seen is mixins.
 +
-These allow us to locate busines logic in one module that "appears" to reside 
in another module.
+These allow us to locate business logic in one module that "appears" to reside 
in another module.
 Examples are the `visits` mixin collection and `bookVisit` mixin action that 
are both contributed by the `visits` module to the `Pet` entity in the `pets` 
module.
 
 * the second is domain events.
diff --git a/antora/components/tutorials/modules/petclinic/partials/domain.adoc 
b/antora/components/tutorials/modules/petclinic/partials/domain.adoc
index e699f8cd23..44bb4da217 100644
--- a/antora/components/tutorials/modules/petclinic/partials/domain.adoc
+++ b/antora/components/tutorials/modules/petclinic/partials/domain.adoc
@@ -35,6 +35,7 @@ package pets {
         -emailAddress
         ..
         -lastVisit
+        -/daysSinceLastVisit
         ..
         -notes
     }
@@ -48,12 +49,6 @@ package visits {
         ..
         #pet
         #visitAt: LocalDateTime
-        ..
-        -reason
-        ..
-        -cost
-        -paid: boolean
-        -outcome
     }
 }
 

Reply via email to