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.

Diagram

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 Pets, 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 the name 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 all Pet 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 PetOwners, because (in a later exercise) the PetOwner entity will have child Pet 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 Prototyping  H2 Console:

    h2 console prompt
  • Connect. There is no password for sa.

  • Confirm the Pet table is created correctly:

    pet and petowner tables created

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 for Pet (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 Pets

    Pet.columnOrder.txt
    petOwner
    name
    #id
    #version

Ex 4.3: Add PetOwner’s collection of Pets

According to our model, Pets are owned by PetOwners:

Diagram

In this next exercise we’ll add the PetOwner's collection of Pets.

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 a pets 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 the pets collection on the right-hand side, above the attachment 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's pets collection. It should be in the same package as PetOwner:

    PetOwner#pets.columnOrder.txt
    name
    #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 the addPet 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 collection
    2 UI hint to render button near the pets collection
    3 To delete the object, it’s sufficient to simply remove from the PetOwner#pets collection, because orphanRemoval was set to true

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.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"});
    
    // ...
    private final String[] petNames;
  • in the PetOwner_persona.Builder class, use the petNames 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

PetOwner.java
@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:

Diagram

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.java
    public 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 the PetOwner#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.java
    Arrays.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.txt
    petOwner
    name
    species
    #notes
    #id
    #version
  • update the column order file for Pet#pets collection:

    PetOwner#pets.columnOrder.txt
    name
    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 Pets 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.