Adding the remaining classes

Right now our domain model still only consists of the single domain class, Owner. We still have the Pet and Visit entities to add, along with the PetSpecies enum.

newPet action and Pet to Owner association

The association from Owner to Pet is bidirectional. Let’s start by tackling one side of this, from Pet to Owner.

  • We’ll add a new action to create a new Pet from an Owner:

    owner newPet

    which will prompt for the name and species of the Pet:

    owner newPet prompt
  • and, when the Pet is returned, it will be associated with the Owner that created it:

    Pet

Solution

git checkout tags/150-add-pet-1-m-collections
mvn clean package jetty:run

Exercise

  • declare the PetSpecies enum:

    public enum PetSpecies {
        Dog,
        Cat,
        Hamster,
        Budgerigar,
    }
  • let’s start with an outline of the Pet class

    @javax.jdo.annotations.PersistenceCapable(identityType = IdentityType.DATASTORE, schema = "pets" )
    @javax.jdo.annotations.DatastoreIdentity(strategy = IdGeneratorStrategy.IDENTITY, column = "id")
    @javax.jdo.annotations.Version(strategy= VersionStrategy.DATE_TIME, column ="version")
    @DomainObject(auditing = Auditing.ENABLED)
    @DomainObjectLayout()  // causes UI events to be triggered
    public class Pet implements Comparable<Pet> {
    
    }

    This won’t compile until we have implemented Comparable<Pet>.

  • let’s add in the key fields, owner and name:

    // ...
    @javax.jdo.annotations.Unique(name="Pet_owner_name_UNQ", members = {"owner","name"})
    // ...
    public class Pet implements Comparable<Pet> {
    
        public Pet(final Owner owner, final String name) {
            this.owner = owner;
            this.name = name;
        }
    
        public String title() {
            return String.format(
                    "%s (%s owned by %s)",
                    getName(), getPetSpecies().name().toLowerCase(), getOwner().getName());
        }
    
        @javax.jdo.annotations.Column(allowsNull = "false", name = "ownerId")
        @Property(editing = Editing.DISABLED)
        @Getter @Setter
        private Owner owner;
    
        @javax.jdo.annotations.Column(allowsNull = "false", length = 40)
        @Property(editing = Editing.ENABLED)
        @Getter @Setter
        private String name;
    
        @Override
        public String toString() {
            return getName();
        }
    
        @Override
        public int compareTo(final Pet other) {
            return ComparisonChain.start()
                    .compare(this.getOwner(), other.getOwner())
                    .compare(this.getName(), other.getName())
                    .result();
        }
    }
  • let’s add in a reference to PetSpecies:

    @javax.jdo.annotations.Column(allowsNull = "false")
    @Property(editing = Editing.DISABLED)
    @Getter @Setter
    private PetSpecies petSpecies;

    Since this is mandatory, we also need to update the constructor:

    // ...
    public Pet(final Owner owner, final String name, final PetSpecies petSpecies) {
        this.owner = owner;
        this.name = name;
        this.petSpecies = petSpecies;
    }
  • finally, let’s add in notes optional property:

    @javax.jdo.annotations.Column(allowsNull = "true", length = 4000)
    @Property(editing = Editing.ENABLED)
    @Getter @Setter
    private String notes;
  • We also need a PetLayout.xml and a Pet.png. The .png files should reside in the same package as the classes.

Now we need a way to create Pets.

We could create a fixture script and an Pets domain service. On the other hand, if we consider the use cases we are implementing we remember that Pets are owned by Owners, and so a better design is to make the creation (and removal) of Pets a responsibility of Owner.

Thus:

  • add a newPet action to Owner:

    @Action(semantics = SemanticsOf.NON_IDEMPOTENT)
    public Pet newPet(final String name, final PetSpecies petSpecies) {
        return repositoryService.persist(new Pet(this, name, petSpecies));
    }

Collection of Pets

At this point in our app, although the Pet knows its Owner, the opposite isn’t true.

Our design says we’d like this to be a bidirectional 1-to-many association:

Owner pets

Let’s add in the Owner#pets collection:

Solution

git checkout tags/160-collection-of-pets
mvn clean package jetty:run

