Value types

Introduction

The state managed by entities and view models is expressed in terms of properties and collections, with properties whose value either refers to other domain objects (reference types) or alternatively holds a value directly, in other words value types. This latter is the topic of this page.

Apache Causeway supports a wide variety of value types, including JDK types (eg primitives, dates and String) Causeway-specific (eg AsciiDoc, Vega) and 3rd party (eg Joda Time). Moreover it is possible to define your own custom types. You can even teach the framework about other 3rd party libraries you might wish to use.

Programming Model

To use a value type is generally very straightforward:

// ...
@Entity
@DomainObject(nature = ENTITY)
public class Customer {

    @Property(editing = Editing.ENABLED)        (1)
    @Getter @Setter                             (2)
    private String firstName;                   (3)

    @Property(editing = Editing.ENABLED)
    @Getter @Setter
    private String lastName;

    @Property(editing = Editing.DISABLED)
    @Column(nullable = false)                   (4)
    @Getter                                     (5)
    private LocalDate dateOfBirth;

    @Property(editing = Editing.DISABLED)
    @Getter
    private Integer numberOfOrdersPlacedToDate;

    // ...
}
1 Depending on configuration, values are usually annotated with @Property, indicating whether or not they can be edited directly.
2 Lombok avoids boilerplate. A setter is required for editable property values.
3 Depending on context and type, value properties may require additional annotations.

In this case we have the JPA @Column annotation. A JAXB view model would require @XmlJavaTypeAdapter(JavaTimeJaxbAdapters.LocalDateToStringAdapter.class)

4 Not setter is required since this property is not ediable.

In the following sections we look at the other datatypes that you can use as the value types, and then we look at defining custom types.

JDK types

The most common value types to use are those built-into JDK. None of the following require additional annotations when used in entities or view models:

  • primitives

    int, long, short, byte, char, float, double, boolean

  • wrappers

    Integer, Long, Short, Byte, Character, Float, Double, Boolean

  • java.lang.String

  • java.math.BigInteger and java.math.BigDecimal

    for arbitrary accuracy. These can be combined with @Digits to specify scale.

  • java.util.UUID

These are also a couple of value types that have special properties when rendered:

  • java.net.URL

    Essentially a string, but will render as a hyperlink that the end-user can click

  • java.awt.image.BufferedImage

    In essence a byte array containing a .png or similar. Note though that properties if this type must be read-only.

    val bytes = _Bytes.of(_Resources.load(getClass(), "spring-boot-logo.png" ));
    return javax.imageio.ImageIO.read(new ByteArrayInputStream(bytes));

Dates

Dates are also supported of course, though there are lots of options:

  • in the java.time package; these are the most modern and generally is to be preferred:

    • local times (no time zone):

      java.time.LocalDate, java.time.LocalTime, java.time.LocalDateTime

    • for storing date/time, with respect to the UTC:

      java.time.OffsetDateTime, java.time.OffsetTime

    • primarily for displaying time in a specified time zone:

      java.time.ZonedDateTime

  • in the java.util package:

    java.util.Date

    The original date class, but unfortunately it is not immutable, and so is not a very good implementation of a value type.

  • in the java.sql package:

    java.sql.Date, java.sql.Timestamp.

    Unfortunately also not immutable. And java.sql.Date compounds the crimes by actually being a subclass of java.util.Date. Nevertheless, these are sometimes used for optimistic locking.

  • in org.joda.time package; a very popular 3rd party library that is still widely. The JDK java.time library took substantial inspiration from Joda.

    • local date/times (no timezone)

      org.joda.time.LocalDate, org.joda.time.LocalDateTime, org.joda.time.LocalTime

    • exact

      org.joda.time.DateTime (similar to java.time.ZonedDateTime)

Depending on context, additional entities may be required on the property’s field:

  • When used in entities, some of these classes may require @Column to be specified (it never hurts to do so anyway).

  • When used in JAXB view models, they all will require @XmlJavaTypeAdapter to be specified. This tells JAXB how to serialize the value in and out of XML. Apache Causeway provides adapters for all of these.

In addition to supporting JDK and Joda, Apache Causeway defines a number of its own custom value types, described next.

Causeway-specific

Apache Causeway defines a number of its own value types.

