PetOwner entity
In this set of exercises we’ll just focus on the PetOwner
entity.
Ex 3.1: Add PetOwner’s knownAs property
In this exercise, we’ll add a new property, knownAs
, to capture the PetOwner
's preferred name.
We’ll also update the fixture script.
Solution
git checkout tags/v2/03-01-adds-PetOwner-knownAs-property
mvn clean install
mvn -pl webapp spring-boot:run
Use the
menu to setup some example data.Tasks
-
add a
knownAs
property:PetOwner.java@Column(length = 40, nullable = true, name = "knownAs") (1) @Getter @Setter @Property(editing = Editing.ENABLED) (2) @PropertyLayout( fieldSetId = LayoutConstants.FieldSetId.IDENTITY, (3) sequence = "1.1" (4) ) private String knownAs;
1 JPA annotation, declared to be nullable
because we don’t necessarily have a value2 The @Property annotation provides domain semantics. (In comparison, the @PropertyLayout annotation defines UI hints/semantics. Directly editable (similar to the "notes" property) 3 Renders this new property in the same field set as name
…4 … and after the name
, not before itIt’s also possible to define UI semantics through the associated
.layout.xml
file; we’ll look at that alternative in a later exercise. -
update
PetOwner_persona
to specify anknownAs
for some of the owners:PetOwner_persona.javapublic enum PetOwner_persona implements Persona<PetOwner, PetOwner_persona.Builder> { JAMAL("Jamal Washington", "jamal.pdf", "J"), CAMILA("Camila González", "camila.pdf", null), ARJUN("Arjun Patel", "arjun.pdf", null), NIA("Nia Robinson", "nia.pdf", null), OLIVIA("Olivia Hartman", "olivia.pdf", null), LEILA("Leila Hassan", "leila.pdf", null), MATT("Matthew Miller", "matt.pdf", "Matt"), BENJAMIN("Benjamin Thatcher", "benjamin.pdf", "Ben"), JESSICA("Jessica Raynor", "jessica.pdf", "Jess"), DANIEL("Daniel Keating", "daniel.pdf", "Dan"); private final String name; private final String contentFileName; private final String knownAs; ... }
-
for the builder, use the
knownAs
value of the entity once created:PetOwner_persona.java@Override protected PetOwner buildResult(final ExecutionContext ec) { val petOwner = wrap(PetOwners).create(persona.name); // ... if (persona.knownAs != null) { petOwner.setKnownAs(persona.knownAs); } // ... }
Ex 3.2: Define PetOwner’s title imperatively
Every domain object has a title, allowing end-users to distinguish one object instance from another. There’s no requirement for this title to be unique, but it does need to be unique "enough".
Our app currently declares the title of PetOwner
declaratively using the @Title annotation on the name
property.
In this exercise, let’s change that to also include the knownAs
property as defined.
Our rule will be:
-
if there is a
knownAs
, then the title should be "<name> (<knownAs>)" -
but if
knownAs
is empty, then the title should be simply thename
, as it is now.
To do that, we need to define the title imperatively, using the title() method.
Solution
git checkout tags/v2/03-02-define-PetOwner-title-imperatively
mvn clean install
mvn -pl webapp spring-boot:run
Tasks
-
locate the
name
property ofPetOwner
, and remove the@Title
annotation -
add a new
title()
method:PetOwner.java@ObjectSupport (1) public String title() { return getName() + (getKnownAs() != null ? " (" + getKnownAs() + ")" : ""); }
1 The @ObjectSupport annotation tells the framework to include this method in the metamodel.
Ex 3.3: Add remaining PetOwner
properties
Comparing PetOwner
as it is currently defined with our domain model, there are a couple of changes still to make:
-
we need to rename
lastCheckedIn
property tolastVisit
property -
we still need to add in a
telephoneNumber
property, and anemailAddress
property
We’ll make these changes in this exercise.
Solution
git checkout tags/v2/03-03-remaining-PetOwner-properties
mvn clean install
mvn -pl webapp spring-boot:run
Tasks
Rename the lastCheckedIn
property to lastVisit
:
-
rename the field in
PetOwner
-
update
PetOwner_persona
also (but your IDE probably refactored this already). -
to make it more realistic, let’s change the fixture script so that the value of
lastVisit
is some number of days in the past:PetOwner_persona.javafinal var numDaysAgo = fakeDataService.ints().between(100, 2); (1) final var lastVisit = clockService.getClock().nowAsLocalDate().minusDays(numDaysAgo); (2) petOwner.setLastVisit(lastVisit);
1 The FakeDataService provides an easy way to create fake data for testing and prototyping 2 It’s good practice to use ClockService, so it can be easily mocked in tests
Now add the two new properties:
-
Add the
telephoneNumber
property:PetOwner.java@Column(length = 40, nullable = true, name = "telephoneNumber") (1) @Getter @Setter @Property(editing = Editing.ENABLED) @PropertyLayout(fieldSetId = "contact", sequence = "1.1") (2) private String telephoneNumber;
1 The JPA @Column
annotation indicates that the property is optional in the database; Causeway also understands this for the domain layer.2 This places the property in a new "contact" fieldset group; we’ll define that below -
and add the
emailAddress
property:PetOwner.java@Column(length = 40, nullable = true, name = "emailAddress") @Getter @Setter @Property(editing = Editing.ENABLED) @PropertyLayout(fieldSetId = "contact", sequence = "1.2") private String emailAddress;
And now let’s define the referenced "contact" fieldset.
-
Locate the associated
.layout.xml
file forPetOwner
. Before the "Details" fieldset, add:PetOwner.layout.xml<bs3:col span="12"> <cpt:fieldSet name="Contact" id="contact"/> </bs3:col>
if you make changes to this file then your IDE may be able detect the changes automatically. For example, with IntelliJ you can use | .
You can learn more about layout files here.
Ex 3.4: List new properties of PetOwner
Several of the actions in the "Pet Owners" menu return lists of PetOwner
s; the "list" action returns all of them.
When we do this we see only the name
property of the PetOwner
.
Let’s update the app to see some other properties we added in the previous exercise.
Solution
git checkout tags/v2/03-04-list-new-properties-of-PetOwner
mvn clean install
mvn -pl webapp spring-boot:run
Tasks
-
Locate the
PetOwner.columnOrder.txt
and make these changes:PetOwner.columnOrder.txtname telephoneNumber lastVisit emailAddress
Confirm that
now shows the additional properties as columns.if you make changes to this file then your IDE may be able detect the changes automatically. For example, with IntelliJ you can use | .
You can learn more about layout files here.
Ex 3.5: Initial Fixture Script
As we prototype with an in-memory database, it means that we need to setup the database each time we restart the application. Using the
menu to setup data saves some time, but it would nicer still if that script could be run automatically. We can do that by specifying a configuration property.We can also leverage Spring Boot profiles to keep this configuration separate.
Solution
git checkout tags/v2/03-05-initial-fixture-script
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
-
locate the
application.yml
configuration file (in the webapp module) and create this new file alongside it:application-dev.ymlcauseway: testing: fixtures: initial-script: domainapp.webapp.application.fixture.scenarios.DomainAppDemo
-
modify the startup of your application to enable this profile, using this system prpoerty:
-Dspring.profiles.active=dev
We also need to make one small modification to the fixture script itself. The initial fixtures are run with a built-in user called "\__system", that has no roles and therefore no permissions. We either need to change the fixture script to run as a different user that does have the permissions (there’s a service called SudoService that would let us do that), or, simpler, just temporarily switch off permission checks. We’ll go with the second option:
-
locate the
PetOwner_persona.Builder
class, and make this change:PetOwner_persona.java@RequiredArgsConstructor public enum PetOwner_persona /*...*/ { @Accessors(chain = true) public static class Builder extends BuilderScriptWithResult<PetOwner> { @Override protected PetOwner buildResult(final ExecutionContext ec) { val petOwner = petOwners.create(persona.name); (1) // ... } // ... } // ... }
1 Previously this was wrap(petOwners).create(…)
. Thewrap(…)
method uses the WrapperFactory to wrap the domain object in a proxy, and the proxy enforces all the business rules, including visibility.
When you now run the application you should now find that there are 10 PetOwner
objects already created.
Ex 3.6: Update Home Page to show PetOwners
Every Causeway app can nominate a home page, basically a view model that’s been annotated with @HomePage.
Currently the home page for our app is the one we inherited from the starter app, showing a list of SimpleObject
s.
In this exercise, we’ll refactor the home page view model to show a list of PetOwner
s instead.
Solution
git checkout tags/v2/03-06-update-home-page-to-show-pet-owners
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
Locate the HomePageViewModel
class:
-
inject
PetOwners
domain service (instead ofSimpleObjects
) -
change the
title()
implementation -
rename the collection from
getObjects()
togetPetOwners()
The positioning of the collection is also specified in the corresponding layout file, and so that file also needs updating.
-
locate the
HomePageViewModel.layout.xml
file, and update accordinglyHomePageViewModel.layout.xml<bs3:col span="6" unreferencedCollections="true"> <bs3:row> <bs3:col span="12"> <cpt:collection id="petOwners" defaultView="table"/> </bs3:col> </bs3:row> </bs3:col>
By default this will show all of the properties of PetOwner
.
We can change this by creating a file HomePageViewModel#petOwners.columnOrder.txt
, alongside the HomePageViewModel
.
name
telephoneNumber
emailAddress
#attachment
#lastVisit
#knownAs
#version
the action "Download .columnOrder.txt files (ZIP)" (available only when prototyping) provides an easy way to obtain this file; you can then update as required. |
Ex 3.7: Modify the menu action that creates PetOwners
If we want to create a new PetOwner
and specify additional details, at the moment it’s a two stage process: create the PetOwner
(using ), then set the additional details afterwards.
In this exercise we’ll simplify that workflow by allowing the additional details to optionally be specified during the create.
Solution
git checkout tags/v2/03-07-modifies-PetOwners-create-action
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
-
update
PetOwners#create()
method, to allow the additional details to optionally be specified:PetOwners.java@Action(semantics = SemanticsOf.NON_IDEMPOTENT) @ActionLayout(promptStyle = PromptStyle.DIALOG_SIDEBAR) public PetOwner create( @Name final String name, @Parameter(maxLength = 40, optionality = Optionality.OPTIONAL) final String knownAs, @Parameter(maxLength = 40, optionality = Optionality.OPTIONAL) final String telephoneNumber, @Parameter(maxLength = 40, optionality = Optionality.OPTIONAL) final String emailAddress) { final var petOwner = PetOwner.withName(name); petOwner.setKnownAs(knownAs); petOwner.setTelephoneNumber(telephoneNumber); petOwner.setEmailAddress(emailAddress); return repositoryService.persist(petOwner); }
-
also update
PetOwner_persona.Builder
fixture script, passing innull
for the new parameters:PetOwner_persona.javaval petOwner = PetOwners.create(persona.name, null, null, null);
(Or, even better - pass in
knownAs
as the 2nd parameter, and remove the explicit setting of this value later in the fixture script)
When you run the app, confirm that only the name
parameter is optional:
Ex 3.8: Prompt styles
The framework provides many ways to customise the UI, either through the layout files or using the @XxxLayout
annotations.
Default UI conventions can also be specified using the application.yml
configuration file.
In this exercise we’ll change the prompt style for both a service (menu) action, ie PetOwner#updateName
.
Solution
git checkout tags/v2/03-08-prompt-styles
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
-
Service (menu) actions are always shown in a dialog, of which there are two styles: modal prompt, or sidebar. If not specified explicitly, they will default to dialog modal.
Therefore, remove the
@ActionLayout(promptStyle)
forPetOwners#create
PetOwners.java@Action(semantics = SemanticsOf.NON_IDEMPOTENT) // @ActionLayout(promptStyle = PromptStyle.DIALOG_SIDEBAR) public PetOwner create( ... ) { ... }
Confirm that the dialog is now shown as a modal prompt.
-
Object actions can be shown either inline or in a dialog, but default to inline. If forced to use a dialog, then they default to a sidebar prompt rather than a modal prompt.
Remove the
@ActionLayout(promptStyle)
forPetOwner#updateName
:PetOwner.java@Action( ... ) @ActionLayout( ... // promptStyle = PromptStyle.INLINE, ... ) public PetOwner updateName( ... ) { ... }
Confirm that prompt is still inline.
-
Using a configuration property we can change the default for object actions to use a dialog rather than inline. We’ll use the "dev" profile introduced earlier:
application-dev.yamlcauseway: viewer: wicket: prompt-style: dialog
Remember to activate this new profile (
-Dspring.profiles.active=dev
) and confirm that theupdateName
prompt now uses a sidebar dialog. -
We can overide the default dialog style for both service and object actions using further configuration properties.
Switch the defaults so that service actions prefer to use a sidebar dialog, while object actions would use a modal dialog:
application-dev.yamlcauseway: viewer: wicket: prompt-style: dialog dialog-mode: modal dialog-mode-for-menu: sidebar
-
Optional: use
@ActionLayout(promptStyle=…)
to override these defaults.Be aware that "inline" makes no sense/is not supported for service actions.
-
Finish off the exercises by setting up these defaults to retain the original behaviour:
application-dev.yamlcauseway: viewer: wicket: prompt-style: inline #dialog-mode: modal # unused if prompt-style is inline dialog-mode-for-menu: sidebar
Ex 3.9: Derived 'days since last visit' property
Not every property has to persisted, nor editable (indeed most properties are not editable).
For example, it might be useful to calculate the number of days since the pet owner last visited; perhaps for marketing purposes.
In this exercise we’ll see how easy it is to create such a derived property.
Solution
git checkout tags/v2/03-09-derived-days-since-last-visit-property
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Tasks
Locate the PetOwner
class:
-
inject an instance of ClockService:
PetOwner.java@Inject @Transient (1) ClockService clockService;
1 instructs JPA that this field is not persisted Note that Apache Causeway allows services to be injected into entities (actually, into pretty much any domain object)
-
implement
getDaysSinceLastVisit()
method, calculating the number of days since "today".PetOwner.java@Property @PropertyLayout(fieldSetId = LayoutConstants.FieldSetId.DETAILS, sequence = "3.1") (1) public long getDaysSinceLastVisit() { return getLastVisit() != null ? ChronoUnit.DAYS.between(getLastVisit(), clockService.getClock().nowAsLocalDate()) : null; }
1 positioned just after the lastVisit
property -
update
PetOwner.columnOrder.txt
to indicate whether this new property should be rendered in standalone tables (returned from finder actions):PetOwner.columnOrder.txtname telephoneNumber emailAddress daysSinceLastVisit #lastVisit #knownAs #attachment #notes #version
-
similarly, update
HomePageViewModel#petOwners.columnOrder.txt
to indicate whether this new property should be rendered on the home page:HomePageViewModel#petOwners.columnOrder.txtname telephoneNumber emailAddress daysSinceLastVisit #lastVisit #knownAs #attachment #notes #version
Ex 3.10: Use meta-annotations to reduce duplication
There is some duplication between PetOwner
class: both define name
as a parameter or property respectively, and they also share a number of other parameters/properties, telephoneNumber
, emailAddress
and knownAs
.
With name
you might have noticed that the @Name
meta-annotation that came with the starter, and which centralizes the domain knowledge about what a name
is.
In this exercise we’ll use the same approach, introducing a meta-annotation to centralize semantics for telephoneNumber
.
(We won’t do emailAddress
or knownAs
though - we’ll explore an even more powerful way to reduce duplication in a following exercise).
Solution
git checkout tags/v2/03-10-use-meta-annotations-to-reduce-duplication
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Task
-
Create a
@PhoneNumber
meta-annotation, defined to be an editable property:PhoneNumber.java@Property( editing = Editing.ENABLED, maxLength = PhoneNumber.MAX_LEN, optionality = Optionality.OPTIONAL ) @Parameter( maxLength = PhoneNumber.MAX_LEN, optionality = Optionality.OPTIONAL ) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface PhoneNumber { int MAX_LEN = 40; }
-
update the
telephoneNumber
ofPetOwner
to use the meta-annotation:PetOwner.java@PhoneNumber (1) @Column(length = PhoneNumber.MAX_LEN, nullable = true, name = "telephoneNumber") (2) @Getter @Setter @PropertyLayout(fieldSetId = "contact", sequence = "1.1") (3) private String telephoneNumber;
1 updated to use meta-annotation 2 The JPA implementation used by Apache Causeway (EclipseLink) does not support meta-annotations, so the field must still be annotated with @Column
. We can at least use thePhoneNumber.MAX_LEN
for the length.3 Any annotations defined at the field level supplement or override those inherited from the meta-annotation. -
and update the
PetOwners#create()
action method also:PetOwners.java@Action(semantics = SemanticsOf.NON_IDEMPOTENT) // @ActionLayout(promptStyle = PromptStyle.DIALOG_SIDEBAR) public PetOwner create( @Name final String name, @Parameter(maxLength = 40, optionality = Optionality.OPTIONAL) final String knownAs, @PhoneNumber (1) final String telephoneNumber, @Parameter(maxLength = 40, optionality = Optionality.OPTIONAL) final String emailAddress) { // ... }
1 updated to use meta-annotation
Ex 3.11: Validation
At the moment there are no constraints for the format of telePhoneNumber
properties.
We can fix this by adding rules to their respective meta-annotations.
Solution
git checkout tags/v2/03-11-validation-rules-using-metaannotations
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Task
-
Update the
@Property
and@Parameter
annotations of the@PhoneNumber
meta-annotation:PhoneNumber.java@Property( editing = Editing.ENABLED, maxLength = PhoneNumber.MAX_LEN, optionality = Optionality.OPTIONAL, regexPattern = PhoneNumber.REGEX_PATTERN, (1) regexPatternReplacement = PhoneNumber.REGEX_PATTERN_REPLACEMENT (2) ) @Parameter( maxLength = PhoneNumber.MAX_LEN, optionality = Optionality.OPTIONAL, regexPattern = PhoneNumber.REGEX_PATTERN, (1) regexPatternReplacement = PhoneNumber.REGEX_PATTERN_REPLACEMENT (2) ) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface PhoneNumber { int MAX_LEN = 40; String REGEX_PATTERN = "[+]?[0-9 ]+"; String REGEX_PATTERN_REPLACEMENT = "Specify only numbers and spaces, optionally prefixed with '+'. " + "For example, '+353 1 555 1234', or '07123 456789'"; }
1 regex constraint 2 validation message if the constraint is not met
Try out the application and check that these rules are applied.
Ex 3.12: More validation
The updateName
action also has a validation rule, applied directly to the method:
public String validate0UpdateName(String newName) { (1)
for (char prohibitedCharacter : "&%$!".toCharArray()) {
if( newName.contains(""+prohibitedCharacter)) {
return "Character '" + prohibitedCharacter + "' is not allowed.";
}
}
return null;
}
1 | The validate…() supporting method is used to validate parameters; in this case the "0th" parameter of updateName .
More details on the validate supporting method can be found here. |
In this exercise we’ll move this constraint onto the @Name
meta-annotation instead, using a Specification.
Solution
git checkout tags/v2/03-12-moves-validation-onto-metaannotation
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Task
-
Update the
@Name
meta-annotation using a Specification:Name.java@Property(maxLength = Name.MAX_LEN, mustSatisfy = Name.Spec.class) (1) @Parameter(maxLength = Name.MAX_LEN, mustSatisfy = Name.Spec.class) (1) @ParameterLayout(named = "Name") @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface Name { int MAX_LEN = 40; String PROHIBITED_CHARACTERS = "&%$!"; class Spec extends AbstractSpecification<String> { (2) @Override public String satisfiesSafely(String candidate) { for (char prohibitedCharacter : PROHIBITED_CHARACTERS.toCharArray()) { if( candidate.contains(""+prohibitedCharacter)) { return "Character '" + prohibitedCharacter + "' is not allowed."; } } return null; } } }
1 indicates that the property or parameter value must satisfy the specification below 2 defines the specification definition, where a non-null value is the reason why the specification is not satisfied. -
Remove the
validate0UpdateName
method andPROHIBITED_CHARACTERS
constant fromPetOwner
. -
update the
@ActionLayout#describedAs
annotation for "updateName" to useName.PROHIBITED_CHARACTERS
Test the app once more.
in making this refactoring we actually fixed a bug: there was no validation of the parameter when a new PetOwner was created; but now there is.
|
Ex 3.13: Scalar custom value types
We could use meta-annotations for the "emailAddress" property and parameter, but instead we’ll reduce duplication using an even more powerful technique, namely custom value types.
We’ll define a custom class EmailAddress
with value semantics, allowing validation and any other behaviour to move onto the custom class itself.
Apache Causeway supports both scalar and composite value types. For email address, we’ll use a single string, so it’s a scalar value type.
Solution
git checkout tags/v2/03-13-scalar-custom-value-type-for-email-address
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Task
-
Define the
EmailAddress
value type:EmailAddress.java@javax.persistence.Embeddable (1) @org.apache.causeway.applib.annotation.Value (2) @lombok.EqualsAndHashCode (3) public class EmailAddress implements Serializable { (4) static final int MAX_LEN = 100; static final int TYPICAL_LEN = 30; static final Pattern REGEX = Pattern.compile("^[\\w-\\+]+(\\.[\\w]+)*@[\\w-]+(\\.[\\w]+)*(\\.[a-zA-Z]{2,})$"); public static EmailAddress of(String value) { (5) if (_Strings.isNullOrEmpty(value)) { return null; } if(!EmailAddress.REGEX.matcher(value).matches()) { throw new RuntimeException("Invalid email format"); } final var ea = new EmailAddress(); ea.value = value; return ea; } protected EmailAddress() {} (6) @Getter @Column( length = MAX_LEN, nullable = true, name = "emailAddress") (1) String value; (7) }
1 Required JPA annotations 2 Indicates to Causeway that this class is a value type (as opposed to an entity, view model or domain service) 3 Value types generally implement equals and hashCode 4 Value types generally are serializable 5 Validation moves to the factory method 6 no-arg constructor is required by JPA 7 The single data attribute to be persisted -
Implement a "value semantics provider". This tells Causeway how to interact with the value type
EmailAddressValueSemantics.java@Named(PetOwnerModule.NAMESPACE + ".EmailAddressValueSemantics") @Component (1) public class EmailAddressValueSemantics extends ValueSemanticsAbstract<EmailAddress> { (2) @Override public Class<EmailAddress> getCorrespondingClass() { return EmailAddress.class; } @Override public ValueType getSchemaValueType() { (3) return ValueType.STRING; } @Override public ValueDecomposition decompose(final EmailAddress value) { (4) return decomposeAsNullable(value, EmailAddress::getValue, ()->null); } @Override public EmailAddress compose(final ValueDecomposition decomposition) { (4) return composeFromNullable( decomposition, ValueWithTypeDto::getString, EmailAddress::of, ()->null); } @Override public DefaultsProvider<EmailAddress> getDefaultsProvider() { (5) return () -> null; } @Override public Renderer<EmailAddress> getRenderer() { (6) return (context, emailAddress) -> emailAddress == null ? null : emailAddress.getValue(); } @Override public Parser<EmailAddress> getParser() { (7) return new Parser<>() { @Override public String parseableTextRepresentation(Context context, EmailAddress emailAddress) { return renderTitle(emailAddress, EmailAddress::getValue); } @Override public EmailAddress parseTextRepresentation(Context context, String text) { return EmailAddress.of(text); } @Override public int typicalLength() { return EmailAddress.TYPICAL_LEN; } @Override public int maxLength() { return EmailAddress.MAX_LEN; } }; } @Override public IdStringifier<EmailAddress> getIdStringifier() { (8) return new IdStringifier.EntityAgnostic<>() { @Override public Class<EmailAddress> getCorrespondingClass() { return EmailAddressValueSemantics.this.getCorrespondingClass(); } @Override public String enstring(@NonNull EmailAddress value) { return _Strings.base64UrlEncode(value.getValue()); } @Override public EmailAddress destring(@NonNull String stringified) { return EmailAddress.of(_Strings.base64UrlDecode(stringified)); } }; } }
1 Defined as a Spring @Component
so that the framework can discover and use this value semantics provider2 Typically inherit from ValueSemanticsAbstract
, a convenience superclass3 The schemaValueType
in essence defines the widget that will be used to interact with the value4 The ValueDecomposition
is primarily used by the REST API (Restful Objects Viewer) to convert to/from JSON.5 The DefaultsProvider
provides an initial value, if any. For some values there is often a reasonable default, eg0
for a number, or[0,0]
for a coordinate, or today’s date.6 The Renderer
provides string and if required HTML representations of the value7 The Parser
converts string representations into the value. Note how this code delegates back to theEmailAddress
value type itself8 The IdStringifier
returns a string representation of the value, in case it is used as an identifier of the object. The returned string would appear in URLs or bookmarks, for example. -
update
PetOwner#emailAddress
to use theEmailAddress
value type:PetOwner.java@javax.persistence.Embedded (1) @Getter @Setter @Property(editing = Editing.ENABLED, optionality = Optionality.OPTIONAL) (2) @PropertyLayout(fieldSetId = "contact", sequence = "1.2") private EmailAddress emailAddress; (3)
1 required JPA annotation 2 need to explicitly indicate that this property is optional (previously it was inferred from @Column(nullable=)
)3 change the type from String
toEmailAddress
-
update
PetOwners#create
to use theEmailAddress
value type:PetOwner.java@Action(semantics = SemanticsOf.NON_IDEMPOTENT) // @ActionLayout(promptStyle = PromptStyle.DIALOG_SIDEBAR) public PetOwner create( @Name final String name, @Parameter(maxLength = 40, optionality = Optionality.OPTIONAL) final String knownAs, @PhoneNumber final String telephoneNumber, @Parameter(optionality = Optionality.OPTIONAL) final EmailAddress emailAddress) { (1) final var petOwner = PetOwner.withName(name); petOwner.setKnownAs(knownAs); petOwner.setTelephoneNumber(telephoneNumber); petOwner.setEmailAddress(emailAddress); return repositoryService.persist(petOwner); }
1 Change the parameter' type from String
toEmailAddress
Run the application and try to enter an invalid email address; the logic in the value type should prevent this.
Ex 3.14: Use layout xml file for UI semantics
At the moment the associated .layout.xml
file for PetOwner
is used to define rows, columns and fieldsets, while the @PropertyLayout annotation is grouped to associate properties with those fieldsets.
If we prefer, we can specify this association within the PetOwner.layout.xml
file instead.
And we can also do the same thing associating actions with properties or collections.
This has the benefit of being dynamic; we can move fields around in the layout without having to recompile/restart the application.
Solution
git checkout tags/v2/03-14-use-layout-xml-for-ui-semantics
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Task
Associate properties with fieldsets using .layout.xml
:
-
associate
name
property withidentity
fieldset:-
remove
@PropertyLayout
fromPetOwner
:PetOwner.java@Name @Column(length = Name.MAX_LEN, nullable = false, name = "name") @Getter @Setter @ToString.Include // @PropertyLayout(fieldSetId = LayoutConstants.FieldSetId.IDENTITY, sequence = "1") private String name;
-
add to
PetOwner.layout.xml
:PetOwner.layout.xml<cpt:fieldSet name="Identity" id="identity"> <cpt:property id="name"/> </cpt:fieldSet>
-
-
similarly for
knownAs
-
similarly for
telephoneNumber
-
similarly for
emailAddress
-
similarly for
notes
-
similarly for
lastVisit
-
similarly for
daysSinceLastVisit
-
similarly for
attachment
-
associate
version
property withmetadata
fieldset, and also explicitly specify the location of the two framework-provided properties that also reside in that fieldset:-
change
`PetOwner#version
property to::PetOwner.java@Version @Column(name = "version", nullable = false) //@PropertyLayout(fieldSetId = "metadata", sequence = "999") (1) @Property (2) @Getter @Setter private long version;
1 The @PropertyLayout
was removed…2 … but @Property
has been added instead.This is because at least one of @Property` or
@PropertyLayout
must be present to identify the field as a property (at least so far as this application has been configured. In fact, it is possible to configure the framework to be either less strict or more strict with respect to whether annotations are specified, see refguide:config:sections/causeway.core.meta-model.introspector.adoc#causeway.core.meta-model.introspector.policy]). -
add to
PetOwner.layout.xml
:PetOwner.layout.xml<cpt:fieldSet name="Metadata" id="metadata"> <cpt:property id="logicalTypeName"/> (1) <cpt:property id="objectIdentifier"/> (1) <cpt:property id="version"/> </cpt:fieldSet>
1 framework-provided properties. If we want the version
property to appear last in the fieldset, then we need to specify these other properties also.
-
-
associate the
updateName
action with thename
property-
remove from
PetOwner
:PetOwner.java@Action( ... ) @ActionLayout( // associateWith = "name", (1) ... ) public PetOwner updateName( @Name final String name) { ... }
1 deleted this line -
add to
PetOwner.layout.xml
:PetOwner.layout.xml<cpt:property id="name"> <cpt:action id="updateName"/> </cpt:property>
-
-
similarly
updateAttachment
action-
remove from
PetOwner
:PetOwner.java@Action( ... ) // @ActionLayout( (1) // associateWith = "attachment", (1) // position = ActionLayout.Position.PANEL (1) // ) (1) public PetOwner updateAttachment( ... ) { ... }
1 deleted lines -
add to
PetOwner.layout.xml
:PetOwner.layout.xml<cpt:property id="attachment"> <cpt:action id="updateAttachment" position="PANEL"/> </cpt:property>
-
remove from
PetOwner
:PetOwner.java@Action( ... ) @ActionLayout( // fieldSetId = LayoutConstants.FieldSetId.IDENTITY, (1) // position = ActionLayout.Position.PANEL, (1) describedAs = "Deletes this object from the persistent datastore" ) public void delete() { ... }
1 deleted lines
-
-
add to
PetOwner.layout.xml
:PetOwner.layout.xml<cpt:fieldSet name="Identity" id="identity"> <cpt:action id="delete" position="PANEL"/> (1) <cpt:property id="name"> <cpt:action id="updateName"/> </cpt:property> ... </cpt:fieldSet>
1 added, before any of the <property>
s
Whether you choose to use layout file only or a mixture of layout file and annotations is a matter of taste.
Notice that the .layout.xml
files has elements with the "unreferencedProperties", "unreferencedCollections" or "unreferencedActions" (and is considered invalid if these are missing).
As you might expect, these tags indicate where to render properties, collections or actions whose placement has not been specified explicitly.
This is an important principle of the naked objects pattern ; the domain object should always be rendered in some way or another.
The presence of UI semantics ( |
Ex 3.15: Update icon for Pet Owner
As we’ve learnt in previous exercises, every domain object has a title which allows the end-user to distinguish one object from another. Every domain object also has an icon, which helps distinguish one domain object type from another.
The icon acts as the hyperlink from one domain object to another, for example from the home page to the PetOwner
entity.
But choosing a good icon also improves the feedback cycle with your domain expert’s; similar to personas, it helps to create a connection between the domain experts concept of the "thing" and its representation in the app’s user interface.
In this exercise, we’ll replace the icon for PetOwner
.
Solution
git checkout tags/v2/03-15-change-pet-owner-icon-png
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"
Task
The icon for PetOwner
is defined in the PetOwner.png
.
All we need to do is replace it with some other icon.
-
Download a
.png
icon (it will be auto-scaled, but 32, 64 or 80 pixels is a good size).There are lots of good resources, for example https://icon8.com. Remember to provide appropriate accreditation if required.
the icon in the solution does indeed use a free icon from icons8.com, namely https://icons8.com/icons/set/person—icons8. |