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:
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-levelpom.xml
-
the
pom.xml
for the new visit module itself was created, and referenced from the top-levelpom.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 newvisit
schema is created (when running with an in-memory database)application.propertiescauseway.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.javaprivate 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 visit
→ petowner
dependency:
-
in the new
visit
module’spom.xml
, add a Maven dependency onpetowner
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 onPetOwnerModule
:VisitModule.java@Configuration @Import({ PetOwnerModule.class, // ... }) // ... public class VisitModule ... { ... }
And next, let’s consider the application
→ visit
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 onVisitModule
.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 thedom.visit
subpackage), declaring thepet
andvisitAt
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
fileVisit.columnOrder.txtpet 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 .
Also check that the unique index has been created correctly.
Ex 5.4: "Book Visit" action
We now want to extend our domain model so that Visit
s 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:
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.javaimport 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 contributedvisits
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 thepetowner
module aware ofvisit
module, albeit it in a very soft way.Alternatively, the
.layout.xml
can be left untouched in which case the contributedvisits
collection will be rendered in the same place as any other "unreferencedCollections".