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 Prototyping  Fixture Scripts 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 value
    2 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 it

    It’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 an knownAs for some of the owners:

    PetOwner_persona.java
    public 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 the name, 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 of PetOwner, 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 to lastVisit property

  • we still need to add in a telephoneNumber property, and an emailAddress 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.java
    final 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 for PetOwner. 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 Run  Debugging Actions  Reload Changed Classes.

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 PetOwners; 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.txt
    name
    telephoneNumber
    lastVisit
    emailAddress

Confirm that Pet Owners  List All 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 Run  Debugging Actions  Reload Changed Classes.

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 Prototyping  Fixture Scripts 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.yml
    causeway:
      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(…​). The wrap(…​) 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 SimpleObjects.

In this exercise, we’ll refactor the home page view model to show a list of PetOwners 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 of SimpleObjects)

  • change the title() implementation

  • rename the collection from getObjects() to getPetOwners()

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 accordingly

    HomePageViewModel.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.

home page default columns

We can change this by creating a file HomePageViewModel#petOwners.columnOrder.txt, alongside the HomePageViewModel.

HomePageViewModel#petOwners.columnOrder.txt
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 PetOwners  create), 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 in null for the new parameters:

    PetOwner_persona.java
    val 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:

new parameters are 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 PetOwners  create, and an object 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) for PetOwners#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) for PetOwner#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.yaml
    causeway:
      viewer:
        wicket:
          prompt-style: dialog

    Remember to activate this new profile (-Dspring.profiles.active=dev) and confirm that the updateName 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.yaml
    causeway:
      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.yaml
    causeway:
      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.txt
    name
    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.txt
    name
    telephoneNumber
    emailAddress
    daysSinceLastVisit
    #lastVisit
    #knownAs
    #attachment
    #notes
    #version

Ex 3.10: Use meta-annotations to reduce duplication

There is some duplication between PetOwners  create action and the 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 of PetOwner 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 the PhoneNumber.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:

PetOwner.java
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 and PROHIBITED_CHARACTERS constant from PetOwner.

  • update the @ActionLayout#describedAs annotation for "updateName" to use Name.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 provider
    2 Typically inherit from ValueSemanticsAbstract, a convenience superclass
    3 The schemaValueType in essence defines the widget that will be used to interact with the value
    4 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, eg 0 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 value
    7 The Parser converts string representations into the value. Note how this code delegates back to the EmailAddress value type itself
    8 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 the EmailAddress 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 to EmailAddress
  • update PetOwners#create to use the EmailAddress 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 to EmailAddress

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 with identity fieldset:

    • remove @PropertyLayout from PetOwner:

      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 with metadata 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 the name 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 (@XxxLayout annotations or the .layout.xml files) merely influence how that rendering is performed.

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.