The visit module and Visit entity

Our domain model now consists of the PetOwner and Pet entities (along with the PetSpecies enum). In this section we’ll add in the Visit entity:

Diagram

Also, note that the Visit entity is in its own module, the visit module. So in this section of exercises we’ll start to learn how Apache Causeway helps you keep your applications modular.

Ex 5.1: The visits module

In this exercise we’ll just create an empty visits module.

Solution

git checkout tags/v2/05-01-visit-module
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"

Tasks

To save time, just checkout the solution tag above and review the git history to see the files and classes that were created:

  • A new simpleapp-jpa-module-visit maven module was defined in the top-level pom.xml

  • the pom.xml for the new visit module itself was created, and referenced from the top-level pom.xml as a child <module>

  • a VisitModule class was created, defining the module to Causeway as a Spring @Configuration:

    VisitModule.java
    @Configuration
    @Import({
            CausewayModuleExtPdfjsApplib.class,
            CausewayModuleExtFullCalendarApplib.class,
            CausewayModuleTestingFakeDataApplib.class,
            CausewayModulePersistenceJpaApplib.class,
    })
    @ComponentScan
    @EnableJpaRepositories
    @EntityScan(basePackageClasses = {VisitModule.class})
    public class VisitModule implements ModuleWithFixtures {
    
        public static final String NAMESPACE = "visit";
        public static final String SCHEMA = "visit";
    
        @Override
        public FixtureScript getTeardownFixture() {
            return new TeardownFixtureJpaAbstract() {
                @Override
                protected void execute(ExecutionContext executionContext) {
                }
            };
        }
    }
  • the top-level application.properties was updated to ensure that the new visit schema is created (when running with an in-memory database)

    application.properties
    causeway.persistence.schema.auto-create-schemas=\
        petowner,visit,simple,...
  • add permissions to the new "visit" namespace. We could do this by adding a new security role, but for simplicity we’ll just add to the existing role (PetOwnerModuleSuperuserRole), renaming it as we do:

    CustomRolesAndUsers.java
    private static class PetClinicSuperuserRole                         (1)
        extends AbstractRoleAndPermissionsFixtureScript {
    
        public static final String ROLE_NAME = "petclinic-superuser";   (2)
    
        public PetClinicSuperuserRole() {
            super(ROLE_NAME, "Permission to use everything in the 'petowner' and 'visit' modules");
        }
    
        @Override
        protected void execute(ExecutionContext executionContext) {
            newPermissions(
                    ApplicationPermissionRule.ALLOW,
                    ApplicationPermissionMode.CHANGING,
                    Can.of(ApplicationFeatureId.newNamespace("petowner"),
                           ApplicationFeatureId.newNamespace("visit")       (3)
                    )
            );
        }
    }
    1 renamed
    2 renamed
    3 added

Ex 5.2: Visit Module Dependencies

Although we have a visit module, it currently is unaware of the pet owner module. And similarly, the top-level application doesn’t yet know about visit module.

In this exercise we’ll fix this, so that:

application module → visit module → petowner module

Solution

git checkout tags/v2/05-02-visit-module-dependencies
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"

Tasks

First, let’s consider the visitpetowner dependency:

  • in the new visit module’s pom.xml, add a Maven dependency on petowner module:

    visit/pom.xml
    <dependency>
        <groupId>org.apache.causeway.starters</groupId>
        <artifactId>simpleapp-jpa-module-petowner</artifactId>
    </dependency>
    In your IDE you may need to reload/refresh dependencies to rebuild the classpath.
  • in the VisitModule Causeway module, add a corresponding import on PetOwnerModule:

    VisitModule.java
    @Configuration
    @Import({
            PetOwnerModule.class,
            // ...
    })
    // ...
    public class VisitModule ... { ... }

