Fleshing out the Owner
entity
Rework Owner
's name (firstName
and lastName
)
In the domain we are working on, Owner
has a firstName
and a lastName
property, not a single name
property.
Let’s update Owner
accordingly.
Solution
git checkout tags/070-rework-Owners-name-firstName-and-lastName
mvn clean package jetty:run
Exercise
-
rename property
Owner#name
→Owner#lastName
-
add property
Owner#firstName
:@javax.jdo.annotations.Column(allowsNull = "false", length = 40) @Property @Getter @Setter private String firstName;
-
update the constructor:
public Owner(final String lastName, final String firstName) { this.lastName = lastName; this.firstName = firstName; }
-
remove
@Title
annotation fromlastName
property, and add atitle()
method to derive from both properties:public String title() { return getLastName() + ", " + getFirstName().substring(0,1); }
-
update
Owner.layout.xml
<c:property id="lastName" namedEscaped="true"/> <c:property id="firstName" namedEscaped="true"> <c:action id="updateName"> <c:describedAs>Updates the object's name</c:describedAs> </c:action> </c:property>
This will place the button for
updateName
underneath thefirstName
property. -
update the corresponding JDO
@Unique
annotation forOwner
:@javax.jdo.annotations.Unique(name="Owner_lastName_firstName_UNQ", members = {"lastName", "firstName"})
-
update the implementation of
Comparable
forOwner
:@Override public int compareTo(final Owner other) { return ComparisonChain.start() .compare(this.getLastName(), other.getLastName()) .compare(this.getFirstName(), other.getFirstName()) .result(); }
The archetype includes a dependency on guava, which is where
ComparisonChain
comes from. -
update
Owner#updateName
to also accept a newfirstName
parameter:@Action(semantics = SemanticsOf.IDEMPOTENT, command = CommandReification.ENABLED, publishing = Publishing.ENABLED) public Owner updateName( @Parameter(maxLength = 40) final String lastName, @Parameter(maxLength = 40) final String firstName) { setLastName(lastName); setFirstName(firstName); return this; } public String default0UpdateName() { return getLastName(); } public String default1UpdateName() { return getFirstName(); }
The "default" supporting methods are called when the action prompt is rendered, providing the default for the "Nth" parameter of the corresponding action.
-
update
Orders#create
:@Action(semantics = SemanticsOf.NON_IDEMPOTENT) @MemberOrder(sequence = "1") public Owner create( @Parameter(maxLength = 40) final String lastName, @Parameter(maxLength = 40) final String firstName) { return repositoryService.persist(new Owner(lastName, firstName)); }
-
update
Orders#findByName
to search across either thefirstName
or thelastName
:@Action(semantics = SemanticsOf.SAFE) @MemberOrder(sequence = "2") public List<Owner> findByName( final String name) { TypesafeQuery<Owner> q = isisJdoSupport.newTypesafeQuery(Owner.class); final QOwner cand = QOwner.candidate(); q = q.filter( cand.lastName.indexOf(q.stringParameter("name")).ne(-1).or( cand.firstName.indexOf(q.stringParameter("name")).ne(-1) ) ); return q.setParameter("name", name) .executeList(); }
-
update test class
OwnerTest_updateName
-
update test class
OwnerTest_delete
Derived name property
The Owner
's firstName
and lastName
properties are updated using the updateName
action, but when the action’s button is invoked, it only "replaces" the firstName
property:
We can improve this by introducing a derived name
property and then hiding the firstName
and lastName
:
And, when Owner#updateName
is invoked, the prompt we’ll see is:
Exercise
-
Add
getName()
as the derivedname
property:@Property(notPersisted = true) public String getName() { return getFirstName() + " " + getLastName(); }
-
Hide the
firstName
andlastName
properties, using@Property(hidden=…)
:@javax.jdo.annotations.Column(allowsNull = "false", length = 40) @Property(hidden = Where.EVERYWHERE) @Getter @Setter private String lastName;
-
Update the
Owner.layout.xml
layout file:<c:property id="name" namedEscaped="true"> <c:action id="updateName"> <c:describedAs>Updates the object's name</c:describedAs> </c:action> </c:property>
Digression: Changing the App Name
Let’s remove the remaining vestiges of the "hello world" archetype, and rename our application to "pet clinic".
Exercise
Rename:
-
HelloWorldModule
→PetClinicModule
-
HelloWorldApplication
→PetClinicApplication
-
Also update the string literals in
newIsisWicketModule()
method -
Also update the reference to the application class in
web.xml
.
-
-
HelloWorldAppManifest
→PetClinicAppManifest
-
Also update
isis.appManifest
property in theisis.properties
file
-
-
various text references to "Hello World" or "HelloWorld" literals in
pom.xml
,index.html
andwelcome.html
files
Changing the "Object Type" Class Alias
The Apache Isis framework allows an optional alias to be specified for each domain class; this is called the "objectType".
This crops up in various places, including the menubars.layout.xml
, and the REST API.
It can used when persisting data, eg to hold a reference to an arbitrary domain object (a "polymorphic association").
It’s good practice to specify an object type, because it makes refactoring easier if we subsequently move the class to another package, or rename it.
Exercise
-
Update
Owners
domain service. The object type alias is specified in@DomainService(objectType=…)
:@DomainService( nature = NatureOfService.VIEW_MENU_ONLY, objectType = "pets.Owners" ) public class Owners { ... }
-
Update
Owner
domain entity. The object type alias is derived from the database schema and the class’s (simple) name:@javax.jdo.annotations.PersistenceCapable(identityType = IdentityType.DATASTORE, schema = "pets" ) ... public class Owner implements Comparable<Owner> { ... }
-
also, update
menubars.layout.xml
, changing "myapp.Owners" to "pets.Owners".
Add other properties for Owner
Let’s add the two remaining properties for Owner
.
Exercise
-
Add
phoneNumber
as an editable property and use a regex to limit the allowed characters:@javax.jdo.annotations.Column(allowsNull = "true", length = 15) @Property( editing = Editing.ENABLED, regexPattern = "[+]?[0-9 ]+", regexPatternReplacement = "Specify only numbers and spaces, optionally prefixed with '+'. " + "For example, '+353 1 555 1234', or '07123 456789'" ) @Getter @Setter private String phoneNumber;
Until we update
Owner.layout.xml
, then the new property will be added to the section specifiedunreferencedProperties="true"
, in other words a field set called "Other". -
Add
emailAddress
as an editable property and use a supportingvalidate
method to verify the format:@javax.jdo.annotations.Column(allowsNull = "true", length = 50) @Property(editing = Editing.ENABLED) @Getter @Setter private String emailAddress; public String validateEmailAddress(String emailAddress) { return emailAddress.contains("@") ? null : "Email address must contain a '@'"; }
Obviously in this previous case we could also have used a declarative approach, but using a "validate" method here shows that arbitrary logic can be used. For example, we could delegate to an injected domain service to actually validate the email.
-
update
Owner.layout.xml
.While we are at it, we could move the
notes
property to its own tab:<bs3:tab name="Contact Details"> <bs3:row> <bs3:col span="12"> <c:fieldSet name="Contact Details"> <c:property id="emailAddress"/> <c:property id="phoneNumber"/> </c:fieldSet> </bs3:col> </bs3:row> </bs3:tab> <bs3:tab name="Notes"> <bs3:row> <bs3:col span="12"> <c:fieldSet name="Notes"> <c:property id="notes" namedEscaped="true" multiLine="10" hidden="ALL_TABLES"/> </c:fieldSet> </bs3:col> </bs3:row> </bs3:tab>
resulting in:
Using specifications to encapsulate business rules
When we create a new Owner
we specify only the first and last name.
If the owner has a phone number, then the user has to edit that property separately.
Suppose we wanted to allow the user to optionally enter the phone number when the Owner
is first created?
That would require extending the Owners#create(…)
action to also accept an optional "phoneNumber" parameter.
However, the regex validation rule that we’ve specified on Owner#phoneNmber
will need duplicating for the phoneNumber
parameter; the framework doesn’t "know" that the value is to be used to populate that particular property.
But duplicating validation violates the single responsibility principle.
Instead, we can move the validation logic into a "specification", and associate both the property and the parameter with that specification.
Solution
git checkout tags/120-using-specifications-to-encapsulate-business-rules
mvn clean package jetty:run
Exercise
-
factor out a
PhoneNumberSpec
:public static class PhoneNumberSpec extends AbstractSpecification<String> { @Override public String satisfiesSafely(final String phoneNumber) { Matcher matcher = Pattern.compile("[+]?[0-9 ]+").matcher(phoneNumber); return matcher.matches() ? null : "Specify only numbers and spaces, optionally prefixed with '+'. " + "For example, '+353 1 555 1234', or '07123 456789'"; } }
In this case it isn’t required, but we could if we wanted inject domain services into this specification class.
-
refactor the
phoneNumber
property to use this spec:@javax.jdo.annotations.Column(allowsNull = "true", length = 15) @Property(editing = Editing.ENABLED, mustSatisfy = PhoneNumberSpec.class ) @Getter @Setter private String phoneNumber;
-
extend the
Orders#create
action to also extend aphoneNumber
parameter, and use thePhoneNumberSpec
to implement the same business rule:using this code:
@Action(semantics = SemanticsOf.NON_IDEMPOTENT) @MemberOrder(sequence = "1") public Owner create( @Parameter(maxLength = 40) final String lastName, @Parameter(maxLength = 40) final String firstName, @Parameter( mustSatisfy = Owner.PhoneNumberSpec.class, maxLength = 15, optionality = Optionality.OPTIONAL ) final String phoneNumber) { Owner owner = new Owner(lastName, firstName); owner.setPhoneNumber(phoneNumber); return repositoryService.persist(owner); }
The above refactoring isn’t perfect: there is still some repetition of the length of the property/parameter, for example. The next version of the framework will support custom meta-annotations which will address this. Then, you’ll be able to write:
and then:
and
|