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/04-01-pet-entity-key-properties
mvn clean install
mvn -pl spring-boot:run

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="pets",
        uniqueConstraints = {
            @UniqueConstraint(name = "Pet__owner_name__UNQ", columnNames = {"owner_id, name"})
        }
    )
    @EntityListeners(IsisEntityListener.class)
    @DomainObject(logicalTypeName = "pets.Pet", 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
        @PropertyLayout(fieldSetId = "metadata", sequence = "1")
        private Long id;
    
        @Version
        @Column(name = "version", nullable = false)
        @PropertyLayout(fieldSetId = "metadata", sequence = "999")
        @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 = "name", sequence = "1")
        @Getter @Setter
        private PetOwner petOwner;
    
        @PetName
        @Column(name = "name", length = FirstName.MAX_LEN, nullable = false)
        @Getter @Setter
        @PropertyLayout(fieldSetId = "name", sequence = "2")
        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);
        }
    
    }

Run the application, and confirm that the table is created correctly using Prototyping  H2 Console.

Ex 4.2: Add PetRepository

We will need to find the Pets belonging to a PetOwner. We do this by introducing a PetRepository, implemented as a Spring Data repository.

Solution

git checkout tags/04-02-PetRepository
mvn clean install
mvn -pl spring-boot:run

Tasks

  • create the PetRepository, extending Spring Data’s org.springframework.data.repository.Repository interface:

    PetRepository.java
    import org.springframework.data.repository.Repository;
    
    public interface PetRepository extends Repository<Pet, Long> {
    
        List<Pet> findByPetOwner(PetOwner petOwner);
    }

Confirm the application still runs

Ex 4.3: Add PetOwner’s collection of Pets

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

Diagram

Solution

git checkout tags/04-03-PetOwner-pets-mixin-collection
mvn clean install
mvn -pl spring-boot:run

Tasks

  • create the PetOwner_pets mixin class:

    import org.apache.isis.applib.annotation.Collection;
    import org.apache.isis.applib.annotation.CollectionLayout;
    
    import lombok.RequiredArgsConstructor;
    
    @Collection                                             (1)
    @CollectionLayout(defaultView = "table")
    @RequiredArgsConstructor                                (2)
    public class PetOwner_pets {                            (3)
    
        private final PetOwner petOwner;                    (4)
    
        public List<Pet> coll() {
            return petRepository.findByPetOwner(petOwner);  (5)
        }
    
        @Inject PetRepository petRepository;                (5)
    }
    1 indicates that this is a collection mixin
    2 lombok annotation to avoid some boilerplate
    3 collection name is derived from the mixin class name, being the name after the '_'.
    4 the "mixee" that is being contributed to, in other words PetOwner.
    5 inject the PetRepository as defined in previous exercise, in order to find the Pets owned by the PetOwner.
  • Run the application to confirm that the pets collection is visible (it won’t have any Pet instances in it just yet).

  • update the PetOwner.layout.xml file to specify the position of the pets collection. For example:

    PetOwner.layout.xml
    <bs3:grid>
        <bs3:row>
            <!--...-->
        </bs3:row>
        <bs3:row>
            <bs3:col span="6">
                <!--...-->
            </bs3:col>
            <bs3:col span="6">
                <bs3:tabGroup  unreferencedCollections="true" collapseIfOne="false">
                    <bs3:tab name="Pets">                   (1)
                        <bs3:row>
                            <bs3:col span="12">
                                <c:collection id="pets"/>
                            </bs3:col>
                        </bs3:row>
                    </bs3:tab>
                </bs3:tabGroup>
            </bs3:col>
        </bs3:row>
    </bs3:grid>
    1 define a tab on the right hand side to hold the pets collection.

    Run the application (or just reload the changed classes) and confirm the positioning the pets collection.

  • Create a column order file to define the order of columns in the PetOwner's pets collection:

    PetOwner#pets.columnOrder.txt
    name
    id

    Run the application (or just reload the changed classes) and confirm the columns of the pets collection are correct.

Ex 4.4: Add Pet’s remaining properties

In this exercise we’ll add the remaining properties for Pet.

Diagram

Solution

git checkout tags/04-04-pet-remaining-properties
mvn clean install
mvn -pl spring-boot:run

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")     (2)
    private PetSpecies petSpecies;
    1 mapped to a string rather than an integer value in the database
    2 anticipates adding a 'details' fieldSet in the layout xml (see ex 4.7)
  • As the petSpecies property is mandatory, also update the constructor:

    Pet.java
    Pet(PetOwner petOwner, String name, PetSpecies petSpecies) {
        this.petOwner = petOwner;
        this.name = name;
        this.petSpecies = petSpecies;
    }
  • 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 = "notes", sequence = "1")
    private String notes;

Run the application and use Prototyping  H2 Console to confirm the database schema for Pet is as expected.

Ex 4.5: Digression: clean-up casing of database schema

Reviewing the tables in the database we can see that we have a mix between lower- and upper-case table and column names. In this exercise we’ll take a timeout to make everything consistent.

Solution

git checkout tags/04-05-db-schema-consistent-casings
mvn clean install
mvn -pl spring-boot:run