In the org.apache.causeway.applib.value we have:

  • Blob

    binary large object, suitable for capturing images, Word documents, Excel spreadsheets, PDFs and so on.

    If this is used to store a PDF, then the @PdfJsViewer (from the PDF.js extension) will cause the Wicket viewer to render it as a PDF.

  • Clob

    Character large object, suitable for text, RFT, base 64 encoded data and similar.

  • Markup

    Intended to holds HTML markup. The Wicket viewer will render this more or less verbatim.

    Take care to sanitize inputs!
  • LocalResourcePath

    Resolves to a resource path local to the webapp. The primary use case for this value type is not as a property, but instead as a return type for an action. In such cases it will cause the web browser to redirect to the resource.

    There are several such built-in resources that can be useful in a development/prototyping context:

    • /restful/ - the REST API

    • /swagger-ui/index.thtml - the Swagger UI

    • /db/ - the H2 database console

    You could of course also define additional resources for your own requirements.

The above value types are part of the core framework. There are also several value types that are packaged as extensions in the Value Types Catalog:

  • AsciiDoc, provided by the asciidoc value type extension

    This renders Asciidoctor content as HTML.

  • Markdown, provided by the markdown value type extension

    This renders Markdown content (as defined by the CommonMark spec) as HTML.

  • Vega, provided by the vega value type extension

    This renders graphics defined by the Vega-Lite grammar.

  • Joda Time, provided by the jodatime value type extension

    This provides support for four value types defined within the Joda Time library.

Custom value types

As well as the built-in support and extensions provided by Apache Causeway, it is also possible to implement your own custom value types.

Implementing value types can be a great way of encapsulating functionality. Rather than have your entities and view models be concerned about the format of an invoice number, instead define an InvoiceNumber. Similarly, instead of littering your entities and view models with the same logic to ensure that a startDate <= endDate, instead define a DateInterval value type.

Scalar value types

By way of example, let’s define an EmailAddress value type.

The value type itself is pretty easy:

EmailAddress.java
@org.apache.causeway.applib.annotation.Value        (1)
@lombok.Value                                       (2)
@lombok.AllArgsConstructor(staticName = "of")       (3)
public class EmailAddress {
    String emailAddress;                            (4)
}
1 Defines this as a value type to the framework
2 Uses lombok to define getters, a hashCode(), equals(), toString().
3 Uses lombok to a factory method (makes the constructor private).
4 The single data attribute

And it can be used in an entity or a view model just like a built-in value type:

// ..
@DomainObject(nature=Nature.ENTITY)
public class Customer {

    @Property(editing = Editing.ENABLED)
    @Getter @Setter
    private EmailAddress emailAddress;
    // ...
}

However, we need some glue to "teach" the framework how to render with the value type. This is done using an implementation of the ValueSemanticsProvider SPI:

EmailAddressValueSemantics.java
@Named("demo.EmailAddressValueSemantics")
@Component
public class EmailAddressValueSemantics
        extends ValueSemanticsAbstract<EmailAddress> {

    @Override
    public Class<EmailAddress> getCorrespondingClass() {
        return EmailAddress.class;
    }

    @Override
    public ValueType getSchemaValueType() {
        return ValueType.STRING;                                            (1)
    }

    @Override
    public ValueDecomposition decompose(final EmailAddress value) {         (2)
        return decomposeAsNullable(value, EmailAddress::getEmailAddress, ()->null);
    }

    @Override
    public EmailAddress compose(final ValueDecomposition decomposition) {   (3)
        return composeFromNullable(
                decomposition, ValueWithTypeDto::getString, EmailAddress::of, ()->null);
    }

    @Override
    public DefaultsProvider<EmailAddress> getDefaultsProvider() {           (4)
        return new DefaultsProvider<EmailAddress>() {
            @Override
            public EmailAddress getDefaultValue() {
                return EmailAddress.of("");
            }
        };
    }

    @Override
    public Renderer<EmailAddress> getRenderer() {                           (5)
        return new Renderer<>() {
            @Override
            public String titlePresentation(Context context, EmailAddress emailAddress) {
                return emailAddress == null ? null : emailAddress.getEmailAddress();
            }
        };
    }

    @Override
    public Parser<EmailAddress> getParser() {                               (6)
        return new Parser<>() {
            // https://stackoverflow.com/a/47181151
            final Pattern REGEX = Pattern.compile("^[\\w-\\+]+(\\.[\\w]+)*@[\\w-]+(\\.[\\w]+)*(\\.[a-zA-Z]{2,})$");

            @Override
            public String parseableTextRepresentation(Context context, EmailAddress value) {
                return renderTitle(value, EmailAddress::getEmailAddress);
            }

            @Override
            public EmailAddress parseTextRepresentation(Context context, String text) {
                if(!REGEX.matcher(text).matches()) {
                    throw new RuntimeException("Invalid email format");
                }
                if (_Strings.isEmpty(text)) return null;
                return EmailAddress.of(text);
            }

            @Override
            public int typicalLength() {
                return 20;
            }

            @Override
            public int maxLength() {
                return 50;
            }
        };
    }

    @Override
    public IdStringifier<EmailAddress> getIdStringifier() {                 (7)
        return new IdStringifier.EntityAgnostic<>() {
            @Override
            public Class<EmailAddress> getCorrespondingClass() {
                return EmailAddressValueSemantics.this.getCorrespondingClass();
            }

            @Override
            public String enstring(@NonNull EmailAddress value) {
                return _Strings.base64UrlEncode(value.getEmailAddress());
            }

            @Override
            public EmailAddress destring(@NonNull String stringified) {
                return EmailAddress.of(_Strings.base64UrlDecode(stringified));
            }
        };
    }
}
1 determines the UI widget that the framework uses to display/edit the value
2 the compose() and decompose() methods are used to serialize the object using the structures defined by the XSD schemas.

