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 8e56f90651801ca1eefbe520a6ab2bc3c06845c8 Author: Dan Haywood <[email protected]> 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