Tasks

  • check out the tag and inspect the changes:

    • Pet entity table name

    • PetOwner entity table name and column names

    • JDBC URL

  • run the application to check the database schema.

Ex 4.6: Add PetOwner action to add Pets

In this exercise we’ll bring in the capability to add Pets, as a responsibility of PetOwner. We’ll use an mixin action to implement this.

Solution

git checkout tags/04-06-PetOwner-addPet-action
mvn clean install
mvn -pl spring-boot:run

Tasks

  • create the PetOwner_addPet action mixin:

    PetOwner_addPet.java
    @Action(                                                (1)
            semantics = SemanticsOf.IDEMPOTENT,
            commandPublishing = Publishing.ENABLED,
            executionPublishing = Publishing.ENABLED
    )
    @ActionLayout(associateWith = "pets")                   (2)
    @RequiredArgsConstructor
    public class PetOwner_addPet {                          (3)
    
        private final PetOwner petOwner;                    (4)
    
        public PetOwner act(
                @PetName final String name,
                final PetSpecies petSpecies
                ) {
            repositoryService.persist(new Pet(petOwner, name, petSpecies));
            return petOwner;
        }
    
        @Inject RepositoryService repositoryService;
    }
    1 indicates that this class is a mixin action.
    2 the action is associated with the "pets" collection (defined earlier). This means that in the UI, the button representing the action will be rendered close to the table representing the "pets" collection.
    3 the action name "addPet" is derived from the mixin class name.

    Run the application and verify that Pets can now be added to PetOwners.

Let’s now add some validation to ensure that two pets with the same name cannot be added.

  • first, we need a new method in PetRepository:

    PetRepository.java
    Optional<Pet> findByPetOwnerAndName(PetOwner petOwner, String name);
  • Now use a supporting validate method to prevent two pets with the same name from being added:

    PetOwner_addPet.java
    public String validate0Act(final String name) {
        return petRepository.findByPetOwnerAndName(petOwner, name).isPresent()
                ? String.format("Pet with name '%s' already defined for this owner", name)
                : null;
    }
    
    @Inject PetRepository petRepository;
    we could also just rely on the database, but adding a check here will make for better UX.

    Run the application and check the validation message is fired when you attempt to add two Pets with the same name for the same PetOwner (but two different PetOwners should be able to have a Pet with the same name).

  • Let’s suppose that owners own dogs for this particular clinic. Use a default supporting method to default the petSpecies parameter:

    PetOwner_addPet.java
    public PetSpecies default1Act() {
        return PetSpecies.Dog;
    }

    Run the application once more to test.

Ex 4.7: Add Pet’s UI customisation

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

git checkout tags/04-07-Pet-ui-customisation
mvn clean install
mvn -pl spring-boot:run

Tasks

  • Create a Pet.layout.xml file as follows:

    Pet.layout.xml
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <bs3:grid xsi:schemaLocation="http://isis.apache.org/applib/layout/component http://isis.apache.org/applib/layout/component/component.xsd http://isis.apache.org/applib/layout/links http://isis.apache.org/applib/layout/links/links.xsd http://isis.apache.org/applib/layout/grid/bootstrap3 http://isis.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd" xmlns:bs3="http://isis.apache.org/applib/layout/grid/bootstrap3" xmlns:cpt="http://isis.apache.org/applib/layout/component" xmlns:lnk="http://isis.apache.org/applib/layout/links" 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="General">
                                <bs3:row>
                                    <bs3:col span="12">
                                        <cpt:fieldSet id="name"/>
                                    </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:tab name="Other">
                                <bs3:row>
                                    <bs3:col span="12">
                                        <cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/>
                                    </bs3:col>
                                </bs3:row>
                            </bs3:tab>
                        </bs3:tabGroup>
                        <cpt:fieldSet id="details" name="Details"/>
                        <cpt:fieldSet id="notes" name="Notes"/>
                    </bs3:col>
                </bs3:row>
                <bs3:row>
                    <bs3:col span="12">
                    </bs3:col>
                </bs3:row>
            </bs3:col>
            <bs3:col span="6">
                <bs3:tabGroup unreferencedCollections="true"/>
            </bs3:col>
        </bs3:row>
    </bs3:grid>
  • reload changed classes (or run the application), and check the layout.

    if the layout isn’t quite as you expect, try using Metadata  Rebuild metamodel to force the domain object metamodel to be recreated.
  • add a Pet.png file to act as the icon, in the same package.

    This might be a good point to find a better icon for PetOwner, too.

  • we also need a title for each Pet, which we can provide using a title() method:

    Pet.java
    public String title() {
        return getName() + " " + getPetOwner().getLastName();
    }

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 iconName() method as follows:

    Pet.java
    public String iconName() {
        return getPetSpecies().name().toLowerCase();
    }
  • Run the application. You should find that the appropriate icon is selected based upon the species of the Pet.

  • One further tweak is to show both the title and icon for objects in tables. This can be done by changing some configuration properties:

    application-custom.yml
    isis:
      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:

    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%;
    }

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:

download layout xml

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 Pets.

Solution