Using this, the framework can render the composite value as JSON (as used by the REST API), or to XML, as used by SPIs such as CommandSubscriber (see Command and CommandDto).

3 the getDefaultsProvider() provides an initial value (eg non-nullable properties)
4 the getRenderer() is used to render the value as a string. An HTML representation can also be provided, though this type doesn’t warrant one.
5 the getParser() is used to convert the string (entered in the UI) into the value type. If the value entered is invalid, then an exception can be thrown.
6 the getIdStringifier() allows the value type to be used as (part of) an identifier of the object. The string returned must be URL safe.

As we can see, this is not the simplest of APIs, but the simplification it brings to your entities and view models that can now consume your new value type means that it may be worth the effort.

We’re not quite finished with the glue code, unfortunately. Chances are that you will want to persist the new value to the database, which means that the ORM also requires its own SPI to be implemented (but they are almost identical).

  • if using JPA, then implement the javax.persistence.AttributeConverter SPI:

    EmailAddressConverter.java
    @Converter(autoApply = true)
    public class EmailAddressConverter implements AttributeConverter<EmailAddress, String>{
    
        @Override
        public String convertToDatabaseColumn(final EmailAddress memberValue) {
            return memberValue != null
                    ? memberValue.getEmailAddress()
                    : null;
        }
    
        @Override
        public EmailAddress convertToEntityAttribute(final String datastoreValue) {
            return datastoreValue != null
                    ? EmailAddress.of(datastoreValue)
                    : null;
        }
    }
  • if using JDO, then implement the org.datanucleus.store.types.converters.TypeConverter SPI:

    public class EmailAddressConverter implements TypeConverter<EmailAddress, String>{
    
        private static final long serialVersionUID = 1L;
    
        @Override
        public String toDatastoreType(final EmailAddress memberValue) {
            return memberValue != null
                    ? memberValue.getEmailAddress()
                    : null;
        }
    
        @Override
        public EmailAddress toMemberType(final String datastoreValue) {
            return datastoreValue != null
                    ? EmailAddress.of(datastoreValue)
                    : null;
        }
    }

Composite value types

A composite value type consists of several simple values. By way of example, let’s consider a DateInterval, with a startDate and an endDate, and where we want to enforce that startDate <= endDate at all times.

DateInterval.java
@org.apache.causeway.applib.annotation.Value
@lombok.Value
@lombok.AllArgsConstructor(staticName = "of")
public class DateInterval {

    LocalDate startDate ;                                       (1)
    LocalDate endDate;                                          (1)

    public boolean overlaps(DateInterval other) {               (2)
        return toJoda().overlap(other.toJoda());
    }
    public DateInterval gap(DateInterval other) {               (2)
        return fromJoda(toJoda().gap(other.toJoda()));
    }
    private Interval toJoda() {                                 (3)
        return new Interval(startDate, endDate);
    }
    private static DateInterval fromJoda(Interval interval) {   (3)
        return interval == null
                ? null
                : DateInterval.of(
                    interval.getStart().toLocalDate(),
                    interval.getEnd().toLocalDate());
    }
}
1 The internal fields
2 It’s common for value types to have a set of methods that act upon them (sometimes called an "algebra").
3 Internally we leverage Joda to do the heavy lifting.

