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 c5b1773e2841168aec684c376e3385da99048a95 Author: Dan Haywood <[email protected]> AuthorDate: Sun May 26 22:49:06 2024 +0100 CAUSEWAY-2873: 07-01 --- .../modules/petclinic/pages/070-modularity.adoc | 153 ++++++++------------- .../modules/petclinic/pages/100-todo.adoc | 2 + 2 files changed, 62 insertions(+), 93 deletions(-) diff --git a/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc b/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc index 793a6aad36..fa827459db 100644 --- a/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc +++ b/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc @@ -8,29 +8,28 @@ If every class potentially can depend on any other class, we'll end up with a "b Instead, we need to ensure that the dependency graph between packages remains acyclic. The framework provides two main tools: -* the first we've already seen is mixins. +* the first we've already seen: mixins. + 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. + -These we haven't yet seen, but provide a way for one module to react to (or to veto) actions performed in logic in another module. +These provide a way for one module to react to (or to veto) actions performed in logic in another module. -In this part of the tutorial we'll look at domain events. +In this section we'll look at domain events. [#exercise-7-1-refactor-petowners-delete-action] == Ex 7.1: refactor PetOwner's delete action -Currently the `delete` action for `PetOwner` is implemented as a mixin within the `Pet` package. -That's a nice place for that functionality, because it can delete any `Pet`s for the `PetOwner` if any exist. +Currently the `delete` action for `PetOwner` is broken: although the owner's ``Pet``s are automatically deleted when the `PetOwner` is itself deleted, if there are any ``Visit``s then the foreign key in the database will prevent deletion. -However, we also have added `Visit`, which has the same issue: we cannot delete a `Pet` if there are associated ``Visit``s. -And, in fact, we don't want to allow a `PetOwner` and their ``Pet``s from being deleted if there are ``Visit``s in the database; they might not have paid! +In one sense this is good: we probably don't want to allow a `PetOwner` and their ``Pet``s from being deleted if there are ``Visit``s in the database; they might not have paid! +However, we ought to have the business logic in the domain layer rather than rely on the database's foreign key. -In this exercise we will move the responsibility to delete an action back to `PetOwner`, and then use subscribers for both `Pet` and `Visit` to cascade delete or to veto the action respectively if there are related objects. +In this exercise we'll use domain events to cascade delete or to veto the action respectively if there are related objects. @@ -43,116 +42,84 @@ mvn clean install mvn -pl spring-boot:run ---- -To test this out: - -* try deleting a `PetOwner` where none of their ``Pet``s have any ``Visit``s; the action should succeed, and the `PetOwner` and the ``Pet``s should all be deleted. - -* now book a `Visit` for a `Pet`, then navigate back to the parent `PetOwner` and attempt to delete it. -This time the action should be vetoed, because of that `Visit`. - - === Tasks -* in `PetOwner_delete` remove the code that deletes the ``Pet``s. -In its place, define a subclass of xref:refguide:applib-classes:events.adoc#domain-event-classes[ActionDomainEvent] as a nested class of the mixin, and reference in the xref:refguide:applib:index/annotation/Action.adoc#domainEvent[@Action#domainEvent] attribute. +* (optional) confirm that although it's not possible to delete a `PetOwner` if there are corresponding ``Visit``s, the error we get back is a database exception + +* in `PetOwner`, modify the `delete` action so that it emits a specific domain event type. + [source,java] -.PetOwner_delete.java +.PetOwner.java ---- -@Action( - domainEvent = PetOwner_delete.ActionEvent.class, // <.> - semantics = SemanticsOf.NON_IDEMPOTENT_ARE_YOU_SURE, - commandPublishing = Publishing.ENABLED, - executionPublishing = Publishing.ENABLED -) -@ActionLayout( - associateWith = "name", position = ActionLayout.Position.PANEL, - describedAs = "Deletes this object from the persistent datastore") -@RequiredArgsConstructor -public class PetOwner_delete { - - public static class ActionEvent // <.> - extends ActionDomainEvent<PetOwner_delete>{} - - private final PetOwner petOwner; - - public void act() { - repositoryService.remove(petOwner); - return; - } - - @Inject RepositoryService repositoryService; -} + public static class DeleteActionDomainEvent + extends org.apache.causeway.applib.events.domain.ActionDomainEvent<PetOwner> {} // <.> + + @Action( + semantics = NON_IDEMPOTENT_ARE_YOU_SURE, + domainEvent = DeleteActionDomainEvent.class // <.> + ) + @ActionLayout( + describedAs = "Deletes this object from the persistent datastore") + public void delete() { ... } ---- <.> specifies the domain event to emit when the action is called <.> declares the action event (as a subclass of the framework's xref:refguide:applib-classes:events.adoc#domain-event-classes[ActionDomainEvent]). ++ +NOTE: in fact, domain events are always emitted; but by default a generic `ActionDomainEvent` is used rather than a specific subclass. * create a subscriber in the `pets` package to delete all ``Pet``s when the `PetOwner_delete` action is invoked: + [source,java] -.PetOwnerForPetsSubscriber.java +.PetOwner_delete_subscriber.java ---- -@Service -public class PetOwnerForPetsSubscriber { - - @EventListener(PetOwner_delete.ActionEvent.class) - public void on(PetOwner_delete.ActionEvent ev) { - switch(ev.getEventPhase()) { - case EXECUTING: // <.> - PetOwner petOwner = ev.getSubject(); // <.> - List<Pet> pets = petRepository.findByPetOwner(petOwner); - pets.forEach(repositoryService::remove); +@Component +public class PetOwner_delete_subscriber { + + @EventListener(PetOwner.DeleteActionDomainEvent.class) // <.> + void on(PetOwner.DeleteActionDomainEvent event) { // <1> + PetOwner subject = event.getSubject(); // <.> + switch (event.getEventPhase()) { // <.> + case HIDE: break; - } - } - - @Inject PetRepository petRepository; - @Inject RepositoryService repositoryService; -} ----- -<.> events are emitted at different phases. -The `EXECUTING` phase is fired before the delete action itself is fired, so is the ideal place for us to perform the cascade delete. -<.> is the mixee of the mixin that is emitting the event. - -* create a subscriber in the `visits` module to veto the `PetOwner_delete` if there are any `Pet`s of the `PetOwner` with at least one `Visit`: -+ -[source,java] -.PetOwnerForVisitsSubscriber.java ----- -@Service -public class PetOwnerForVisitsSubscriber { - - @EventListener(PetOwner_delete.ActionEvent.class) - public void on(PetOwner_delete.ActionEvent ev) { - switch(ev.getEventPhase()) { - case DISABLE: - PetOwner petOwner = ev.getSubject(); - List<Pet> pets = petRepository.findByPetOwner(petOwner); - for (Pet pet : pets) { - List<Visit> visits = visitRepository.findByPetOrderByVisitAtDesc(pet); - int numVisits = visits.size(); - if(numVisits > 0) { - ev.disable(String.format("%s has %d visit%s", - titleService.titleOf(pet), - numVisits, - numVisits != 1 ? "s" : "")); - } + case DISABLE: // <.> + List<Visit> visits = visitRepository.findByPetOwner(subject); + if (!visits.isEmpty()) { + event.veto("This owner has %d visit%s", visits.size(), (visits.size() == 1 ? "" : "s")); } break; + case VALIDATE: + break; + case EXECUTING: + break; + case EXECUTED: + break; } } - @Inject TitleService titleService; @Inject VisitRepository visitRepository; - @Inject PetRepository petRepository; } ---- +<.> subscribes to the event using Spring `@EventListener` +<.> returns the effective originator of the event. +This works for both regular actions and mixin actions +<.> the subscriber is called multiple times, for the various phases of the execution lifecycle; more on this below +<.> if there are any ``Visit``s for this pet owner, then veto the interaction. +In the user interface, the "delete" button will be disabled, that is greyed out. +The returned string is used as the tooltip to explain _why_ the button is disabled. +The event lifecycle allows subscribers to veto (in other words, specify preconditions) in three different ways: -=== Optional Exercise - +* hide - will hide the action's button in the UI completely +* disable - will disable (grey) out the action's button +* validate - will prevent the button from being pressed. +this case applies when validating the action arguments. -Improve the implementation of `PetOwnerForVisitsSubscriber` so that it performs only a single database query to find if there are any ``Visit`` for the `PetOwner`. +If the action is not vetored, then the subscriber is also possible to perform additional steps: +* executing - the action is about to execute. +* executed - the action is just execute +For example, if the business rule had instead been to simply delete all ``Visit``s, then this could have been implemented in the "executing" phase. +TIP: it's also worth knowing that these events are fired for properties and collections as well as actions. +Therefore subscribers can substantially dictate what is accessible for any given domain object. diff --git a/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc b/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc index 5ce9d43c8d..13156e85d2 100644 --- a/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc +++ b/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc @@ -8,6 +8,8 @@ validate pet name is unique within Pet refactor addPet to be an inline-mixin. +need a disable and a hide action + update home page, show upcoming appointments