And next, let’s consider the applicationvisit dependency:

  • in the webapp module, add a Maven dependency on the visit module:

    visit/pom.xml
    <dependency>
        <groupId>org.apache.causeway.starters</groupId>
        <artifactId>simpleapp-jpa-module-visit</artifactId>
    </dependency>

    The dependency on <artifactId>simpleapp-jpa-module-petowner</artifactId> can also be removed, due to transitivity.

  • And, in the webapp’s ApplicationModule Causeway module, add a corresponding import on VisitModule.

    ApplicationModule.java
    @Configuration
    @Import({
            VisitModule.class,
            SimpleModule.class,
    })
    @ComponentScan
    public class ApplicationModule {
    }

    The import of PetOwnerModule.class can similarly be removed, due to transitivity.

Run the application and check that it starts ok.

Ex 5.3: Visit entity’s key properties

Now we have a visits module, we can now add in the Visit entity. We’ll start just with the key properties.

Solution

git checkout tags/v2/05-03-visit-entity-key-properties
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"

Tasks

  • add a Visit entity (in the dom.visit subpackage), declaring the pet and visitAt key properties:

    Visit.java
    @Entity
    @Table(
            schema=VisitModule.SCHEMA,
            uniqueConstraints = {
                    @UniqueConstraint(name = "Visit__pet_visitAt__UNQ", columnNames = {"pet_id", "visitAt"})
            }
    )
    @EntityListeners(CausewayEntityListener.class)
    @Named(VisitModule.NAMESPACE + ".Visit")
    @DomainObject(entityChangePublishing = Publishing.ENABLED)
    @DomainObjectLayout()
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @XmlJavaTypeAdapter(PersistentEntityAdapter.class)
    @ToString(onlyExplicitlyIncluded = true)
    public class Visit implements Comparable<Visit> {
    
        @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")
        @Getter @Setter
        private long version;
    
        public Visit(Pet pet, LocalDateTime visitAt) {
            this.pet = pet;
            this.visitAt = visitAt;
        }
    
        @ObjectSupport
        public String title() {
            return titleService.titleOf(getPet())       (1)
                    + " @ "
                    + getVisitAt().format(DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"));
        }
    
        @ManyToOne(optional = false)
        @JoinColumn(name = "pet_id")
        @PropertyLayout(fieldSetId = "identity", sequence = "1")
        @Getter @Setter
        private Pet pet;
    
        @Column(name = "visitAt", nullable = false)
        @Getter @Setter
        @PropertyLayout(fieldSetId = "identity", sequence = "2")
        private LocalDateTime visitAt;
    
    
        private final static Comparator<Visit> comparator =
                Comparator.comparing(Visit::getPet).thenComparing(Visit::getVisitAt);
    
        @Override
        public int compareTo(final Visit other) {
            return comparator.compare(this, other);
        }
    
        @Inject @Transient TitleService titleService;   (1)
    }
    1 uses the injected TitleService to obtain the title of the object, as determined by the framework.
  • create a Visit.layout.xml layout file:

    Visit.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>
  • create a Visit.columnOrder.txt file

    Visit.columnOrder.txt
    pet
    visitAt
    #id
    #version
  • download a Visit.png file.

Run the application, and login as secman-admin/pass to confirm that the table is created correctly using Prototyping  H2 Console. Also check that the unique index has been created correctly.

Visit entity

Ex 5.4: "Book Visit" action

We now want to extend our domain model so that Visits to be created.

However, there’s a problem:

  • we would like that behaviour to reside on PetOwner (say)

  • however the pet owner module doesn’t know about visits.

We can see this in the domain model:

Diagram

Causeway’s solution to this is to allow the visit module to define behaviour, but have the behaviour seem to belong to the Pet entity, at least so far as the user interface is concerned. This is done using a mixin.

Because Visit is its own root entity, we’re also going to need a repository to be able to look them up for a given Pet.

In this exercise we’ll define the repository, and create the "book visit" mixin action (also sometimes called a contributed action. We’ll also create a mixin collection to be able to view the visits from a PetOwner's UI, too.

Solution

git checkout tags/v2/05-04-book-visit-action
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"

Tasks

  • create the VisitRepository, using Spring Data:

    VisitRepository.java
    import org.springframework.data.repository.Repository;
    
    // ...
    
    public interface VisitRepository extends Repository<Visit, Long> {
    
        @Query("select v from Visit v where v.pet.petOwner = :petOwner")
        List<Visit> findByPetOwner(PetOwner petOwner);
    
    }
  • define the "book visit" mixin action on PetOwner, in the visit module.

    we simply use a datetime to capture when the visit occurs. This isn’t particularly realistic, we know - there would probably be a domain concept such as AppointmentSlot.
    PetOwner_bookVisit.java
    @Action                                                     (1)
    @ActionLayout(associateWith = "visits")                     (2)
    @RequiredArgsConstructor
    public class PetOwner_bookVisit {                           (3)
    
        private final PetOwner petOwner;                        (4)
    
        @MemberSupport
        public PetOwner act(Pet pet, LocalDateTime visitAt) {
            Visit visit = new Visit(pet, visitAt);
            repositoryService.persistAndFlush(visit);           (5)
            return petOwner;
        }
        @MemberSupport
        public Set<Pet> choices0Act() {                         (6)
            return petOwner.getPets();
        }
        @MemberSupport
        public Pet default0Act() {                              (7)
            Set<Pet> pets = petOwner.getPets();
            return pets.size() == 1 ? pets.iterator().next() : null;
        }
        @MemberSupport
        public LocalDateTime default1Act() {                    (7)
            return officeHoursTomorrow();
        }
        @MemberSupport
        public String validate1Act(LocalDateTime visitAt) {
            if (visitAt.isBefore(officeHoursTomorrow())) {
                return "Must book in the future";
            }
            return null;
        }
    
        private LocalDateTime officeHoursTomorrow() {
            return clockService.getClock().nowAsLocalDate().atStartOfDay().plusDays(1).plusHours(9);
        }
    
    
        @Inject ClockService clockService;
        @Inject RepositoryService repositoryService;            (5)
    }
    1 indicates that this class is a mixin action
    2 anticipates there being a "visits" collection also
    3 the name of the contributed action is inferred from the mixin’s class name
    4 the type to which this mixin is being contributed, that is, PetOwner
    5 injected RepositoryService acts as a facade to the database for all entities. For querying it’s usually worth defining a custom repository.
    6 the choices…​() supporting method provides programmatic set of choices for a parameter, in this case for the 0th parameter Pet, rendered as a drop-down list.
    7 the default…​() supporting method returns a default value for a parameter.
  • define the "visits" mixin collection on PetOwner, in the visit module.

    PetOwner_visits.java
    @Collection                                     (1)
    @RequiredArgsConstructor
    public class PetOwner_visits {
    
        private final PetOwner petOwner;            (2)
    
        @MemberSupport
        public List<Visit> coll() {
            return visitRepository.findByPetOwner(petOwner);
        }
    
        @Inject VisitRepository visitRepository;
    }
    1 indicates that this class is a mixin collection
    2 the type to which this mixin is being contributed, that is, PetOwner
  • update PetOwner's .layout.xml, to indicate where the contributed visits collection should be placed:

    PetOwner.layout.xml
    <bs3:col span="12">
        <bs3:row>
            <bs3:col span="12">
                <cpt:collection id="pets"/>
            </bs3:col>
        </bs3:row>
        <bs3:row>
            <bs3:col span="12">
                <cpt:collection id="visits"/>
            </bs3:col>
        </bs3:row>
        <cpt:fieldSet name="Content" id="content">
            ...
        </cpt:fieldSet>
    </bs3:col>

    Strictly speaking, updating the .layout.xml does make the petowner module aware of visit module, albeit it in a very soft way.

    Alternatively, the .layout.xml can be left untouched in which case the contributed visits collection will be rendered in the same place as any other "unreferencedCollections".