The value type can be used in entities and view models the same as any other value type. For example:

// ..
@DomainObject(nature=Nature.ENTITY)
public class CarRental {

    @Property(editing = Editing.ENABLED)
    @Getter @Setter
    private DateInterval dateInterval;
    // ...
}

As with scalar custom types, we need some glue to "teach" the framework how to render with the value type, though it works slightly differently; rather than parsing text input to set the value, instead we provide a special mixin that the framework uses to prompt for the constituent values. The name of this mixin is always called "default".

For our DateInterval example:

DateInterval_default.java
@Action(semantics = SemanticsOf.SAFE)
@ActionLayout(promptStyle = PromptStyle.INLINE_AS_IF_EDIT)          (1)
@RequiredArgsConstructor
public class DateInterval_default {                                 (2)

    private final DateInterval mixee;

    @MemberSupport public DateInterval act(
            final LocalDate startDate,
            final LocalDate endDate
    ) {
        return DateInterval.of(startDate, endDate);
    }
    @MemberSupport public LocalDate defaultStartDate() {
        return mixee.getStartDate();
    }
    @MemberSupport public LocalDate defaultEndDate() {
        return mixee.getEndDate();
    }
    @MemberSupport public LocalDate validateAct(                    (3)
            final LocalDate startDate,
            final LocalDate endDate) {
        return startDate.isBefore(endDate)
                    ? null
                    : "Start date must be before the end date";
    }
}
1 The "default" action must use this prompt style
2 Must be named "default"
3 Enforces validation constraints

In addition, we also need an implementation of the ValueSemanticsProvider SPI:

DateIntervalValueSemantics.java
@Named("demo.DateIntervalValueSemantics")
@Component
@Import({
        DateInterval_default.class                                          (1)
})
@RequiredArgsConstructor(onConstructor_ = { @Inject })
public class DateIntervalValueSemantics
        extends ValueSemanticsAbstract<DateInterval> {

    final ClockService clockService;

    @Override
    public Class<DateInterval> getCorrespondingClass() {
        return DateInterval.class;
    }

    @Override
    public ValueType getSchemaValueType() {                                 (2)
        return ValueType.COMPOSITE;
    }

    @Override
    public ValueDecomposition decompose(final DateInterval value) {         (3)
        return CommonDtoUtils.typedTupleBuilder(value)
                .addFundamentalType(ValueType.LOCAL_DATE, "startDate", DateInterval::getStartDate)
                .addFundamentalType(ValueType.LOCAL_DATE, "endDate", DateInterval::getEndDate)
                .buildAsDecomposition();
    }

    @Override
    public DateInterval compose(final ValueDecomposition decomposition) {   (3)
        return decomposition.right()
                .map(CommonDtoUtils::typedTupleAsMap)
                .map(map-> DateInterval.of(
                        (LocalDate)map.get("startDate"),
                        (LocalDate)map.get("endDate")))
                .orElse(null);
    }

    @Override
    public DefaultsProvider<DateInterval> getDefaultsProvider() {           (4)
        val nowAsMilli = clockService.getClock().now().toEpochMilli();
        val now = new org.joda.time.DateTime(nowAsMilli).toLocalDate();
        return ()-> DateInterval.of(now, now.plusDays(7));
    }

    @Override
    public Renderer<DateInterval> getRenderer() {                           (5)
        return new Renderer<>() {
            @Override
            public String titlePresentation(Context context, DateInterval object) {
                if (object == null) return "(none)";
                return "[" + object.getStartDate() + ", " + object.getEndDate() + "]";
            }
        };
    }
}
1 Declares the existence of the "default" mixin.
2 Indicates this is a composite, and therefore that the value should be manipulated in the UI by way of the "default" mixin’s prompt
3 the compose() and decompose() methods are used to serialize the object using the structures defined by the XSD schemas.

Using this, the framework can render the composite value as JSON (as used by the REST API), or to XML, as used by SPIs such as CommandSubscriber (see Command and CommandDto).

4 the getDefaultsProvider() provides an initial value (eg non-nullable properties)
5 the getRenderer() is used to render the value as a string. An HTML representation can also be provided, though this type doesn’t warrant one.

Compared to scalar types, note that the ValueSemanticsProvider does not need to provide an implementation of getParser() - instead the "default" mixin does this work. Also, it is not possible to use a custom value type as part of the object’s id, and so no implementation of getIdStringifier() is required either.

If using within the database then you will also need to map the custom type to the database: