Pet entity
Right now our domain model still only consists of the single domain class, PetOwner
.
We still have the Pet
and Visit
entities to add, along with the PetSpecies
enum.
In this set of exercises we’ll focus on the Pet
entity and its relationship with PetOwner
.
Each PetOwner
will hold a collection of their Pet
s, with actions to add or remove Pet
instances for that collection.
Ex 4.1: Pet entity’s key properties
In this exercise we’ll just create the outline of the Pet
entity, and ensure it is mapped to the database correctly.
Solution
git checkout tags/v2/04-01-pet-entity-key-properties
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
-
create a meta-annotation
@PetName
for the Pet’s name:PetName.java@Property(maxLength = PetName.MAX_LEN, optionality = Optionality.MANDATORY) @Parameter(maxLength = PetName.MAX_LEN, optionality = Optionality.MANDATORY) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface PetName { int MAX_LEN = 60; }
-
create the
Pet
entity, using the@PetName
meta-annotation for thename
property:Pet.java@Entity @Table( schema= PetOwnerModule.SCHEMA, uniqueConstraints = { @UniqueConstraint(name = "Pet__owner_name__UNQ", columnNames = {"owner_id, name"}) } ) @EntityListeners(CausewayEntityListener.class) @Named(PetOwnerModule.NAMESPACE + ".Pet") @DomainObject(entityChangePublishing = Publishing.ENABLED) @DomainObjectLayout() @NoArgsConstructor(access = AccessLevel.PUBLIC) @XmlJavaTypeAdapter(PersistentEntityAdapter.class) @ToString(onlyExplicitlyIncluded = true) public class Pet implements Comparable<Pet> { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id", nullable = false) @Getter @Setter private Long id; @Version @Column(name = "version", nullable = false) @PropertyLayout(fieldSetId = "metadata", sequence = "999") (1) @Getter @Setter private long version; Pet(PetOwner petOwner, String name) { this.petOwner = petOwner; this.name = name; } @ManyToOne(optional = false) @JoinColumn(name = "owner_id") @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 = "identity", sequence = "2") (1) private String name; private final static Comparator<Pet> comparator = Comparator.comparing(Pet::getPetOwner).thenComparing(Pet::getName); @Override public int compareTo(final Pet other) { return comparator.compare(this, other); } }
1 we’ll look at the layout in the next exercise. -
locate the owning module,
PetOwnerModule
. Add in a line to delete allPet
entities on teardown:PetOwnerModule.java@Override public FixtureScript getTeardownFixture() { return new TeardownFixtureJpaAbstract() { @Override protected void execute(ExecutionContext executionContext) { deleteFrom(Pet.class); (1) deleteFrom(PetOwner.class); } }; }
1 This lne must come before the deletion of PetOwner
s, because (in a later exercise) thePetOwner
entity will have childPet
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.
Let’s also confirm that JPA created the corresponding table automatically:
-
Login as
secman-admin
login (password:pass
) -
Access the
: -
Connect. There is no password for
sa
. -
Confirm the
Pet
table is created correctly:
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.
Solution
git checkout tags/v2/04-02-pet-ui-and-layout-semantics
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
-
annotate the
Pet#name
property with @Title:Pet.java@PetName @Title (1) @Column(name = "name", length = PetName.MAX_LEN, nullable = false) @Getter @Setter @PropertyLayout(fieldSetId = "identity", sequence = "2") private String name;
1 added -
create a
.layout.xml
file forPet
(easiest is to copy an existing one and adapt)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>
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
-
create column order file, used to determine the order of columns of any actions that might be written that return a list of
Pet
sPet.columnOrder.txtpetOwner name #id #version
Ex 4.3: Add PetOwner’s collection of Pets
According to our model, Pet
s are owned by PetOwner
s:
In this next exercise we’ll add the PetOwner
's collection of Pet
s.
Solution
git checkout tags/v2/04-03-PetOwner-pets-collection
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
-
update the
PetOwner
class, add apets
collection:@org.apache.causeway.applib.annotation.Collection @Getter @OneToMany(mappedBy = "petOwner", cascade = CascadeType.ALL, orphanRemoval = true) private Set<Pet> pets = new TreeSet<>();
-
update
PetOwner.layout.xml
file to position thepets
collection on the right-hand side, above theattachment
property:PetOwner.layout.xml<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
'spets
collection. It should be in the same package asPetOwner
:PetOwner#pets.columnOrder.txtname #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).
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
git checkout tags/v2/04-04-add-remove-pets
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
-
within
PetOwner
, create theaddPet
action:Pet.java@Action @ActionLayout(associateWith = "pets", sequence = "1") (1) public PetOwner addPet(@PetName final String name) { final var pet = new Pet(); pet.setName(name); pet.setPetOwner(this); pets.add(pet); return this; }
1 UI hint to render button near the pets
collection -
and create the
removePet
action:Pet.java@Action(choicesFrom = "pets") (1) @ActionLayout(associateWith = "pets", sequence = "2") (2) public PetOwner removePet(@PetName final Pet pet) { (1) pets.remove(pet); (3) return this; }
1 Indicates that a drop-down list of choices for the parameter should be taken from the pets
collection2 UI hint to render button near the pets
collection3 To delete the object, it’s sufficient to simply remove from the PetOwner#pets
collection, becauseorphanRemoval
was set totrue
Run the application and confirm that you can now add and remove pets for a pet owner.
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
git checkout tags/v2/04-05-extend-fixture-data-to-add-in-pets
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
-
update the enum constants of
PetOwner_persona
, adding a fourth parameter of pet names:PetOwner_persona.javaJAMAL("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"}); // ... private final String[] petNames;
-
in the
PetOwner_persona.Builder
class, use thepetNames
to add pets to each owner:PetOwner_persona.java@Override protected PetOwner buildResult(final ExecutionContext ec) { // ... Arrays.stream(persona.petNames).forEach(petOwner::addPet); return petOwner; }
Run the application and confirm that each pet owner has one or several pets associated with them.
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.
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
git checkout tags/v2/04-06-unique-pet-names-validation
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
@MemberSupport (1)
public String validate0AddPet(final String name) { (2)
if (getPets().stream().anyMatch(x -> Objects.equals(x.getName(), name))) {
return "This owner already has a pet called '" + name + "'"; (3)
}
return null; (4)
}
1 | Indicates that this method is part of the Causeway metamodel |
2 | Naming convention indicates that this is the validation of the 0th parameter of addPet |
3 | If a non-null value is returned, the framework uses its as the reason the action cannot be invoked |
4 | If null is returned then the validation has succeeded. |
Run the application and confirm that the validation is working as you expect.
Ex 4.7: Add Pet’s remaining properties
In this exercise we’ll add the remaining properties for Pet
.
Let’s remind ourselves of the domain:
So, we need to add a species
, and some notes
.
Solution
git checkout tags/v2/04-07-pet-remaining-properties
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
-
declare the
PetSpecies
enum:PetSpecies.javapublic enum PetSpecies { Dog, Cat, Hamster, Budgerigar, }
-
add in a reference to
PetSpecies
:Pet.java@Enumerated(EnumType.STRING) (1) @Column(nullable = false) @Getter @Setter @PropertyLayout(fieldSetId = "details", sequence = "1") private PetSpecies species;
1 mapped to a string rather than an integer value in the database -
As the
petSpecies
property is mandatory, also update thePetOwner#addPet
action:PetOwner.java@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 need to update the
PetOwner_persona.Builder
, because that uses this domain logic. We’ll use the FakeDataService to select a species at random:PetOwner_persona.javaArrays.stream(persona.petNames).forEach(petName -> { PetSpecies randomSpecies = fakeDataService.enums().anyOf(PetSpecies.class); petOwner.addPet(petName, randomSpecies); });
-
add in an optional
notes
property:@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:
-
update the column order file for
PetOwner
(used to render standalone lists of objects returned by an action):Pet.columnOrder.txtpetOwner name species #notes #id #version
-
update the column order file for
Pet#pets
collection:PetOwner#pets.columnOrder.txtname species #notes #id #version #petOwner
Run the application, confirm it runs up ok and that the new properties of Pet
are present and correct.
Ex 4.8: Add dynamic icons for Pet
Now for a bit of UI candy.
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
git checkout tags/v2/04-08-dynamic-icons
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
-
download additional icons for each of the
PetSpecies
(dog, cat, hamster, budgerigar) -
save these icons as
Pet-dog.png
,Pet-cat.png
and so on, ie the pet species as suffix. -
implement the iconName() method as follows:
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
.