Exercise

  • in the Owner class, add the pets collection:

    @Persistent(
        mappedBy = "owner",             (1)
        dependentElement = "true"       (2)
    )
    @Collection()
    @Getter @Setter
    private SortedSet<Pet> pets = new TreeSet<Pet>();
    1 specifies a bidirectional property. (Pet#owner "points back to" the Owner).
    2 Deleting an Owner will also delete any associated Pets.
  • update the Owner.layout.xml file to specify the position of the pets collection. For example:

    <bs3:tabGroup collapseIfOne="false">
    <bs3:tab name="Details">
        <bs3:row>
            <bs3:col span="12">
                <c:collection id="pets" defaultView="table"/>
            </bs3:col>
        </bs3:row>
    </bs3:tab>
    </bs3:tabGroup>
  • update the newPet action to associate with the pets collection:

    @Action(
        semantics = SemanticsOf.NON_IDEMPOTENT,
        associateWith = "pets"
    )
    public Pet newPet(final String name, final PetSpecies petSpecies) { ... }
  • we could also take the opportunity to add an action to remove a Pet:

    @Action(
        semantics = SemanticsOf.NON_IDEMPOTENT,
        associateWith = "pets", associateWithSequence = "2"
    )
    public Owner removePet(Pet pet) {
        repositoryService.removeAndFlush(pet);
        return this;
    }

When the removePet action is invoked, note how the available Pets is restricted to those in the collection. This is due to the @Action#associateWith attribute.

Extend our fixture

Before we go any further, let’s take some time out to extend our fixture so that each Owner also has some Pets.

Solution

git checkout tags/170-extend-our-fixtures
mvn clean package jetty:run

Exercise

  • update RecreateOwners by adding a PetData (Lombok) data class:

    @Data
    static class PetData {
        private final String name;
        private final PetSpecies petSpecies;
    }
  • factor out a createOwner helper method:

    private Owner createOwner(
            final String lastName,
            final String firstName,
            final String phoneNumber,
            final PetData... pets) {
        Owner owner = this.owners.create(lastName, firstName, phoneNumber);
        for (PetData pet : pets) {
            owner.newPet(pet.name, pet.petSpecies);
        }
        return owner;
    }
  • and update execute to use both:

    @Override
    protected void execute(final ExecutionContext ec) {
    
        isisJdoSupport.deleteAll(Pet.class);
        isisJdoSupport.deleteAll(Owner.class);
    
        ec.addResult(this,
                createOwner("Smith", "John", null,
                        new PetData("Rover", PetSpecies.Dog))
        );
        ec.addResult(this,
                createOwner("Jones", "Mary", "+353 1 555 1234",
                        new PetData("Tiddles", PetSpecies.Cat),
                        new PetData("Harry", PetSpecies.Budgerigar)
                ));
        ec.addResult(this,
                createOwner("Hughes", "Fred", "07777 987654",
                        new PetData("Jemima", PetSpecies.Hamster)
                ));
    }
  • rename from RecreateOwners to RecreateOwnersAndPets

Adding Visit

Our final entity is Visit. Let’s extend our app to allow Visits to be booked from an Owner's Pet:

Pet bookVisit prompt

returning

Visit

Solution

git checkout tags/180-adding-Visit
mvn clean package jetty:run

Exercise

First let’s create the Visit entity:

  • add the outline of Visit:

    @javax.jdo.annotations.PersistenceCapable(identityType = IdentityType.DATASTORE, schema = "visits" )
    @javax.jdo.annotations.DatastoreIdentity(strategy = IdGeneratorStrategy.IDENTITY, column = "id")
    @javax.jdo.annotations.Version(strategy= VersionStrategy.DATE_TIME, column ="version")
    @DomainObject(auditing = Auditing.ENABLED)
    @DomainObjectLayout()  // causes UI events to be triggered
    public class Visit implements Comparable<Visit> {
    
    }
  • add the three mandatory properties, pet, visitAt and reason:

    @javax.jdo.annotations.Column(allowsNull = "false", name = "petId")
    @Property(editing = Editing.DISABLED)
    @Getter @Setter
    private Pet pet;
    
    @javax.jdo.annotations.Column(allowsNull = "false")
    @Property(editing = Editing.DISABLED)
    @Getter @Setter
    private LocalDateTime visitAt;
    
    @javax.jdo.annotations.Column(allowsNull = "false", length = 4000)
    @Property(editing = Editing.ENABLED)
    @PropertyLayout(multiLine = 5)
    @Getter @Setter
    private String reason;
  • specify unique constraints and boilerplate for constructors, title, toString and compareTo:

    @javax.jdo.annotations.Unique(name="Visit_visitAt_pet_UNQ", members = {"visitAt","pet"})
    @javax.jdo.annotations.Index(name="Visit_pet_visitAt_IDX", members = {"pet","visitAt"})
    //...
    public class Visit implements Comparable<Visit> {
    
        public Visit(final Pet pet, final LocalDateTime visitAt, final String reason) {
            this.pet = pet;
            this.visitAt = visitAt;
            this.reason = reason;
        }
    
        public String title() {
            return String.format(
                    "%s: %s (%s)",
                    getVisitAt().toString("yyyy-MM-dd hh:mm"),
                    getPet().getOwner().getName(),
                    getPet().getName());
        }
    
        @Override
        public String toString() {
            return getVisitAt().toString("yyyy-MM-dd hh:mm");
        }
    
        @Override
        public int compareTo(final Visit other) {
            return ComparisonChain.start()
                    .compare(this.getVisitAt(), other.getVisitAt())
                    .compare(this.getPet(), other.getPet())
                    .result();
        }
    }
  • create a Visit.layout.xml layout file

We also need the ability to book a Visit (ie create a new Visit entity instance). We’ll make this a responsibility of Pet for now (we can always refactor later if we find a better place to do this):

  • add the following action to Pet:

    @Action(semantics = SemanticsOf.NON_IDEMPOTENT)
    public Visit bookVisit(
            final LocalDateTime at,
            @Parameter(maxLength = 4000)
            @ParameterLayout(multiLine = 5)
            final String reason) {
        return repositoryService.persist(new Visit(this, at, reason));
    }
    
    @javax.jdo.annotations.NotPersistent
    @javax.inject.Inject
    RepositoryService repositoryService;