git checkout tags/04-08-Pet-personas
mvn clean install
mvn -pl spring-boot:run

Tasks

  • First we need to modify the PetOwnerBuilder to make it idempotent:

    PetOwnerBuilder.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;
        }
    
        @Inject PetOwners petOwners;
    }
  • Now we create a similar PetBuilder fixture script to add Pets through a PetOwner:

    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;
    
        @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(); (1)
    
            Pet pet = petRepository.findByPetOwnerAndName(petOwner, name).orElse(null);
            if(pet == null) {
                wrapMixin(PetOwner_addPet.class, petOwner).act(name, petSpecies);       (2)
                pet = petRepository.findByPetOwnerAndName(petOwner, name).orElseThrow();
            }
    
            return this.object = pet;
        }
    
        @Inject PetRepository petRepository;
    }
    1 Transitively sets up its prereqs (PetOwner). This relies on thefact that PetOwnerBuilder is idempotent.
    2 calls domain logic to add a Pet if required
  • Now we create a "persona" enum for Pets:

    Pet_persona.java
    @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);
    
        @Getter private final String name;
        @Getter private final PetSpecies petSpecies;
        @Getter private final PetOwner_persona petOwner_persona;
    
        @Override
        public PetBuilder builder() {
            return new PetBuilder()                                     (1)
                            .setName(name)                              (2)
                            .setPetSpecies(petSpecies)
                            .setPetOwner_persona(petOwner_persona);
        }
    
        @Override
        public Pet findUsing(final ServiceRegistry serviceRegistry) {   (3)
            PetOwner petOwner = petOwner_persona.findUsing(serviceRegistry);
            PetRepository petRepository = serviceRegistry.lookupService(PetRepository.class).orElseThrow();
            return petRepository.findByPetOwnerAndName(petOwner, name).orElse(null);
        }
    
        public static class PersistAll
        extends PersonaEnumPersistAll<Pet_persona, Pet> {
            public PersistAll() {
                super(Pet_persona.class);
            }
        }
    }
    1 Returns the PetBuilder added earlier
    2 Copies over the state of the enum to the builder
    3 Personas can also be used to lookup domain entities. The ServiceRegistry can be used as a service locator of any domain service (usually a repository).
  • Finally, update the top-level PetClinicDemo to create both Pets and also PetOwners.

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

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 Visits for a Pet, we’ll need to disable this action).

Solution

git checkout tags/04-09-PetOwner-deletePet-action
mvn clean install
mvn -pl spring-boot:run

Tasks

+ create a new action mixins, PetOwner_removePet:

+

PetOwner_removePet.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;
}
  • To be explicit, add in an @ActionLayout#sequence for "addPet" also:

    PetOwner_addPet.java
    // ...
    @ActionLayout(associateWith = "pets", sequence = "1")
    // ...
    public class PetOwner_addPet {
        // ...
    }
  • Run the application and test the action; it should work, but requires the Pet's name to be spelt exactly correctly.

  • Use a choices supporting method to restrict the list of Pet names:

    PetOwner_removePet.java
    public List<String> choices0Act() {
        return petRepository.findByPetOwner(petOwner)
                .stream()
                .map(Pet::getName)
                .collect(Collectors.toList());
    }
  • We also should disable (grey out) the removePet action if the PetOwner has no Pets:

    PetOwner_removePet.java
    public String disableAct() {
        return petRepository.findByPetOwner(petOwner).isEmpty() ? "No pets" : null;
    }
  • As a final refinement, if there is exactly one Pet then that could be the default:

    PetOwner_removePet.java
    public String default0Act() {
        List<String> names = choices0Act();
        return names.size() == 1 ? names.get(0) : null;
    }

Optional exercise

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 choices method using the @Action#choicesFrom attribute.

Add this mixin to allow multiple Pets to be removed at the same time:

PetOwner_removePets.java
@Action(
        semantics = SemanticsOf.IDEMPOTENT,
        commandPublishing = Publishing.ENABLED,
        executionPublishing = Publishing.ENABLED,
        choicesFrom = "pets"                            (1)
)
@ActionLayout(associateWith = "pets", sequence = "2")
@RequiredArgsConstructor
public class PetOwner_removePets {                      (2)

    private final PetOwner petOwner;

    public PetOwner act(final List<Pet> pets) {         (3)
        pets.forEach(repositoryService::remove);
        return petOwner;
    }
    public String disableAct() {
        return petRepository.findByPetOwner(petOwner).isEmpty() ? "No pets" : null;
    }
                                                        (4)
    @Inject PetRepository petRepository;
    @Inject RepositoryService repositoryService;
}
1 Results in checkboxes in the table, allowing the user to optionally check one or more instances before invoking the action.
2 Renamed as the action now works with a list of Pets
3 Signature changed.
4 The choices method is removed.

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:

  • 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 Pets, due to a referential integrity issue.

In this exercise we clean up these oversights.

Solution

git checkout tags/04-10-pets-module-cleanup
mvn clean install
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

  • 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 Pets. This abliity to control the direction of dependencies is very useful for ensuring modularity.

  • 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 Pets first.

    Also fixes tests.

  • 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.