You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2021/10/05 15:22:34 UTC

[isis] branch ISIS-2873-petclinic updated: ISIS-2873: ex 5.2, 5.3

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch ISIS-2873-petclinic
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/ISIS-2873-petclinic by this push:
     new 9aeceec  ISIS-2873: ex 5.2, 5.3
9aeceec is described below

commit 9aeceec38bc25e5b223d1a30010f553e1514e8ed
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Tue Oct 5 16:21:47 2021 +0100

    ISIS-2873: ex 5.2, 5.3
---
 .../modules/petclinic/pages/040-pet-entity.adoc    |  44 ++-
 .../modules/petclinic/pages/050-visit-entity.adoc  | 324 +++++++++++++++------
 2 files changed, 285 insertions(+), 83 deletions(-)

diff --git a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
index 13599de..35fdf5c 100644
--- a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
@@ -11,7 +11,7 @@ include::partial$domain.adoc[]
 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 ``Pet``s, with actions to add or remove `Pet` instances for that collection.
 
-== Exercise 4.1: Pet domain class
+== Exercise 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.
 
@@ -896,6 +896,8 @@ public String default0Act() {
 
 === Optional exercise
 
+NOTE: 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 xref:refguide:applib-methods:prefixes.adoc#choices[choices] method using the xref:refguide:applib:index/annotation/Action.adoc#choicesFrom[@Action#choicesFrom] attribute.
 
 Add this mixin to allow multiple ``Pet``s to be removed at the same time:
@@ -936,3 +938,43 @@ public class PetOwner_removePets {                      // <.>
 
 
 
+== Exercise 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 ``Pet``s, due to a referential integrity issue.
+
+In this exercise we clean up these oversights.
+
+
+
+=== Solution
+
+[source,bash]
+----
+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 ``Pet``s.
+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 ``Pet``s 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.
diff --git a/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
index 4bdb3f4..7951c88 100644
--- a/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
@@ -1,47 +1,85 @@
-= Adding the remaining classes
+= Visit module and entity
 
 :Notice: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or ag [...]
 
 
-ur domain model now consists of the domain class, `PetOwner` and `Pet` entities (along with the `PetSpecies`  enum).
-We still have the `Visit` entities to add:
+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:
 
 include::partial$domain.adoc[]
 
-In this set of exercises we'll focus on this final `Visit` entity.
+Also, note that the `Visit` entity is in its own module.
+We'll look at the important topic of modularity in later exercises.
 
 
 == Exercise 5.1: The visits module
 
+In this exercise we'll just create an empty visits module.
+
+=== Solution
+
 [source,bash]
 ----
-git checkout tags/05-01-visits-module
+git checkout tags/05-01-visit-module
 mvn clean install
 mvn -pl spring-boot:run
 ----
 
 
+
+
 === Tasks
 
-* TODO
+Just check out the tag above and inspect the changes:
+
+* A new `petclinic-module-visits` maven module has been created
+
+* its `pom.xml` declares a dependency on the ``petclinic-module-visits` maven module
+
+* the top-level pom.xml declares the new Maven module and references it
+
+* the `VisitsModule` class is a Spring `@Configuration` bean that resides in the root of the visits module, and declares an app dependency on the pets module that mirrors the maven dependency:
++
+[source,java]
+.VisitsModule.java
+----
+@Configuration
+@ComponentScan
+@Import(PetsModule.class)
+@EnableJpaRepositories
+@EntityScan(basePackageClasses = {VisitsModule.class})
+public class VisitsModule implements ModuleWithFixtures {
+
+    @Override
+    public FixtureScript getTeardownFixture() {
+        return new FixtureScript() {
+            @Override
+            protected void execute(ExecutionContext executionContext) {
+                // nothing to do
+            }
+        };
+    }
+}
+----
+
+* the webapp Maven module now depends on the new visits maven module, and the top-level `ApplicationModule` Spring `@Configuration` bean now depends upon `VisitsModule` rather than `PetsModule`
++
+It still depends upon `PetsModule`, but now as a transitive dependency.
 
 
-== Exercise 5.2: Adding the Visit entity
 
-Our final entity is `Visit`.
-Let's extend our app to allow ``Visit``s to be booked from an ``PetOwner``'s ``Pet``:
 
-image::Pet-bookVisit-prompt.png[width="800px",link="_images/Pet-bookVisit-prompt.png"]
 
-returning
+== Exercise 5.2: Visit entity's key properties
 
-image::Visit.png[width="800px",link="_images/Visit.png"]
+Now we have a visits module, we can now add in the `Visit` entity.
+We'll start just with the key properties.
 
 
 
 [source,bash]
 ----
-git checkout tags/05-02-visit-entity
+git checkout tags/05-02-visit-entity-key-properties
 mvn clean install
 mvn -pl spring-boot:run
 ----
@@ -50,105 +88,227 @@ mvn -pl spring-boot:run
 
 === Tasks
 
-* TODO
+* add a `Visit` entity, declaring the `pet` and `visitedAt` key properties:
++
+[source,java]
+.Visit.java
+----
+@Entity
+@Table(
+    schema="visits",        // <.>
+    name = "Visit",
+    uniqueConstraints = {
+        @UniqueConstraint(name = "Visit__pet_visitAt__UNQ", columnNames = {"owner_id", "name"})
+    }
+)
+@EntityListeners(IsisEntityListener.class)
+@DomainObject(logicalTypeName = "visits.Visit", entityChangePublishing = Publishing.ENABLED)
+@DomainObjectLayout()
+@NoArgsConstructor(access = AccessLevel.PUBLIC)
+@XmlJavaTypeAdapter(PersistentEntityAdapter.class)
+@ToString(onlyExplicitlyIncluded = true)
+public class Visit implements Comparable<Visit> {
 
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    @Column(name = "id", nullable = false)
+    @Getter @Setter
+    @PropertyLayout(fieldSetId = "metadata", sequence = "1")
+    private Long id;
 
-First let's create the `Visit` entity:
+    @Version
+    @Column(name = "version", nullable = false)
+    @PropertyLayout(fieldSetId = "metadata", sequence = "999")
+    @Getter @Setter
+    private long version;
 
-* add the outline of `Visit`:
+
+    Visit(Pet pet, LocalDateTime visitAt) {
+        this.pet = pet;
+        this.visitAt = visitAt;
+    }
+
+
+    public String title() {
+        return titleService.titleOf(getPet()) + " @ " + getVisitAt().format(DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"));
+    }
+
+    @ManyToOne(optional = false)
+    @JoinColumn(name = "pet_id")
+    @PropertyLayout(fieldSetId = "name", sequence = "1")
+    @Getter @Setter
+    private Pet pet;
+
+    @Column(name = "visitAt", nullable = false)
+    @Getter @Setter
+    @PropertyLayout(fieldSetId = "name", 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;
+}
+----
+<.> in the "visits" schema.
+Modules are vertical, cutting through the layers.
+Therefore the database schemas echo the Spring ``@Configuration``s and maven modules.
 +
-[source,xml]
+Run the application, and confirm that the table is created correctly using menu:Prototyping[H2 Console].
+
+
+
+
+== Exercise 5.2: "Book Visit" action
+
+In addition to the key properties, the `Visit` has one further mandatory property, `reason`.
+This is required to be specified when a `Visit` is created ("what is the purpose of this visit?")
+
+In this exercise we'll add that additional property and use a mixin to allow ``Visit``s to be created.
+
+
+[source,bash]
 ----
-@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> {
+git checkout tags/05-03-schedule-visit-action
+mvn clean install
+mvn -pl spring-boot:run
+----
+
+
+=== Tasks
 
+* add the `@Reason` meta-annotation
++
+[source,java]
+.Reason.java
+----
+@Property(maxLength = Reason.MAX_LEN)
+@PropertyLayout(named = "Reason")
+@Parameter(maxLength = Reason.MAX_LEN)
+@ParameterLayout(named = "Reason")
+@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Reason {
+
+    int MAX_LEN = 255;
 }
 ----
 
-* add the three mandatory properties, `pet`, `visitAt` and `reason`:
+* add the `reason` mandatory property:
 +
-[source,xml]
+[source,java]
+.Visit.java
 ----
-@javax.jdo.annotations.Column(allowsNull = "false", name = "petId")
-@Property(editing = Editing.DISABLED)
+@Reason
+@Column(name = "reason", length = FirstName.MAX_LEN, nullable = false)
 @Getter @Setter
-private Pet pet;
+@PropertyLayout(fieldSetId = "details", sequence = "1")
+private String reason;
+----
 
-@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;
+* update constructor (as this is a mandatory property)
++
+[source,java]
+.Visit.java
+----
+Visit(Pet pet, LocalDateTime visitAt, String reason) {
+    this.pet = pet;
+    this.visitAt = visitAt;
+    this.reason = reason;
+}
 ----
 
-* specify unique constraints and boilerplate for constructors, title, toString and compareTo:
+* create a "visits" mixin collection as a mixin of `Pet`, so we can see the ``Visit``s that have been booked:
 +
-[source,xml]
+[source,java]
+.Pet_visits.java
 ----
-@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> {
+@Collection
+@CollectionLayout(defaultView = "table")
+@RequiredArgsConstructor
+public class Pet_visits {
 
-    public Visit(final Pet pet, final LocalDateTime visitAt, final String reason) {
-        this.pet = pet;
-        this.visitAt = visitAt;
-        this.reason = reason;
-    }
+    private final Pet pet;
 
-    public String title() {
-        return String.format(
-                "%s: %s (%s)",
-                getVisitAt().toString("yyyy-MM-dd hh:mm"),
-                getPet().getOwner().getName(),
-                getPet().getName());
+    public List<Visit> coll() {
+        return visitRepository.findByPetOrderByVisitAtDesc(pet);
     }
 
-    @Override
-    public String toString() {
-        return getVisitAt().toString("yyyy-MM-dd hh:mm");
-    }
+    @Inject VisitRepository visitRepository;
+}
+----
 
-    @Override
-    public int compareTo(final Visit other) {
-        return ComparisonChain.start()
-                .compare(this.getVisitAt(), other.getVisitAt())
-                .compare(this.getPet(), other.getPet())
-                .result();
+* create a "bookVisit" mixin action (in the visits module), as a mixin of `Pet`.
++
+We can use xref:refguide:applib:index/services/clock/ClockService.adoc[ClockService] to ensure that the date/time specified is in the future, and to set a default date/time for "tomorrow"
++
+[source,java]
+.Pet_bookVisit.java
+----
+@Action(
+        semantics = SemanticsOf.IDEMPOTENT,
+        commandPublishing = Publishing.ENABLED,
+        executionPublishing = Publishing.ENABLED
+)
+@ActionLayout(associateWith = "visits", sequence = "1")
+@RequiredArgsConstructor
+public class Pet_bookVisit {
+
+    private final Pet pet;
+
+    public Visit act(
+            LocalDateTime visitAt,
+            @Reason final String reason
+            ) {
+        return repositoryService.persist(new Visit(pet, visitAt, reason));
+    }
+    public String validate0Act(LocalDateTime visitAt) {
+        return clockService.getClock().nowAsLocalDateTime().isBefore(visitAt)   // <.>
+                ? null
+                : "Must be in the future";
+    }
+    public LocalDateTime default0Act() {
+        return clockService.getClock().nowAsLocalDateTime()                     // <.>
+                .toLocalDate()
+                .plusDays(1)
+                .atTime(LocalTime.of(9, 0));
     }
+
+    @Inject ClockService clockService;
+    @Inject RepositoryService repositoryService;
 }
 ----
+<.> ensures that the date/time specified is in the future.
+<.> defaults to 9am tomorrow morning.
+
+Also add in the UI files:
 
-* create a `Visit.layout.xml` layout file
+* 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 a `Visit.png` file
 
-* add the following action to `Pet`:
+* add a `Pet#visits.columnOrder.txt` file
 +
-[source,java]
-----
-@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));
-}
+to define which properties of Visit are visible as columns in ``Pet``'s `visits` collection.
 
-@javax.jdo.annotations.NotPersistent
-@javax.inject.Inject
-RepositoryService repositoryService;
-----
 
 
+=== Optional exercises
+
+NOTE: 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.
+
+. Download a separate `Visit-NN.png` for each of the days of the month (1 to 31), and then use `iconName()` to show a more useful icon based on the `visitAt` date.
+
+. Use choices to provide a set of available date/times, in 15 minutes slots, say.
+
+. Refine the list of slots to filter out any visits that already exist
++
+Assume that visits take 15 minutes, and that only on visit can happen at a time.
+