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 d9b319b149441234b81776540fca8a98999659d6 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 } }
