<
Fork me on GitHub

1. Annotations

Apache Isis uses annotations to provide additional metadata about the domain objects.

To give just a few examples:

  • if a property is read-only, then this can be annotated with @Property(editing=EditingDISABLED).

  • if a class has a small fixed set of instances (eg a picklist), then it can be annotated using @DomainObject(bounded=true)

  • if a class is a domain service and should be automatically instantiated as a singleton, then it can be annotated using @DomainService

  • if an action is idempotent, then it can be annotated using @Action(semantics=SemanticsOf.IDEMPOTENT).

  • if an action parameter is optional, it can be annotated using @Parameter(optionality=Optionality.OPTIONAL)

Some annotations act as UI hints, for example:

  • if a collection should be rendered "open" rather than collapsed, it can be annotated using @CollectionLayout(render=RenderType.EAGERLY)

  • if an action has a tooltip, it can be annotated using @ActionLayout(describedAs=…​)

  • if a domain object is bookmarkable, it can be annotated using @DomainObjectLayout(bookmarking=BookmarkPolicy.AS_ROOT).

This chapter documents all the annotations currently supported by Isis. It also identifies a number of annotations that are now deprecated, and indicates their replacement.

1.1. Summary

This section summarizes the various annotations supported by Isis. They break out into five categories.

1.1.1. Core annotations

In Apache Isis every domain object is either a domain entity, a view model or a domain service. And each of these are made up of properties, collections and actions (domain services only have actions).

For each of these domain types and members there are two annotations. One covers the semantics intrinsic to the domain (eg whether an action parameter is optional or not), then other (suffix …​Layout) captures semantics relating to the UI/presentation layer.

Most UI semantics can also be specified using dynamic object layout.

The table below summarizes these most commonly used annotations in Isis.

Table 1. Core annotations for domain objects, services and members (as of 1.8.0)
Annotation Purpose Layer Dynamic layout?

@Action

Domain semantics for actions

Domain

@ActionLayout

User interface hints for actions

UI

Yes

@Collection

Domain semantics for collections

Domain

@CollectionLayout

User interface hints for collections

UI

Yes

@DomainObject

Domain semantics for domain object (entities and optionally view models, see also @ViewModel)

Domain

@DomainObjectLayout

User interface hints for domain object (entities and optionally view models, see also @ViewModelLayout)

UI

Yes

@DomainService

Class is a domain service (rather than an entity or view model)

Domain

@DomainServiceLayout

User interface hints for domain services

UI

@Parameter

Domain semantics for action parameters

Domain

@ParameterLayout

Layout hints for an action parameter (currently: its label position either to top or the left).

UI

Yes

@Property

Domain semantics for properties

Domain

@PropertyLayout

Layout hints for a property

UI

Yes

@ViewModel

Specify that a class is a view model (as opposed to an entity or domain service); equivalent to @DomainObject(nature=VIEW_MODEL).

Domain, Persistence

@ViewModelLayout

User interface hints for view models. For use with @ViewModel. If specifying view models using @DomainObject(nature=VIEW_MODEL) then use @DomainObjectLayout)

UI

Yes

1.1.2. Other Isis Annotations

These annotations are also commonly used, but relate not to objects or object members but instead to other aspects of the Isis metamodel.

Table 2. Other Isis Annotations
Annotation Purpose Layer Dynamic layout?

@Facets

Install arbitrary facets within the Isis metamodel.

(any)

@HomePage

Query-only action (on domain service) to be invoked, result of which is rendered as the user’s home page.

UI

@MemberGroupLayout

Grouping of properties into groups, and organizing of properties, collections into columns.

UI

Yes

@MemberOrder

Ordering of properties, collections and actions, and also associating actions with either a property or a collection.

UI

Yes

@MinLength

Minimum number of characters required for an auto-complete search argument.

UI

@Programmatic

Ignore a public method, excluded from the Isis metamodel.

Domain

1.1.3. JDO Annotations

Apache Isis uses JDO/DataNucleus as its ORM, and infers some of its own metadata from the JDO annotations.

Isis (currently) builds up metadata by parsing the JDO annotations from source, not by querying the JDO metamodel. The upshot is that, for the annotations documented here at least, your domain entities must use JDO annotations rather than XML.

Furthermore, note that although JDO (the property-related) annotations to be placed on either the field or on the getter, Isis requires that annotations are placed on the getter.

The table below lists the JDO annotations currently recognized by Isis.

Table 3. JDO Annotations
Annotation Purpose Layer Applies to

@javax.jdo.annotations.
Column

Used to determine whether a property is mandatory or optional. For String and BigDecimal properties, used to determine length/precision/scale.

Domain / persistence

Property

@javax.jdo.annotations.
Discriminator

Override for the object type, as used in `Bookmark`s, URLs for RestfulObjects viewer and elsewhere.

Note that the discriminator overrides the object type that may otherwise be inferred from the @PersistenceCapable annotation.

Domain / persistence

Class

@javax.jdo.annotations.
NotPersistent

Used to determine whether to enforce or skip some metamodel validation for @Column versus equivalent Isis annotations.

Domain / persistence

Property

@javax.jdo.annotations.
PersistenceCapable

Used to build Isis' own internal identifier for objects.

If the schema() attribute is specified (and if @Discriminator _hasn’t been specified), is also used to derive the object type, as used in `Bookmark`s, URLs for RestfulObjects viewer and elsewhere.

Domain / persistence

Class

@javax.jdo.annotations.
PrimaryKey

Used to ensure Isis does not overwrite application-defined primary keys, and to ensure is read-only in the UI.

Domain / persistence

Property

Isis also parses the following JDO annotations, but the metadata is currently unused.

Table 4. JDO Annotations (unused within Isis)
Annotation Purpose Layer Applies to

@javax.jdo.annotations.
DataStoreIdentity

Unused

Persistence

Class

@javax.jdo.annotations.
EmbeddedOnly

Unused

Persistence

Class

@javax.jdo.annotations.
Query

Unused

Persistence

Class

1.1.4. Java EE Annotations

While Apache Isis does, as of today, define a good number of its own annotations, the policy is to reuse standard Java/JEE annotations wherever they exist or are added to the Java platform.

The table below lists the JEE annotations currently recognized. Expect to see more added in future releases of Isis.

Table 5. Java EE Annotations
Annotation Purpose Layer Dynamic layout?

@javax.validation.
constraints.
Digits

Precision/scale for BigDecimal values.

Domain

@javax.inject.
Inject

Inject domain service into a domain object (entity or view model) or another domain service.

Domain

@javax.annotations.
PostConstruct

Callback for domain services (either singleton or request-scoped) to initialize themselves once instantiated.

Domain

@javax.annotations.
PreDestroy

Callback for domain services (either singleton or request-scoped) to clean up resources prior to destruction.

Domain

@javax.enterprise.
context.
RequestScoped

Specify that a domain service has request-scope (rather than a singleton).

Domain

1.1.5. Deprecated Annotations

As Apache Isis has evolved and grown, we found ourselves adding more and more annotations; but most of these related to either an object type (entity, view model, service) or an object member (property, collection, action). Over time it became harder and harder for end programmers to discover these new features.

Accordingly, (in v1.8.0) we decided to unify the semantics into the main (core) annotations listed above.

The annotations listed in the table below are still supported by Apache Isis, but will be retired in Isis v2.0.

Table 6. Deprecated Annotations
Annotation Purpose Use instead Layer Dynamic layout?

@ActionOrder

Order of buttons and menu items representing actions.

UI

Yes

@ActionInteraction

Enable subscribers on the Event Bus Service to either veto, validate or take further steps before/after an action has been invoked.

Domain

@ActionSemantics

Query-only, idempotent or non-idempotent.

Domain

@Audited

Audit changes to an object.

Domain

@AutoComplete

Repository method to search for entities

UI/Domain

@Bookmarkable

Whether (and how) to create a bookmark for visited object.

UI

@Bounded

Bounded (and limited) number of instances of an entity type, translates into a drop-down for any property of that type.

Domain

@Bulk

Indicates an action is a bulk action, can be applied to multiple instances.

UI, Domain

@CollectionInteraction

Enable subscribers on the Event Bus Service to either veto, validate or take further steps before/after a collection has been added to or removed from.

Domain

@Command

Action invocation should be reified as a command object, optionally persistable for profiling and enhanced auditing, and background/async support.

Domain

@CssClass

Allow visual representation of individual objects or object members layout to be customized by application-specific CSS.

UI

Yes

@CssClassFa

So that font awesome icons can be applied to action buttons/menu items and optionally as an object icon.

cssClassFa() attribute for: @ActionLayout, DomainObjectLayout and ViewModelLayout

UI

Yes

@Debug

Action only invokable in debug mode.

Not supported by either the Wicket viewer or the RestfulObjects viewer; use prototype mode instead (@Action#restrictTo())

UI

@DescribedAs

Provide a longer description/tool-tip of an object or object member.

UI

Yes

@Disabled

Object property cannot be edited, an object collection cannot be added to/removed from, or an object action cannot be invoked.

#editing() attribute for @Property, @Collection and @DomainObject

UI, Domain

Yes

@Exploration

Action available in special 'exploration' mode.

Not supported by either the Wicket viewer or the RestfulObjects viewer; use prototype mode instead (@Action#restrictTo())

UI

@FieldOrder

Order of properties and collections.

UI

Yes

@Hidden

Object member is not visible, or on domain service (to indicate that none of its actions are visible).

For domain object members, use #hidden() attribute of Action, Property or Collection.
For domain service, use @DomainService(
nature=DOMAIN)

UI, Domain

Yes

@Idempotent

Whether an action is idempotent (can be invoked multiple times with same post-condition).

Domain

@Ignore

Exclude this method from the metamodel.

@Ignore was deprecated because it can easily clash with @org.junit.Ignore.

Domain

@Immutable

An object’s state cannot be changed (properties cannot be edited, collections cannot be added to or removed from). Actions can still be invoked.

Domain

@Mask

How to parse/render values (never properly supported)

(None)

UI/domain

@MaxLength

Maximum length of a property value (strings).

#maxLength() attribute for @Property or @Parameter

Domain

@MemberGroups

Layout of properties and collections of a domain object or view model object.

UI

Yes

@MultiLine

Render string property over multiple lines (a textarea rather than a textbox).

#multiLine() attribute for @Property or @Parameter

UI

Yes

@MustSatisfy

Specify arbitrary specification constraints on a property or action parameter.

#mustSatisfy() attribute for @Property or @Parameter

Domain

@Named

Override name inferred from class. Required for parameter names (prior to Java8).

UI

Yes

@NotContributed

Indicates that a domain service action is not rendered as an action on the (entity) types of its parameters. For 1-arg query-only actions, controls whether the domain service action is rendered as a property or collection on the entity type of its parameter.

Use @DomainService#nature() to specify whether any of the actions in a domain service should appear in the menu bars (applies at type level, not action level). For individual actions, use @ActionLayout#
contributedAs()
to specify whether any individual action should be contributed only as an action or as an association (property or collection).

UI

@NotInServiceMenu

Indicates that a domain service should not be rendered in the application menu (at top of page in Wicket viewer).

@DomainService#nature() to signify that none of the actions in a domain service should appear in the menu bars

UI

@NotPersisted

Indicates that an object property is not persisted (meaning it is excluded from view model mementos, and should not be audited).

#notPersisted() attribute of @Property and @Collection

Domain, Persistence

@ObjectType

For constructing the external identifier (URI) of an entity instance (part of its URL in both Wicket viewer and Restful Objects viewer). Also part of the toString representation of bookmarks, if using the Bookmark Service

Domain

@Optional

Specifies that a property or action parameter is not mandatory.

#optionality() attribute for @Property or @Parameter

Domain

@Paged

Number of instances to display in tables representing (standalone or parented) collections.

#paged() attribute for @DomainObjectLayout or @CollectionLayout

UI

Yes

@Plural

For the irregular plural form of an entity type.

UI

@PostsAction
InvokedEvent

Post a domain event to the Event Bus Service indicating that an action has been invoked.

Domain

@PostsCollection
AddedToEvent

Post a domain event to the Event Bus Service indicating that an element has been added to a collection.

Domain

@PostsCollection
RemovedFromEvent

Post a domain event to the Event Bus Service indicating that an element has been removed from a collection.

Domain

@PostsProperty
ChangedEvent

Post a domain event to the Event Bus Service indicating that the value of a property has changed.

Domain

@PropertyInteraction

Enable subscribers on the Event Bus Service to either veto, validate or take further steps before/after a property has been modified or cleared.

Domain

@Prototype

Indicates that an action should only be visible in 'prototype' mode.

UI

Yes

@PublishedAction

Action invocation should be serialized and published by configured PublishingService (if any), eg to other systems.

Domain

@PublishedObject

Change to object should be serialized and published by configured PublishingService (if any), eg to other systems.

Domain

@QueryOnly

Whether an action is query-only (has no side-effects).

Domain

@RegEx

Validate change to value of string property.

#regexPattern() for @Property or @Parameter.

Domain

@Render

Eagerly (or lazily) render the contents of a collection.

UI

Yes

@RenderedAsDayBefore

Render dates as the day before; ie store [a,b) internally but render [a,b-1]) to end-user.

#renderedAsDayBefore() attribute for @PropertyLayout and @ParameterLayout.

UI

@Resolve

Eagerly (or lazily) render the contents of a collection (same as @Render)

UI

Yes

@SortedBy

Display instances in collections in the order determined by the provided Comparator.

UI

Yes

@TypeOf

The type of entity stored within a collection, or as the result of invoking an action, if cannot be otherwise inferred, eg from generics.

#typeOf() attribute for @Collection and @Action

Domain

@TypicalLength

The typical length of a string property, eg to determine a sensible length for a textbox.

#typicalLength() attribute for @PropertyLayout and @ParameterLayout

UI

Yes

1.1.6. Incomplete/partial support

These annotations have only incomplete/partial support, primarily relating to the management of value types. We recommend that you do not use them for now. Future versions of Isis may either formally deprecate/retire them, or we may go the other way and properly support them. This will depend in part on the interactions between the Isis runtime, its two viewer implementations, and DataNucleus persistence.

Table 7. Annotations with incomplete/partial support
Annotation Purpose Layer

@Aggregated

Indicates that the object is aggregated, or wholly owned, by a root object.

This information could in theory provide useful semantics for some object store implementations, eg to store the aggregated objects "inline".

Currently neither the JDO ObjectStore nor any of the viewers exploit this metadata.

Domain, Persistence

@Defaulted

Indicates that a (value) class has a default value.

The concept of "defaulted" means being able to provide a default value for the type by way of the o.a.i.applib.adapters.DefaultsProvider interface. Generally this only applies to value types, where the @Value annotation implies encodability through the ValueSemanticsProvider interface.

For these reasons the @Defaulted annotation is generally never applied directly, but can be thought of as a placeholder for future enhancements whereby non-value types might also have a default value provided for them.

Domain

@Encodable

Indicates that a (value) class can be serialized/encoded.

Encodability means the ability to convert an object to-and-from a string, by way of the o.a.i.applib.adapters.EncoderDecoder interface. Generally this only applies to value types, where the @Value annotation implies encodability through the ValueSemanticsProvider interface.

For these reasons the @Encodable annotation is generally never applied directly, but can be thought of as a placeholder for future enhancements whereby non-value types might also be directly encoded.

Currently neither the Wicket viewer nor the RO viewer use this API. The Wicket viewer uses Wicket APIs, while RO viewer has its own mechanisms (parsing data from input JSON representations, etc.)

Persistence

@NotPersistable

Indicates that a domain object may not be programmatically persisted.

+ This annotation indicates that transient instances of this class may be created but may not be persisted. The framework will not provide the user with an option to 'save' the object, and attempting to persist such an object programmatically would be an error.

For example:

@NotPersistable(By.USER)
public class InputForm {
    ...
}

By default the annotated object is effectively transient (ie default to By.USER_OR_PROGRAM).

This annotation is not supported by: Wicket viewer (which does not support transient objects). See also ISIS-743 contemplating the removal of this annotation.

Domain, Persistence

@Parseable

Indicates that a (value) class can be reconstructed from a string.

Parseability means being able to parse a string representation into an object by way of the o.a.i.applib.adapters.Parser interface. Generally this only applies to value types, where the @Value annotation implies encodability through the ValueSemanticsProvider interface.

For these reasons the @Parser annotation is generally never applied directly, but can be thought of as a placeholder for future enhancements whereby non-value types might also have be able to be parsed.

Note that the Wicket viewer uses Apache Wicket’s Converter API instead.

UI, Domain

@Value

Specify that a class has value-semantics.

The @Value annotation indicates that a class should be treated as a value type rather than as a reference (or entity) type. It does this providing an implementation of a o.a.i.applib.adapters.ValueSemanticsProvider.

For example:

@Value(semanticsProviderClass=
    ComplexNumberValueSemanticsProvider.class)
public class ComplexNumber {
    ...
}

The ValueSemanticsProvider allows the framework to interact with the value, parsing strings and displaying as text, and encoding/decoding (for serialization).

Domain

1.2. @Action

The @Action annotation groups together all domain-specific metadata for an invokable action on a domain object or domain service.

The table below summarizes the annotation’s attributes.

Table 8. @Action attributes
Attribute Values (default) Description

command()

AS_CONFIGURED, ENABLED, DISABLED
(AS_CONFIGURED)

whether the action invocation should be reified into a o.a.i.applib.
services.command.Command object through the CommandContext service.

commandExecuteIn()

FOREGROUND,BACKGROUND
(FOREGROUND)

whether to execute the command immediately, or to persist it (assuming that an appropriate implementation of CommandService has been configured) such that a background scheduler can execute the command asynchronously

commandPersistence()

PERSISTED, NOT_PERSISTED, IF_HINTED
(PERSISTED)

whether the reified Command (as provided by the CommandContext domain service) should actually be persisted (assuming an appropriate implementation of CommandService has been configured).

domainEvent()

subtype of ActionDomainEvent
(ActionDomainEvent.Default)

the event type to be posted to the EventBusService to broadcast the action’s business rule checking (hide, disable, validate) and its invocation (pre-execute and post-execution).

hidden()

EVERYWHERE, NOWHERE
(NOWHERE)

indicates where (in the UI) the action should be hidden from the user.

invokeOn()

OBJECT_ONLY, COLLECTION_ONLY, OBJECT_AND_COLLECTION
(OBJECT_ONLY)

whether an action can be invoked on a single object and/or on many objects in a collection.

Currently this is only supported for no-arg actions.

publishing()

AS_CONFIGURED, ENABLED, DISABLED
(AS_CONFIGURED)

whether the action invocation should be published to the registered PublishingService.

publishing-
PayloadFactory()

subtype of PublishingPayloadFactory- ForAction (none)

specifies that a custom implementation of PublishingPayloadFactoryForAction be used to create the (payload of the) published event representing the action invocation

restrictTo()

NO_RESTRICTIONS,PROTOTYPING
(NO_RESTRICTIONS)

whether the action is only available in prototyping mode, or whether it is available also in production mode.

semantics()

QUERY, IDEMPOTENT, NON_IDEMPOTENT
(NON_IDEMPOTENT)

the action’s semantics (ie whether objects are modified as the result of invoking this action, and if so whether reinvoking the action would result in no further change)

typeOf()

(none)

if the action returns a collection, hints as to the run-time type of the objects within that collection (as a fallback)

For example:

public class ToDoItem {
    public static class CompletedEvent extends ActionDomainEvent<ToDoItem> {
        private static final long serialVersionUID = 1L;
        public CompletedEvent(
                final ToDoItem source,
                final Identifier identifier,
                final Object... arguments) {
            super("completed", source, identifier, arguments);
        }
    }
    @Action(
        command=CommandReification.ENABLED,
        commandExecuteIn=CommandExecuteIn.FOREGROUND,          (1)
        commandPersistence=CommandPersistence.NOT_PERSISTED,   (2)
        domainEvent=CompletedEvent.class,
        hidden = Where.NOWHERE,                                (3)
        invokeOn = InvokeOn.OBJECT_ONLY,                       (4)
        publishing = Publishing.ENABLED,
        semantics = SemanticsOf.IDEMPOTENT
    )
    public ToDoItem completed() { ... }
}
1 default value, so could be omitted
2 default value, so could be omitted
3 default value, so could be omitted
4 default value, so could be omitted

1.2.1. command()

The @Action(command=…​) attribute (and the related @Action(commandPersistence=…​) and @Action(commandExecuteIn=…​) attributes) allows an action invocation to be made into a concrete object such that it can be inspected and persisted. The primary use case for this is enhanced profiling/auditing, and it also supports the deferring the execution of the action such that it can be invoked in the background.

The annotation works with (and is influenced by the behaviour of) a number of domain services:

Each action invocation is reified by the CommandContext service into a Command object, capturing details of the target object, the action, the parameter arguments, the user, a timestamp and so on.

If an appropriate CommandService is configured (for example using (non-ASF) Isis addons' command module), then the Command itself is persisted.

By default, actions are invoked in directly in the thread of the invocation. If there is an implementation of BackgroundCommandService (as the Isis addons' command module does provide), then this means in turn that the BackgroundService can be used by the domain object code to programmatically create background Commands.

If background Commands are used, then an external scheduler, using headless access, must also be configured.

The command() attribute determines whether the action invocation should be reified into a Command object (by the CommandContext service).

The default is AS_CONFIGURED, meaning that the configuration property isis.services.command.actions is used to determine the whether the action is reified:

  • all

    all actions are reified

  • ignoreSafe (or ignoreQueryOnly)

    actions with safe (read-only) semantics are ignored, but actions which may modify data are not ignored

  • none

    no actions are reified.

If there is no configuration property in isis.properties then all actions are reified into Commands.

Note: Command reification does not necessarily imply that Command objects will be persisted; that depends on whether there is a CommandService configured that will persist said Commands.

This default can be overridden on an action-by-action basis; if command() is set to ENABLED then the action is reified irrespective of the configured value; if set to DISABLED then the action is NOT reified irrespective of the configured value.

For example:

public class Order {
    @Action(command=CommandReification.ENABLED)
    public Invoice generateInvoice(...) { ... }
}

corresponds to the behaviour described above; the Command object is persisted (assuming an appropriate CommandService is defined, and executed immediately in the foreground).

commandPersistence()

If the action has been reified, then the commandPersistence() attribute determines whether that Command object should then also be persisted (the default), or not persisted, or only if hinted.

To explain this last alternative:

public class Order {
    @Action(
        command=CommandReification.ENABLED,
        commandPersistence=CommandPersistence.IF_HINTED
    )
    public Invoice generateInvoice(...) { ... }
}

will suppress the persistence of the Command object unless a child background Command has been created in the body of the action by way of the BackgroundService.

On the other hand:

public class Order {
    @Action(
        command=CommandReification.ENABLED,
        commandExecuteIn=CommandExecuteIn.FOREGROUND,
        commandPersistence=CommandPersistence.NOT_PERSISTED
    )
    public Invoice generateInvoice(...) { ... }
}

will prevent the parent Command object from being persisted, even if a child background Command is created.

commandExecuteIn()

For persisted commands, the commandExecuteIn() attribute determines whether the Command should be executed in the foreground (the default) or executed in the background.

Background execution means that the command is not executed immediately, but is available for a configured BackgroundCommandService to execute, eg by way of an in-memory scheduler such as Quartz. See here for further information on this topic.

For example:

public class Order {
    @Action(
        command=CommandReification.ENABLED,
        commandExecuteIn=CommandExecuteIn.BACKGROUND)
    public Invoice generateInvoice(...) { ... }
}

will result in the Command being persisted but its execution deferred to a background execution mechanism. The returned object from this action is the persisted Command itself.

1.2.2. domainEvent()

Whenever a domain object (or list of domain objects) is to be rendered, the framework fires off multiple domain events for every property, collection and action of the domain object. In the cases of the domain object’s actions, the events that are fired are:

  • hide phase: to check that the action is visible (has not been hidden)

  • disable phase: to check that the action is usable (has not been disabled)

  • validate phase: to check that the action’s arguments are valid

  • pre-execute phase: before the invocation of the action

  • post-execute: after the invocation of the action

Subscribers subscribe through the EventBusService using either Guava or Axon Framework annotations and can influence each of these phases.

By default the event raised is ActionDomainEvent.Default. For example:

public class ToDoItem {
    @Action()
    public ToDoItem completed() { ... }
    ...
}

The purpose of the domainEvent() attribute is to allows a custom subclass to be emitted instead. This attribute is also supported for collections and properties.

For example:

public class ToDoItem {
    public static class CompletedEvent extends ActionDomainEvent<ToDoItem> { (1)
        private static final long serialVersionUID = 1L;
        public CompletedEvent(                                               (2)
                final ToDoItem source,                                       (3)
                final Identifier identifier,                                 (4)
                final Object... arguments) {                                 (5)
            super("completed", source, identifier, arguments);
        }
    }
    @Action(domainEvent=CompletedEvent.class)
    public ToDoItem completed() { ... }
}
1 inherit from ActionDomainEvent<T> where T is the type of the domain object being interacted with
2 constructor called reflectively by the framework
3 populated with the object being interacted with (the source of the event)
4 identifier of the collection
5 action argument(s)

The benefit is that subscribers can be more targetted as to the events that they subscribe to.

Subscribers

Subscribers (which must be domain services) subscribe using either the Guava API or (if the EventBusService has been appropriately configured) using the Axon Framework API. The examples below use the Guava API.

Subscribers can be either coarse-grained (if they subscribe to the top-level event type):

@DomainService
public class SomeSubscriber {
    @Programmatic
    @com.google.common.eventbus.Subscribe
    public void on(ActionInteractionEvent ev) {
        ...
    }
}

or can be fine-grained (by subscribing to specific event subtypes):

@DomainService
public class SomeSubscriber {
    @Programmatic
    @com.google.common.eventbus.Subscribe
    public void on(ToDoItem.CompletedEvent ev) {
        ...
    }
}

If the AxonFramework is being used, replace @com.google.common.eventbus.Subscribe with @org.axonframework.eventhandling.annotation.EventHandler.

The subscriber’s method is called (up to) 5 times:

  • whether to veto visibility (hide)

  • whether to veto usability (disable)

  • whether to veto execution (validate)

  • steps to perform prior to the action being invoked.

  • steps to perform after the action has been invoked.

The subscriber can distinguish these by calling ev.getEventPhase(). Thus the general form is:

@Programmatic
@com.google.common.eventbus.Subscribe
public void on(ActionDomainEvent ev) {
    switch(ev.getPhase()) {
        case HIDE:
            // call ev.hide() or ev.veto("") to hide the action
            break;
        case DISABLE:
            // call ev.disable("...") or ev.veto("...") to disable the action
            break;
        case VALIDATE:
            // call ev.invalidate("...") or ev.veto("...")
            // if action arguments are invalid
            break;
        case EXECUTING:
            break;
        case EXECUTED:
            break;
    }
}

It is also possible to abort the transaction during the executing or executed phases by throwing an exception. If the exception is a subtype of RecoverableException then the exception will be rendered as a user-friendly warning (eg Growl/toast) rather than an error.

Raising events programmatically

Normally events are only raised for interactions through the UI. However, events can be raised programmatically either by calling the EventBusService API directly, or by emulating the UI by wrapping the target object using the WrapperFactory domain service.

1.2.3. hidden()

Actions can be hidden at the domain-level, indicating that they are not visible to the end-user. This attribute can also be applied to properties and collections.

It is also possible to use @ActionLayout#hidden() or dynamic layouts such that the action can be hidden at the view layer. Both options are provided with a view that in the future the view-layer semantics may be under the control of (expert) users, whereas domain-layer semantics should never be overridden or modified by the user.

For example:

public class Customer {
    @Action(hidden=Where.EVERYWHERE)
    public void updateStatus() { ... }
    ...
}

The acceptable values for the where parameter are:

  • Where.EVERYWHERE or Where.ANYWHERE

    The action should be hidden at all times.

  • Where.NOWHERE

    The action should not be hidden.

The other values of the Where enum have no meaning for a collection.

For actions of domain services the visibility is dependent upon its @DomainService#nature() and also on whether it is contributed (as per @ActionLayout#contributedAs()).

1.2.4. invokeOn()

The invokeOn() attribute indicates whether the an action can be invoked on a single object (the default) and/or on many objects in a collection.

For example:

public class ToDoItem {
    @Action(invokeOn=InvokeOn.OBJECT_AND_COLLECTION)
    public void markAsCompleted() {
        setCompleted(true);
    }
    ...
}

Actions to be invoked on collection (currently) have a number of constraints. It:

  • must take no arguments

  • cannot be hidden (any annotations or supporting methods to that effect will be ignored)

  • cannot be disabled (any annotations or supporting methods to that effect will be ignored).

The example given above is probably ok, because setCompleted() is most likely idempotent. However, if the action also called some other method, then we should add a guard.

For example, for this non-idempotent action:

@Action(invokeOn=InvokeOn.OBJECT_AND_COLLECTION)
public void markAsCompleted() {
    setCompleted(true);
    todoTotalizer.incrementNumberCompleted();
}

we should instead write it as:

@Action(invokeOn=InvokeOn.OBJECT_AND_COLLECTION)
public void markAsCompleted() {
    if(isCompleted()) {
        return;
    }
    setCompleted(true);
    todoTotalizer.incrementNumberCompleted();
}

This attribute has no meaning if annotated on an action of a domain service.

1.2.5. publishing()

The publishing() attribute determines whether and how an action invocation is published via the registered implementation of a PublishingService). This attribute is also supported for domain objects, where it controls whether changed objects are published as events.

A common use case is to notify external "downstream" systems of changes in the state of the Isis application. The default value for the attribute is AS_CONFIGURED, meaning that the configuration property isis.services.publish.actions is used to determine the whether the action is published:

  • all

    all actions are published

  • ignoreSafe (or ignoreQueryOnly)

    actions with safe (read-only) semantics are ignored, but actions which may modify data are not ignored

  • none

    no actions are published

If there is no configuration property in isis.properties then publishing is automatically enabled.

This default can be overridden on an action-by-action basis; if publishing() is set to ENABLED then the action invocation is published irrespective of the configured value; if set to DISABLED then the action invocation is not published, again irrespective of the configured value.

For example:

public class Order {
    @Action(publishing=Publishing.ENABLED)        (1)
    public Invoice generateInvoice(...) { ... }
}
1 because set to enabled, will be published irrespective of the configured value.
publishingPayloadFactory()

The (optional) related publishingPayloadFactory() specifies the class to use to create the (payload of the) event to be published by the publishing factory.

Rather than simply broadcast that the action was invoked, the payload factory allows a "fatter" payload to be instantiated that can eagerly push commonly-required information to all subscribers. For at least some subscribers this should avoid the necessity to query back for additional information.

1.2.6. restrictTo()

By default actions are available irrespective of the deployment mode. The restrictTo() attribute specifies whether the action should instead be restricted to only available in prototyping mode.

For example:

public class Customer {
    public Order placeNewOrder() { ... }
    public List<Order> listRecentOrders() { ... }

    @Action(restrictTo=RestrictTo.PROTOTYPING)
    public List<Order> listAllOrders() { ... }
    ...
}

In this case the listing of all orders (in the listAllOrders() action) probably doesn’t make sense for production; there could be thousands or millions. However, it would be useful to disaply how for a test or demo system where there are only a handful of orders.

1.2.7. semantics()

The semantics() attribute describes whether the invocation is safe (as no side-effects), is idempotent (may have side-effects but always has the same post-conditions), or is neither safe nor idempotent. If the annotation is missing then the framework assumes non-idempotent.

The attribute was originally introduced for the RestfulObjects viewer in order that action invocations could be made available using either HTTP GET, PUT or POST (respectively).

Since then its use has been extended into the core runtime. For example, it is now aso used for the in-built concurrency checking; the invocation of a safe action does not perform a concurrency check, whereas non-safe actions do perform a concurrency check.

For example:

public class Customer {
    @Action(semantics=SemanticsOf.SAFE)
    public CreditRating checkCredit() { ... }

    @Action(semantics=SemanticsOf.IDEMPOTENT)
    public void changeOfAddress(Address address) { ... }

    @Action(semantics=SemanticsOf.NON_IDEMPOTENT)
    public Order placeNewOrder() { ... }
    ...
}

1.2.8. typeOf()

The typeOf() attribute specifies the expected type of an element returned by the action (returning a collection), when for whatever reason the type cannot be inferred from the generic type, or to provide a hint about the actual run-time (as opposed to compile-time) type. This attribute can also be specified for collections.

For example:

public void AccountService {
    @Action(typeOf=Customer.class)
    public List errantAccounts() {
        return customers.allNewCustomers();
    }
    ...

    @Inject
    CustomerRepository customers;
}

In general we recommend that you use generics instead, eg List<Customer>.

1.3. @ActionLayout

The @ActionLayout annotation applies to actions, collecting together all UI hints within a single annotation.

The table below summarizes the annotation’s attributes.

Table 9. @ActionLayout attributes
Attribute Values (default) Description

bookmarking()

AS_ROOT, NEVER
(NEVER)

indicates if an action (with safe action semantics) is automatically bookmarked.

contributedAs()

AS_BOTH, AS_ACTION, AS_ASSOCIATION, AS_NEITHER
(AS_BOTH)

for a domain service action that can be contributed, whether to contribute as an action or as an association (ie a property or collection).

For a domain service action to be contributed, the domain services must have a nature nature of either VIEW or VIEW_CONTRIBUTIONS_ONLY, and the action must have safe action semantics, and takes a single argument, namely the contributee domain object.

cssClass()

Any string valid as a CSS class

an additional CSS class around the HTML that represents for the action, to allow targetted styling in application.css.

Supported by the Wicket viewer but currently ignored by the RestfulObjects viewer.

cssClassFa()

Any valid Font awesome icon name

specify a font awesome icon for the action’s menu link or icon.

cssClassFaPosition()

LEFT, RIGHT
(LEFT)

Positioning of the icon on the button/menu item.

describedAs()

String.

provides a short description of the action, eg for rendering as a 'tool tip'.

hidden()

EVERYWHERE, NOWHERE
(NOWHERE)

indicates where (in the UI) the action should be hidden from the user.

named()

String.

to override the name inferred from the action’s name in code.

A typical use case is if the desired name is a reserved Java keyword, such as default or package.

position()

BELOW, RIGHT, PANEL, PANEL_DROPDOWN (BELOW)

for actions associated (using @MemberOrder#named()) with properties, the positioning of the action’s button with respect to the property

For example:

public class ToDoItems {
    @Action(semantics=SemanticsOf.SAFE)             (1)
    @ActionLayout(
        bookmarking=BookmarkPolicy.AS_ROOT,
        cssClass="x-key",
        cssClassFa="fa-checkbox",
        describedAs="Mark the todo item as not complete after all",
        hidden=Where.NOWHERE                        (2)
    )
    @MemberOrder(sequence = "1")
    public List<ToDoItem> notYetComplete() {
        ...
    }
}
1 required for bookmarkable actions
2 default value, so could be omitted

As an alternative to using the @ActionLayout annotation, a dynamic layout using .layout.json file can be specified; for example:

"notYetComplete": {
    "actionLayout": {
        "bookmarking": "AS_ROOT",
        "cssClass": "x-key",
        "cssClassFa": "fa-checkbox",
        "describedAs": "Mark the todo item as not complete after all",
        "hidden": "NOWHERE"
    }
}

1.3.1. bookmarking()

The bookmarking() attribute indicates if an action (with safe action semantics) is automatically bookmarked. This attribute is also supported for domain objects.

In the Wicket viewer, a link to a bookmarked object is shown in the bookmarks panel:

bookmarking

Note that this screenshot shows an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

The Wicket viewer supports alt-[ as a shortcut for opening the bookmark panel. Esc will close.

For example:

public class ToDoItems {
    @Action(semantics=SemanticsOf.SAFE)
    @ActionLayout(bookmarking=BookmarkPolicy.AS_ROOT)
    @MemberOrder(sequence = "1")
    public List<ToDoItem> notYetComplete() {
        ...
    }
}

indicates that the notYetComplete() action is bookmarkable.

The enum value AS_CHILD has no meaning for actions; it relates only to bookmarked domain objects.

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"notYetComplete": {
    "actionLayout": { "bookmarking": "AS_ROOT" }
}

1.3.2. contributedAs()

For a domain service action that can be contributed, the contributedAs() attribute determines how it is contributed: as an action or as an association (ie a property or collection).

The distinction between property or collection is automatic: if the action returns a java.util.Collection (or subtype) then the action is contributed as a collection; otherwise it is contributed as a property.

For a domain service action to be contributed, the domain services must have a nature nature of either VIEW or VIEW_CONTRIBUTIONS_ONLY, and the action must have safe action semantics, and takes a single argument, namely the contributee domain object.

For example:

@DomainService(nature=NatureOfService.VIEW_CONTRIBUTIONS_ONLY)
public class CustomerContributions {
    @Action(semantics=SemanticsOf.SAFE)
    @ActionLayout(contributedAs=Contributed.AS_ASSOCIATION)
    public List<Order> mostRecentOrders(Customer customer) { ... }
    ...
}

The @ActionLayout is not required if the action does not have safe semantics, or if the action takes more than one argument; in these cases the action can only be contributed as an action.

It’s also possible to use the attribute to suppress the action completely:

@DomainService(nature=NatureOfService.VIEW)
public class OrderContributions {
    @ActionLayout(contributedAs=Contributed.AS_NEITHER)
    public void cancel(Order order);
    ...
}

In such cases, though, it would probably make more sense to annotate the action as either hidden or indeed @Programmatic.

Unlike other @ActionLayout attributes, this attribute cannot be specified dynamically in the .layout.json dynamic layout file because it relates to the contributor domain service, not the contributee domain object.

1.3.3. cssClass()

The cssClass() attribute can be used to render additional CSS classes in the HTML (a wrapping <div>) that represents the action. Application-specific CSS can then be used to target and adjust the UI representation of that particular element.

This attribute can also be applied to domain objects, view models, properties, collections and parameters.

For example:

public class ToDoItem {
    @ActionLayout(cssClass="x-key")
    public ToDoItem postpone(LocalDate until) { ... }
    ...
}

The similar @ActionLayout#cssClassFa() annotation attribute is also used as a hint to apply CSS, specifically to add Font Awesome icons on action menu items or buttons.

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"postpone": {
    "actionLayout": { "cssClass": "x-key" }
}

1.3.4. cssClassFa()

The cssClassFa() attribute is used to specify the name of a Font Awesome icon name, to be rendered on the action’s representation as a button or menu item. The related cssClassFaPosition() attribute specifies the positioning of the icon, to the left or the right of the text.

These attributes can also be applied to domain objects and to view models to specify the object’s icon.

For example:

public class ToDoItem {
    @ActionLayout(
        cssClassFa="fa-step-backward"
    )
    public ToDoItem previous() { ... }

    @ActionLayout(
        cssClassFa="fa-step-forward",
        cssClassFaPosition=ActionLayout.CssClassFaPosition.RIGHT
    )
    public ToDoItem next() { ... }
}

There can be multiple "fa-" classes, eg to mirror or rotate the icon. There is no need to include the mandatory fa "marker" CSS class; it will be automatically added to the list. The fa- prefix can also be omitted from the class names; it will be prepended to each if required.

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"previous": {
    "actionLayout": {
        "cssClassFa": "fa-step-backward",
        "cssClassFaPosition": "LEFT"
    }
},
"next": {
    "actionLayout": {
        "cssClassFa": "fa-step-forward",
        "cssClassFaPosition": "RIGHT"
    }
}

The similar @ActionLayout#cssClass() annotation attribute is also used as a hint to apply CSS, but for wrapping the representation of an object or object member so that it can be styled in an application-specific way.

1.3.5. describedAs()

The describedAs() attribute is used to provide a short description of the action to the user. In the Wicket viewer it is displayed as a 'tool tip'.

This attribute can also be specified for collections, properties, parameters, domain objects and view models.

For example:

public class Customer {
    @ActionLayout(describedAs="Place a repeat order of the last (most recently placed) order")
    public Order placeRepeatOrder(...) { ... }
}

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"postpone": {
    "actionLayout": { "describedAs": "Place a repeat order of the last (most recently placed) order" }
}

1.3.6. hidden()

The hidden() attribute indicates where (in the UI) the action should be hidden from the user. This attribute can also be applied to properties and collections.

It is also possible to use @Action#hidden() to hide an action at the domain layer. Both options are provided with a view that in the future the view-layer semantics may be under the control of (expert) users, whereas domain-layer semantics should never be overridden or modified by the user.

For example:

public class Customer {
    @ActionLayout(hidden=Where.EVERYWHERE)
    public void updateStatus() { ... }
    ...
}

The acceptable values for the where parameter are:

  • Where.EVERYWHERE or Where.ANYWHERE

    The action should be hidden at all times.

  • Where.NOWHERE

    The action should not be hidden.

The other values of the Where enum have no meaning for a collection.

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"updateStatus": {
    "actionLayout": { "hidden": "EVERYWHERE" }
}

For actions of domain services the visibility is dependent upon its @DomainService#nature() and also on whether it is contributed (as per @ActionLayout#contributedAs()).

1.3.7. named()

The named() attribute explicitly specifies the action’s name, overriding the name that would normally be inferred from the Java source code. This attribute can also be specified for collections, properties, parameters, domain objects, view models and domain services.

Following the don’t repeat yourself principle, we recommend that you only use this attribute when the desired name cannot be used in Java source code. Examples of that include a name that would be a reserved Java keyword (eg "package"), or a name that has punctuation, eg apostrophes.

For example:

public class Customer {
    @ActionLayout(named="Get credit rating")
    public CreditRating obtainCreditRating() { ... }
}

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"obtainCreditRating": {
    "actionLayout": { "named": "Get credit rating" }
}

The framework also provides a separate, powerful mechanism for internationalization.

1.3.8. position()

The position() attribute pertains only to actions that have been associated with properties using @MemberOrder#named(). For these actions, it specifies the positioning of the action’s button with respect to the field representing the object property.

The attribute can take one of four values: BELOW, RIGHT, PANEL or PANEL_DROPDOWN.

For example:

public class Customer {

    @Property(
        editing=Editing.DISABLED                 (1)
    )
    public CustomerStatus getStatus() { ... }
    public void setStatus(CustomerStatus customerStatus) { ... }

    @MemberOrder(
        named="status",                          (2)
        sequence="1"
    )
    @ActionLayout(
        named="Update",                          (3)
        position=Position.BELOW
    )
    public CreditRating updateStatus(Customer ) { ... }
}
1 indicate the property as read-only, such that it can only be updated using an action
2 associate the "updateStatus" action with the "status" property
3 give the action an abbreviated name, because the fact that the "status" property is to be updated is implied by its positioning

The default is BELOW, which is rendered (by the Wicket viewer) as shown below:

position BELOW

If the action is positioned as RIGHT, then the action’s button is rendered to the right of the property’s field, in a compact drop-down. This is ideal if there are many actions associated with a property:

position RIGHT

If the action is positioned as PANEL, then the action’s button is rendered on the header of the panel that contains the property:

position PANEL

And finally, if the action is positioned as PANEL_DROPDOWN, then the action’s button is again rendered on the panel header, but as a drop-down:

position PANEL DROPDOWN

If there are multiple actions associated with a single property then the positioning can be mix’ed-and-match’ed as required. If the PANEL or PANEL_DROPDOWN are used, then (as the screenshots above show) the actions from potentially multiple properties grouped by that panel will be shown together.

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"obtainCreditRating": {
    "actionLayout": { "named": "Get credit rating" }
}

The fact that the layout is dynamic (does not require a rebuild/restart) is particularly useful in that the look-n-feel can be easily experimented with and adjusted.

1.4. @Collection

The @Collection annotation applies to collections collecting together all domain semantics within a single annotation.

The table below summarizes the annotation’s attributes.

Table 10. @Collection attributes
Attribute Values (default) Description

domainEvent()

subtype of CollectionDomainEvent
(CollectionDomainEvent.Default)

the event type to be posted to the EventBusService to broadcast the collection’s business rule checking (hide, disable, validate) and its modification (before and after).

editing()

ENABLED, DISABLED, AS_CONFIGURED
(AS_CONFIGURED)

whether a collection can be added to or removed from within the UI

editingDisabledReason()

String value

if editing() is DISABLED, provides a reason as to why.

hidden()

EVERYWHERE, OBJECT_FORMS, NOWHERE
(NOWHERE)

indicates where (in the UI) the collection should be hidden from the user.

notPersisted()

true, false
(false)

whether to exclude from snapshots.

Collection must also be annotated with @javax.jdo.annotations.NotPersistent in order to not be persisted.

typeOf()

hints as to the run-time type of the objects within that collection (as a fallback)

For example:

public class ToDoItem {
    public static class DependenciesChangedEvent
            extends CollectionDomainEvent<ToDoItem, ToDoItem> {
        private static final long serialVersionUID = 1L;
        public DependenciesChangedEvent(
                ToDoItem source,
                Identifier identifier,
                Of of,
                ToDoItem value) {
            super(source, identifier, of, value);
        }
    }
    @Collection(
        domainEvent=DependenciesChangedEvent.class,
        editing = Editing.ENABLED,
        hidden = Where.NOWHERE,                       (1)
        notPersisted = false,                         (2)
        typeOf = ToDoItem.class                       (3)
    )
    public SortedSet<ToDoItem> getDependencies() { ... }
    ...
}
1 default value, so could be omitted
2 default value, so could be omitted
3 default value, so could be omitted

1.4.1. domainEvent()

Whenever a domain object (or list of domain objects) is to be rendered, the framework fires off multiple domain events for every property, collection and action of the domain object. In the cases of the domain object’s collections, the events that are fired are:

  • hide phase: to check that the collection is visible (has not been hidden)

  • disable phase: to check that the collection is usable (has not been disabled)

  • validate phase: to check that the collection’s arguments are valid (to add or remove an element)

  • pre-execute phase: before the modification of the collection

  • post-execute: after the modification of the collection

Subscribers subscribe through the EventBusService using either Guava or Axon Framework annotations and can influence each of these phases.

The Wicket viewer does _not currently support the modification of collections; they are rendered read-only. However, domain events are still relevant to determine if such collections should be hidden.

The workaround is to create add/remove actions and use UI hints to render them close to the collection.

By default the event raised is CollectionDomainEvent.Default. For example:

public class ToDoItem {
    @Collection()
    public SortedSet<ToDoItem> getDependencies() { ... }
    ...
}

The purpose of the domainEvent() attribute is to allows a custom subclass to be emitted instead. This attribute is also supported for actions and properties.

For example:

public class ToDoItem {
    public static class DependenciesChangedEvent
            extends CollectionDomainEvent<ToDoItem, ToDoItem> { (1)
        private static final long serialVersionUID = 1L;
        public DependenciesChangedEvent(                        (2)
                ToDoItem source,                                (3)
                Identifier identifier,                          (4)
                Of of,                                          (5)
                ToDoItem value) {                               (6)
            super(source, identifier, of, value);
        }
    }
    @Collection(
        domainEvent=DependenciesChangedEvent.class
    )
    public SortedSet<ToDoItem> getDependencies() { ... }
    ...
}
1 inherit from CollectionDomainEvent<T,E> where T is the type of the domain object being interacted with, and E is the type of the element in the collection (both ToDoItem in this example)
2 constructor called reflectively by the framework
3 populated with the object being interacted with (the source of the event)
4 identifier of the collection
5 indicates if the collection is being accessed, added to or removed from.
6 the element being added or removed from the collection

The benefit is that subscribers can be more targetted as to the events that they subscribe to.

Subscribers

Subscribers (which must be domain services) subscribe using either the Guava API or (if the EventBusService has been appropriately configured) using the Axon Framework API. The examples below use the Guava API.

Subscribers can be either coarse-grained (if they subscribe to the top-level event type):

@DomainService
public class SomeSubscriber {
    @Programmatic
    @com.google.common.eventbus.Subscribe
    public void on(CollectionInteractionEvent ev) {
        ...
    }
}

or can be fine-grained (by subscribing to specific event subtypes):

@DomainService
public class SomeSubscriber {
    @Programmatic
    @com.google.common.eventbus.Subscribe
    public void on(ToDoItem.DependenciesChangedEvent ev) {
        ...
    }
}

If the AxonFramework is being used, replace @com.google.common.eventbus.Subscribe with @org.axonframework.eventhandling.annotation.EventHandler.

The subscriber’s method is called (up to) 5 times:

  • whether to veto visibility (hide)

  • whether to veto usability (disable)

  • whether to veto execution (validate) the element being added to/removed from the collection

  • steps to perform prior to the collection being added to/removed from

  • steps to perform after the collection has been added to/removed from.

The subscriber can distinguish these by calling ev.getPhase(). Thus the general form is:

@Programmatic
@com.google.common.eventbus.Subscribe
public void on(CollectionDomainEvent ev) {
    switch(ev.getPhase()) {
        case HIDE:
            // call ev.hide() or ev.veto("") to hide the collection
            break;
        case DISABLE:
            // call ev.disable("...") or ev.veto("...") to disable the collection
            break;
        case VALIDATE:
            // call ev.invalidate("...") or ev.veto("...")
            // if object being added/removed to collection is invalid
            break;
        case EXECUTING:
            break;
        case EXECUTED:
            break;
    }
}

It is also possible to abort the transaction during the executing or executed phases by throwing an exception. If the exception is a subtype of RecoverableException then the exception will be rendered as a user-friendly warning (eg Growl/toast) rather than an error.

Raising events programmatically

Normally events are only raised for interactions through the UI. However, events can be raised programmatically either by calling the EventBusService API directly, or by emulating the UI by wrapping the target object using the WrapperFactory domain service.

1.4.2. editing()

The editing() annotation indicates whether a collection can be added to or removed from within the UI. This attribute can also be specified for properties, and can also be specified for the domain object

The related editingDisabledReason() attribute specifies the a hard-coded reason why the collection cannot be modified directly.

The Wicket viewer does _not currently support the modification of collections; they are rendered read-only.

The workaround is to create add/remove actions and use UI hints to render them close to the collection.

Whether a collection is enabled or disabled depends upon these factors:

  • whether the domain object has been configured as immutable through the @DomainObject#editing() attribute

  • else (that is, if the domain object’s editability is specified as being AS_CONFIGURED), then the value of the configuration property isis.objects.editing. If set to false, then the object’s collections (and properties) are _not editable

  • else, then the value of the @Collection(editing=…​) attribute itself.

  • else, the result of invoking any supporting disable…​() supporting methods

Thus, to make a collection read-only even if the object would otherwise be editable, use:

public class ToDoItem {
    @Collection(
        editing=Editing.DISABLED,
        editingDisabledReason="Use the add and remove actions to modify"
    )
    public SortedSet<ToDoItem> getDependencies() { ... }
}

To reiterate, it is not possible to enable editing for a collection if editing has been disabled at the object-level.

1.4.3. hidden()

Collections can be hidden at the domain-level, indicating that they are not visible to the end-user. This attribute can also be applied to actions and properties.

It is also possible to use @CollectionLayout#hidden() or dynamic layouts such that the collection can be hidden at the view layer. Both options are provided with a view that in the future the view-layer semantics may be under the control of (expert) users, whereas domain-layer semantics should never be overridden or modified by the user.

For example:

public class Customer {
    @Collection(where=Where.EVERYWHERE)
    public SortedSet<Address> getAddresses() { ... }
}

The acceptable values for the where parameter are:

  • Where.EVERYWHERE or Where.ANYWHERE

    The collection should be hidden everywhere.

  • Where.ANYWHERE

    Synonym for everywhere.

  • Where.OBJECT_FORMS

    The collection should be hidden when displayed within an object form.

  • Where.NOWHERE

    The collection should not be hidden.

The other values of the Where enum have no meaning for a collection.

The Wicket viewer suppresses collections when displaying lists of objects.

The RestfulObjects viewer by default suppress collections when rendering a domain object.

1.4.4. notPersisted()

The (somewhat misnamed) notPersisted() attribute indicates that the collection should be excluded from any snapshots generated by the XmlSnapshotService. This attribute is also supported for properties.

This annotation does not specify that a collection is not persisted in the JDO/DataNucleus objectstore. See below for details as to how to additionally annotate the collection for this.

For example:

public class Customer {
    @Collection(notPersisted=true)
    public SortedSet<Order> getPreviousOrders() {...}
    public void setPreviousOrder(SortedSet<Order> previousOrders) {...}
    ...
}

Historically this annotation also hinted as to whether the collection’s contents should be persisted in the object store. However, the JDO/DataNucleus objectstore does not recognize this annotation. Thus, to ensure that a collection is actually not persisted, it should also be annotated with @javax.jdo.annotations.NotPersistent.

For example:

public class Customer {
    @Collection(notPersisted=true)             (1)
    @javax.jdo.annotations.NotPersistent       (2)
    public SortedSet<Order> getPreviousOrders() {...}
    public void setPreviousOrder(SortedSet<Order> previousOrders) {...}
    ...
}
1 ignored by Isis
2 ignored by JDO/DataNucleus

Alternatively, if the collection is derived, then providing only a "getter" will also work:

public class Customer {
    public SortedSet<Order> getPreviousOrders() {...}
    ...
}

1.4.5. typeOf()

The typeOf() attribute specifies the expected type of an element contained within a collection when for whatever reason the type cannot be inferred from the generic type, or to provide a hint about the actual run-time (as opposed to compile-time) type. This attribute can also be specified for actions.

For example:

public void Customer {
    @TypeOf(Order.class)
    public SortedSet getOutstandingOrders() { ... }
    ...
}

In general we recommend that you use generics instead, eg SortedSet<Order>.

1.5. @CollectionLayout

The @CollectionLayout annotation applies to collections, collecting together all UI hints within a single annotation. It is also possible to apply the annotation to actions of domain services that are acting as contributed collections.

The table below summarizes the annotation’s attributes.

Table 11. @CollectionLayout attributes
Attribute Values (default) Description

cssClass()

Any string valid as a CSS class

the css class that a collection should have, to allow more targetted styling in application.css

describedAs()

String.

description of this collection, eg to be rendered in a tooltip.

hidden()

EVERYWHERE, OBJECT_FORMS, NOWHERE
(NOWHERE)

indicates where (in the UI) the collection should be hidden from the user.

named()

String.

to override the name inferred from the collection’s name in code.

A typical use case is if the desired name is a reserved Java keyword, such as default or package.

namedEscaped()

true,false (true)

whether to HTML escape the name of this property.

paged()

Positive integer

the page size for instances of this class when rendered within a table.

render()

EAGERLY, LAZILY
(LAZILY)

whether the collection should be (eagerly) rendered open or (lazily) rendered closed

sortedBy()

Subclass of java.util.Comparator for element type

indicates that the elements in the java.util.SortedSet collection should be sorted according to a specified Comparator rather than their natural sort order.

For example:

public class ToDoItem {
    @CollectionLayout(
        cssClass="x-key",
        named="Todo items that are <i>dependencies</i> of this item.",
        namedEscaped=false,
        describedAs="Other todo items that must be completed before this one",
        labelPosition=LabelPosition.LEFT,
        render=EAGERLY)
    public SortedSet<ToDoItem> getDependencies() { ... }
    ...
}

As an alternative to using the @CollectionLayout annotation, a dynamic layout using .layout.json file can be specified; for example:

"dependencies": {
    "collectionLayout": {
        "cssClass": "x-key",
        "named": "Todo items that are <i>dependencies</i> of this item.",
        "namedEscaped": false,
        "describedAs": "Other todo items that must be completed before this one",
        "labelPosition": "LEFT",
        "render": "EAGERLY"
    }
}

1.5.1. cssClass()

The cssClass() attribute can be used to render additional CSS classes in the HTML (a wrapping <div>) that represents the collection. Application-specific CSS can then be used to target and adjust the UI representation of that particular element.

This attribute can also be applied to domain objects, view models, actions, properties and parameters.

For example:

public class ToDoItem {
    @CollectionLayout(
        cssClass="x-important"
    )
    public SortedSet<ToDoItem> getDependencies() { ... }
    ...
}

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"dependencies": {
    "collectionLayout": { "cssClass": "x-important" }
}

1.5.2. describedAs()

The describedAs() attribute is used to provide a short description of the collection to the user. In the Wicket viewer it is displayed as a 'tool tip'.

The describedAs() attribute can also be specified for properties, actions, parameters, domain objects and view models.

For example:

public class ToDoItem {
    @CollectionLayout(
        describedAs="Other todo items that must be completed before this one")
    public SortedSet<ToDoItem getDependencies() { ... }
    ...
}

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"dependencies": {
    "collectionLayout": {
        "describedAs": "Other todo items that must be completed before this one"
    }
}

1.5.3. hidden()

The hidden() attribute indicates where (in the UI) the collection should be hidden from the user. This attribute can also be applied to actions and properties.

It is also possible to use @Collection#hidden() to hide an action at the domain layer. Both options are provided with a view that in the future the view-layer semantics may be under the control of (expert) users, whereas domain-layer semantics should never be overridden or modified by the user.

For example:

public class ToDoItem {
    @CollectionLayout(
        hidden=Where.EVERYWHERE
    public SortedSet<ToDoItem> getDependencies() { ... }
    ...
}

The acceptable values for the where parameter are:

  • Where.EVERYWHERE or Where.ANYWHERE

    The collection should be hidden everywhere.

  • Where.ANYWHERE

    Synonym for everywhere.

  • Where.OBJECT_FORMS

    The collection should be hidden when displayed within an object form.

  • Where.NOWHERE

    The collection should not be hidden.

The other values of the Where enum have no meaning for a collection.

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"dependencies": {
    "collectionLayout": { "hidden": "EVERYWHERE" }
}

1.5.4. named()

The named() attribute explicitly specifies the collection’s name, overriding the name that would normally be inferred from the Java source code. This attribute can also be specified for actions, properties, parameters, domain objects, view models and domain services.

Following the don’t repeat yourself principle, we recommend that you only use this attribute when the desired name cannot be used in Java source code. Examples of that include a name that would be a reserved Java keyword (eg "package"), or a name that has punctuation, eg apostrophes.

By default the name is HTML escaped. To allow HTML markup, set the related namedEscaped() attribute to false.

For example:

public class ToDoItem {
    @CollectionLayout(
        named="Todo items that are <i>dependencies</i> of this item",
        namedEscaped=false
    )
    public SortedSet<ToDoItem getDependencies() { ... }
    ...
}

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"dependencies": {
    "collectionLayout": {
        "named": "Todo items that are <i>dependencies</i> of this item",
        "namedEscaped": false,
    }
}

The framework also provides a separate, powerful mechanism for internationalization.

1.5.5. paged()

The paged() attribute specifies the number of rows to display in a (parented) collection. This attribute can also be applied to domain objects and view models.

The RestfulObjects viewer currently does not support paging. The Wicket viewer _does support paging, but note that the paging is performed client-side rather than server-side.

We therefore recommend that large collections should instead be modelled as actions (to allow filtering to be applied to limit the number of rows).

For example:

public class Order {
    @CollectionLayout(paged=15)
    public SortedSet<OrderLine> getDetails() {...}
}

It is also possible to specify a global default for the page size of standalone collections, using the configuration property isis.viewer.paged.parented.

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"details": {
    "collectionLayout": {
        "paged": 15
    }
}

1.5.6. render()

The render() attribute specifies that the collection be rendered either "eagerly" (shown open, displaying its contents) or "lazily" (shown closed, hiding its contents). The terminology here is based on the similar concept of lazy loading of collections in the domain/persistence layer boundary (except that the rendering relates to the presentation/domain layer boundary).

For example:

public class Order {
    @CollectionLayout(render=RenderType.EAGERLY)
    public SortedSet<LineItem> getDetails() { ... }
    ...
}

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"details": {
    "collectionLayout": {
        "render": "EAGERLY"
    }
}

Note that contributed collections (which, under the covers are just action invocations against a domain service) are always rendered eagerly.

1.5.7. sortedBy()

The sortedBy() attribute specifies that the collection be ordered using the specified comparator, rather than the natural ordering of the entity (as would usually be the case).

For example:

public class ToDoItem implements Comparable<ToDoItem> {      (1)
    public static class DependenciesComparator               (2)
            implements Comparator<ToDoItem> {
        @Override
        public int compare(ToDoItem p, ToDoItem q) {
            return ORDERING_BY_DESCRIPTION                   (3)
                    .compound(Ordering.<ToDoItem>natural())
                    .compare(p, q);
        }
    }
    @CollectionLayout(sortedBy=DependenciesComparator.class) (4)
    public SortedSet<ToDoItem> getDependencies() { ... }
    ...
}
1 the class has a natural ordering (implementation not shown)
2 declaration of the comparator class
3 ordering defined as being by the object’s description property (not shown), and then by the natural ordering of the class
4 specify the comparator to use

When the dependencies collection is rendered, the elements are sorted by the description property first:

sortedby dependencies

Note that this screenshot shows an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

Without this annotation, the order would have been inverted (because the natural ordering places items not completed before those items that have been completed.

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"dependencies": {
    "collectionLayout": {
        "sortedBy": "com.mycompany.myapp.dom.ToDoItem.DependenciesComparator"
    }
}

1.6. @Column (javax.jdo)

The JDO @javax.jdo.annotation.Column provides metadata describing how JDO/DataNucleus should persist the property to a database RDBMS table column (or equivalent concept for other persistence stores).

Apache Isis also parses and interprets this annotation in order to build up aspects of its metamodel.

Isis parses the @Column annotation from the Java source code; it does not query the JDO metamodel. This means that it the @Column annotation must be used rather than the equivalent <column> XML metadata.

Moreover, while JDO/DataNucleus will recognize annotations on either the field or the getter method, Isis (currently) only inspects the getter method. Therefore ensure that the annotation is placed there.

This section identifies which attributes of @Column are recognized and used by Isis.

1.6.1. Nullability

The allowsNull() attribute is used to specify if a property is mandatory or is optional.

For example:

public class Customer {
    @javax.jdo.annotations.Column(allowNulls="true")
    public String getMiddleInitial() { ... }
    public void setMiddleInitial(String middleInitial) { ... }

Isis also provides @Property#optionality() attribute. If both are specified, Isis will check when it initializes for any contradictions, and will fail-fast with an appropriate error message in the log if there are.

You should also be aware that in the lack of either the @Column#allowsNull() or the @Property#optionality() attributes, that the JDO and Isis defaults differ. Isis rule is straight-forward: properties are assumed to be required. JDO on the other hand specifies that only primitive types are mandatory; everything else is assumed to be optional. Therefore a lack of either annotation can also trigger the fail-fast validation check.

In the vast majority of cases you should be fine just to add the @Column#allowsNull() attribute to the getter. But see the documentation for @Property#optionality() attribute for discussion on one or two minor edge cases.

1.6.2. Length for Strings

The length() attribute is used to specify the length of java.lang.String property types as they map to varchar(n) columns.

For example:

public class Customer {
    @javax.jdo.annotations.Column(length=20)
    public String getFirstName() { ... }
    public void setFirstName(String firstName) { ... }
    @javax.jdo.annotations.Column(allowNulls="true", length=1)
    public String getMiddleInitial() { ... }
    public void setMiddleInitial(String middleInitial) { ... }
    @javax.jdo.annotations.Column(length=30)
    public String getLastName() { ... }
    public void setLastName(String lastName) { ... }

Isis also provides @Property#maxLength() attribute. If both are specified, Isis will check when it initializes for any contradictions, and will fail-fast with an appropriate error message in the log if there are.

1.6.3. Length/scale for BigDecimals

The length() and scale() attributes are used to infer the precision/scale of java.math.BigDecimal property types as they map to decimal(n,p) columns.

For example:

public class Customer {
    @javax.jdo.annotations.Column(length=10, scale=2)
    public BigDecimal getTotalOrdersToDate() { ... }
    public void setTotalOrdersToDate(BigDecimal totalOrdersToDate) { ... }

For BigDecimals it is also possible to specify the @Digits annotation, whose form is @Digits(integer, fraction). There is a subtle difference here: while @Column#scale() corresponds to @Digits#fraction(), the value of @Column#length() (ie the precision) is actually the _sum of the @Digits’ `integer() and fraction() parts.

If both are specified, Isis will check when it initializes for any contradictions, and will fail-fast with an appropriate error message in the log if there are.

1.6.4. Hints and Tips

This seems to be a good place to describe some additional common mappings that use @Column. Unlike the sections above, the attributes specified in these hints and tips aren’t actually part of Isis metamodel.

Mapping foreign keys

The name() attribute can be used to override the name of the column. References to other objects are generally mapped as foreign key columns. If there are multiple references to a given type, then you will want to override the name that JDO/DataNucleus would otherwise default.

For example (taken from estatio app):

public class PartyRelationship {
    @Column(name = "fromPartyId", allowsNull = "false")
    public Party getFrom() { ... }
    public void setFrom(Party from) { ... }
    @Column(name = "toPartyId", allowsNull = "false")
    public Party getTo() { ... }
    public void setTo(Party to) { ... }
    ...
}

1.6.5. Mapping Blobs and Clobs

Isis provides custom value types for Blobs and Clobs. These value types have multiple internal fields, meaning that they corresponding to multiple columns in the database. Mapping this correctly requires using @Column within JDO’s @Persistent annotation.

For example, here’s how to map a Blob (taken from (non-ASF) Isis addons' todoapp):

private Blob attachment;
@javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = {
        @javax.jdo.annotations.Column(name = "attachment_name"),
        @javax.jdo.annotations.Column(name = "attachment_mimetype"),
        @javax.jdo.annotations.Column(name = "attachment_bytes", jdbcType = "BLOB", sqlType = "BLOB")
})
@Property(
        domainEvent = AttachmentDomainEvent.class,
        optionality = Optionality.OPTIONAL
)
public Blob getAttachment() { ... }
public void setAttachment(Blob attachment) { ... }

And here’s how to map a Clob (also taken from the todoapp):

private Clob doc;
@javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = {
        @javax.jdo.annotations.Column(name = "doc_name"),
        @javax.jdo.annotations.Column(name = "doc_mimetype"),
        @javax.jdo.annotations.Column(name = "doc_chars", jdbcType = "CLOB", sqlType = "CLOB")
})
@Property(
        optionality = Optionality.OPTIONAL
)
public Clob getDoc() { ... }
public void setDoc(final Clob doc) { ... }

1.7. @Digits (javax)

The @javax.validation.constraints.Digits annotation is recognized by Apache Isis as a means to specify the precision for properties and action parameters of type java.math.BigDecimal.

For example (taken from the (non-ASF) Isis addons' todoapp):

@javax.jdo.annotations.Column(
    scale=2                                              (1)
)
@javax.validation.constraints.Digits(
    integer=10,
    fraction=2                                           (2)
)
public BigDecimal getCost() {
    return cost;
}
public void setCost(final BigDecimal cost) {
    this.cost = cost!=null
        ? cost.setScale(2, BigDecimal.ROUND_HALF_EVEN)   (3)
        :null;
}
1 the @Column#scale() attribute must be …​
2 …​ consistent with @Digits#fraction()
3 the correct idiom when setting a new value is to normalized to the correct scale

1.8. @Discriminator (javax.jdo)

The @javax.jdo.annotation.Discriminator is used by JDO/DataNucleus to specify how to discriminate between subclasses of an inheritance hierarchy.

It is valid to add a @Discriminator for any class, even those not part of an explicitly mapped inheritance hierarchy. Apache Isis also checks for this annotation, and if present will use the @Discriminator#value() as the object type, a unique alias for the object’s class name.

Isis parses the @Discriminator annotation from the Java source code; it does not query the JDO metamodel. This means that it the @Discriminator annotation must be used rather than the equivalent <discriminator> XML metadata.

Moreover, while JDO/DataNucleus will recognize annotations on either the field or the getter method, Isis (currently) only inspects the getter method. Therefore ensure that the annotation is placed there.

The object type is used internally by Isis to generate a string representation of an objects identity (the Oid). This can appear in several contexts, including:

For example:

@javax.jdo.annotations.Discriminator(value="custmgmt.Customer")
public class Customer {
    ...
}

has an object type of custmgmt.Customer.

If the object type has not been specified, then Isis will use the fully qualified class name of the entity.

This might be obvious, but to make explicit: we recommend that you use namespaces of some form or other.

If the object type has not been specified (by either this annotation, or by @PersistenceCapable, or by Isis' own @DomainObject#objectType() annotation/attribute), then (as noted above) Isis will use the fully qualified class name of the entity.

However, chances are that the fully qualified class name is liable to change over time, for example if the code is refactored or (more fundamentally) if your company/organization reorganizes/renames itself/is acquired.

We therefore strongly recommend that you specify an object type for all entities, one way or another. Specifying @Discriminator will override @PersistenceCapable, which overrides @DomainObject#objectType(). Using @PersistenceCapable#schema() is probably the best choice in most cases.

If the object type is not unique across all domain classes then the framework will fail-fast and fail to boot. An error message will be printed in the log to help you determine which classes have duplicate object tyoes.

1.9. @DomainObject

The @DomainObject annotation applies to domain objects, collecting together all domain semantics within a single annotation.

The table below summarizes the annotation’s attributes.

Table 12. @DomainObject attributes
Attribute Values (default) Description

auditing()

AS_CONFIGURED, ENABLED, DISABLED
(AS_CONFIGURED)

indicates whether each of the changed properties of an object should be submitted to the registered AuditingService.

autoCompleteRepository()

Domain service class

nominate a method on a domain service to be used for looking up instances of the domain object

autoCompleteAction()

Method name
(autoComplete())

override the method name to use on the auto-complete repository

bounded()

true, false
(false)

Whether the number of instances of this domain class is relatively small (a "bounded" set), such that instances could be selected from a drop-down list box or similar.

editing()

AS_CONFIGURED, ENABLED, DISABLED
(AS_CONFIGURED)

whether the object’s properties and collections can be edited or not (ie whether the instance should be considered to be immutable)

nature()

NOT_SPECIFIED, JDO_ENTITY, EXTERNAL_ENTITY, INMEMORY_ENTITY, VIEW_MODEL (NOT_SPECIFIED)

whether the domain object logically is an entity (part of the domain layer) or is a view model (part of the application layer); and if an entity, how is its persistence managed

objectType()

(none, which implies fully qualified class name)

specify an alias for the domain class used to uniquely identify the object both within the Isis runtime and externally

publishing()

AS_CONFIGURED, ENABLED, DISABLED
(AS_CONFIGURED)

whether changes to the object should be published to the registered PublishingService.

publishing-
PayloadFactory()

subtype of PublishingPayloadFactory- ForObject (none)

specifies that a custom implementation of PublishingPayloadFactoryForObject be used to create the (payload of the) published event representing the change to the object

For example:

@DomainObject(
    auditing=Auditing.ENABLED,
    autoCompleteRepository=CustomerRepository.class
    editing=Editing.ENABLED,               (1)
    publishing=Publishing.ENABLED
)
public class Customer {
    ...
}
1 default value, so could be omitted

1.9.1. auditing()

The auditing() attribute indicates that if the object is modified, then each of its changed properties should be submitted to the AuditingService, if one has been configured.

The default value for the attribute is AS_CONFIGURED, meaning that the configuration property isis.services.audit.objects is used to determine the whether the action is published:

  • all

    all changed objects are published

  • none

    no changed objects are published

If there is no configuration property in isis.properties then auditing is automatically enabled for domain objects.

This default can be overridden on an object-by-object basis; if auditing() is set to ENABLED then changed properties of instances of the domain class are audited irrespective of the configured value; if set to DISABLED then the changed properties of instances are not audited, again irrespective of the configured value.

For example:

@DomainObject(
    auditing=Auditing.ENABLED  (1)
)
public class Customer {
    ...
}
1 because set to enabled, will be audited irrespective of the configured value.

1.9.2. autoCompleteRepository()

The autoCompleteRepository() attribute nominates a single method on a domain service as the fallback means for lookuping up instances of the domain object using a simple string.

For example, this might search for a customer by their name or number. Or it could search for a country based on its ISO-3 code or user-friendly name.

If you require additional control - for example restricting the returned results based on the object being interacted with - then use the autoComplete…​() supporting method instead.

For example:

@AutoComplete(
    autoCompleteRepository=Customers.class
)
public class Customer extends AbstractDomainObject {
   ....
}

where:

public interface Customers {
    @Programmatic                                (1)
    List<Customer> autoComplete(String search);  (2)
    ...
}
1 do not expose in the UI
2 is assumed to be called "autoComplete", and accepts a single string
autoCompleteAction()

As noted above, by default the method invoked on the repository is assumed to be called "autoComplete". The optional autoCompleteAction() attribute allows the method on the repository to be overridden.

For example:

@AutoComplete(
    autoCompleteRepository=Customers.class,
    autoCompleteAction="findByCustomerName"
)
public class Customer extends AbstractDomainObject {
   ....
}

where in this case findByCustomerName might be an existing method already defined:

public interface Customers {
    @Action(semantics=SemanticsOf.SAFE)
    List<Customer> findByCustomerName(@ParameterLayout(named="name") String name);
    ...
}

1.9.3. bounded()

Some domain classes are immutable to the user, and moreover have only a fixed number of instances. Often these are "reference" ("standing") data, or lookup data/pick lists. Typical examples could include categories, countries, states, and tax or interest rate tables.

Where the number of instances is relatively small, ie bounded, then the bounded() attribute can be used as a hint. For such domain objects the framework will automatically allow instances to be selected; Wicket viewer displays these as a drop-down list.

For example:

@DomainObject(
    bounded=true,
    editing=Editing.DISABLED  (1)
)
public class Currency {
    ...
}
1 This attribute is commonly combined with editing=DISABLED to enforce the fact that reference data is immutable

There is nothing to prevent you from using this attribute for regular mutable entities, and indeed this is sometimes worth doing during early prototyping. However, if there is no realistic upper bound to the number of instances of an entity that might be created, generally you should use autoComplete…​() supporting method or the @DomainObject#autoComplete() attribute instead.

1.9.4. editing()

The editing() attribute determines whether a domain object’s properties and collections are not editable (are read-only).

The default is AS_CONFIGURED, meaning that the configuration property isis.objects.editing is used to determine the whether the object is modifiable:

  • true

    the object’s properties and collections are modifiable.

  • false

    the object’s properties and collections are read-only, ie not modifiable.

If there is no configuration property in isis.properties then object are assumed to be modifiable.

In other words, editing can be disabled globally for an application by setting:

isis.objects.editing=false

We recommend enabling this feature; it will help drive out the underlying business operations (processes and procedures) that require objects to change; these can then be captured as business actions.

The related editingDisabledReason() attribute specifies the a hard-coded reason why the object’s properties and collections cannot be modified directly.

This default can be overridden on an object-by-object basis; if editing() is set to ENABLED then the object’s properties and collections are editable irrespective of the configured value; if set to DISABLED then the object’s properties and collections are not editable irrespective of the configured value.

For example:

@DomainObject(
    editing=Editing.DISABLED,
    editingDisabledReason="Reference data, so cannot be modified"
)
public class Country {
    ...
}

Another interesting example of immutable reference data is to define an entity to represent individual dates; after all, for a system with an expected lifetime of 20 years that equates to only 7,300 days, a comparatively tiny number of rows to hold in a database.

1.9.5. nature()

The nature() attribute is used to characterize the domain object as either an entity (part of the domain layer) or as a view model (part of the application layer). If the domain object should be thought of as an entity, it also captures how the persistence of that entity is managed.

For example:

@DomainObject(nature=Nature.VIEW_MODEL)
public class PieChartAnalysis {
    ...
}

Specifically, the nature must be one of:

  • NOT_SPECIFIED,

    (the default); specifies no paricular semantics for the domain class.

  • JDO_ENTITY

    indicates that the domain object is an entity whose persistence is managed internally by Isis, using the JDO/DataNucleus objectstore.

  • EXTERNAL_ENTITY

    indicates that the domain objecct is a wrapper/proxy/stub (choose your term) to an entity that is managed by some related external system. For example, the domain object may hold just the URI to a RESTful resource of some third party REST service, or the id of some system accessible over SOAP.

    The identity of an external entity is determined solely by the state of entity’s properties. The framework will automatically recreate the domain object each time it is interacted with.

  • INMEMORY_ENTITY

    indicates that the domain object is a wrapper/proxy/stub to a "synthetic" entity, for example one that is constructed from some sort of internal memory data structure.

    The identity of an inmemory entity is determined solely by the state of entity’s properties. The framework will automatically recreate the domain object each time it is interacted with.

  • VIEW_MODEL

    indicates that the domain object is conceptually part of the application layer, and exists to surfaces behaviour and/or state that is aggregate of one or more domain entities.

The identity of an inmemory entity is determined solely by the state of entity’s properties. The framework will automatically recreate the domain object each time it is interacted with.

Those natures that indicate the domain object is an entity (of some sort or another) mean then that the domain object is considered to be part of the domain model layer. As such the domain object’s class cannot be annotated with @ViewModel or implement the ViewModel interface.

Under the covers Isis' support for VIEW_MODEL, EXTERNAL_ENTITY and INMEMORY_ENTITY domain objects is identical; the state of the object is encoded into its internal OID (represented ultimately as its URL), and is recreated directly from that URL.

Because this particular implementation was originally added to Isis in support of view models, the term was also used for the logically different external entities and inmemory entities.

The benefit of nature() is that it allows the developer to properly characterize the layer (domain vs application) that an entity lives, thus avoiding confusion as "view model" (the implementation technique) and "view model" (the application layer concept).

1.9.6. objectType()

The objectType() attribute is used to provide a unique alias for the object’s class name.

This value is used internally to generate a string representation of an objects identity (the Oid). This can appear in several contexts, including:

For example:

@DomainObject(
    objectType="ORD"
)
public class Order {
    ...
}

If the object type has not been specified, then Isis will use the fully qualified class name of the entity.

This might be obvious, but to make explicit: we recommend that you use namespaces of some form or other for object types.

As noted above, if the object type has not been specified, then Isis will use the fully qualified class name of the entity. However, this is liable to change over time, for example if the code is refactored or (more fundamentally) if your company/organization reorganizes/renames itself/is acquired.

We therefore strongly recommend that you specify an object type for all entities, either using objectType() or using the JDO @PersistenceCapable (with a schema() attribute) or @Discriminator annotations. Specifying @Discriminator will override @PersistenceCapable, which in turn overrides objectType(). Using @PersistenceCapable#schema() is probably the best choice in most cases.

If the object type is not unique across all domain classes then the framework will fail-fast and fail to boot. An error message will be printed in the log to help you determine which classes have duplicate object tyoes.

1.9.7. publishing()

The publishing() attribute determines whether and how a modified object instance is published via the registered implementation of a PublishingService). This attribute is also supported for actions, where it controls whether action invocations are published as events.

A common use case is to notify external "downstream" systems of changes in the state of the Isis application.

The default value for the attribute is AS_CONFIGURED, meaning that the configuration property isis.services.publish.objects is used to determine the whether the action is published:

  • all

    all changed objects are published

  • none

    no changed objects are published

If there is no configuration property in isis.properties then publishing is automatically enabled for domain objects.

This default can be overridden on an object-by-object basis; if publishing() is set to ENABLED then changed instances of the domain class are published irrespective of the configured value; if set to DISABLED then the changed instances are not published, again irrespective of the configured value.

For example:

@DomainObject(
    publishing=Publishing.ENABLED  (1)
)
public class InterestRate {
    ...
}
1 because set to enabled, will be published irrespective of the configured value.
publishingPayloadFactory()

The (optional) related publishingPayloadFactory() specifies the class to use to create the (payload of the) event to be published by the publishing factory.

Rather than simply broadcast that the object was changed, the payload factory allows a "fatter" payload to be instantiated that can eagerly push commonly-required information to all subscribers. For at least some subscribers this should avoid the necessity to query back for additional information.

1.10. @DomainObjectLayout

The @DomainObjectLayout annotation applies to domain classes, collecting together all UI hints within a single annotation.

For view models that have been annotated with @ViewModel the equivalent @ViewModelLayout can be used.

The table below summarizes the annotation’s attributes.

Table 13. @DomainObjectLayout attributes
Attribute Values (default) Description

bookmarking()

AS_ROOT, AS_CHILD, NEVER
(NEVER)

whether (and how) this domain object should be automatically bookmarked

cssClass()

Any string valid as a CSS class

the css class that a domain class (type) should have, to allow more targetted styling in application.css

cssClassFa()

Any valid Font awesome icon name

specify a font awesome icon for the action’s menu link or icon.

cssClassFaPosition()

LEFT, RIGHT
(LEFT)

Currently unused.

describedAs()

String.

description of this class, eg to be rendered in a tooltip.

named()

String.

to override the name inferred from the action’s name in code.

A typical use case is if the desired name is a reserved Java keyword, such as default or package.

paged()

Positive integer

the page size for instances of this class when rendered within a table (as returned from an action invocation)

plural()

String.

the plural name of the class

For example:

@DomainObjectLayout(
    cssClass="x-key",
    cssClassFa="fa-checklist",
    describedAs="Capture a task that you need to do",
    named="ToDo",
    paged=30,
    plural="ToDo List")
)
public class ToDoItem {
    ...
}

Note that there is (currently) no support for specifying UI hints for domain objects through the dynamic .layout.json file (only for properties, collections and actions are supported).

1.10.1. bookmarking()

The bookmarking() attribute indicates that an entity is automatically bookmarked. This attribute is also supported for domain objects.

(In the Wicket viewer), a link to a bookmarked object is shown in the bookmarks panel:

bookmarking

Note that this screenshot shows an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

For example:

@DomainObject(bookmarking=BookmarkPolicy.AS_ROOT)
public class ToDoItem ... {
    ...
}

indicates that the ToDoItem class is bookmarkable:

It is also possible to nest bookmarkable entities. For example, this screenshot is taken from Estatio:

bookmarking nested

Note that this screenshot shows an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

For example, the Property entity "[OXF] Oxford Super Mall" is a root bookmark, but the Unit child entity "[OXF-001] Unit 1" only appears as a bookmark but only if its parent Property has already been bookmarked.

This is accomplished with the following annotations:

@DomainObject(bookmarking=BookmarkPolicy.AS_ROOT)
public class Property { ... }

and

@DomainObject(bookmarking=BookmarkPolicy.AS_CHILD)
public abstract class Unit { ... }

The nesting can be done to any level; the Estatio screenshot also shows a bookmark nesting Lease > LeaseItem > LeaseTerm (3 levels deep).

1.10.2. cssClass()

The cssClass() attribute can be used to render additional CSS classes in the HTML (a wrapping <div>) that represents the domain object. Application-specific CSS can then be used to target and adjust the UI representation of that particular element.

This attribute can also be applied to domain objects, view models, actions properties, collections and parameters.

For example:

@DomainObject(
    cssClass="x-core-entity"
)
public class ToDoItem { ... }

The similar @DomainObjectLayout#cssClassFa() annotation attribute is also used as a hint to apply CSS, but in particular to allow Font Awesome icons to be rendered as the icon for classes.

1.10.3. cssClassFa()

The cssClassFa() attribute is used to specify the name of a Font Awesome icon name, to be rendered as the domain object’s icon.

These attributes can also be applied to view models to specify the object’s icon, and to actions to specify an icon for the action’s representation as a button or menu item.

If necessary the icon specified can be overridden by a particular object instance using the iconName() method.

For example:

@DomainObjectLayout(
    cssClassFa="fa-check-circle"
)
public class ToDoItem { ... }

There can be multiple "fa-" classes, eg to mirror or rotate the icon. There is no need to include the mandatory fa "marker" CSS class; it will be automatically added to the list. The fa- prefix can also be omitted from the class names; it will be prepended to each if required.

The related cssClassFaPosition() attribute is currently unused for domain objects; the icon is always rendered to the left.

The similar @DomainObjectLayout#cssClass() annotation attribute is also used as a hint to apply CSS, but for wrapping the representation of an object or object member so that it can be styled in an application-specific way.

1.10.4. describedAs()

The describedAs() attribute is used to provide a short description of the domain object to the user. In the Wicket viewer it is displayed as a 'tool tip'. The attribute can also be specified for collections, properties, actions, parameters and view models.

For example:

@DescribedAs("A customer who may have originally become known to us via " +
             "the marketing system or who may have contacted us directly.")
public class ProspectiveSale {
   ...
}

1.10.5. named()

The named() attribute explicitly specifies the domain object’s name, overriding the name that would normally be inferred from the Java source code. The attribute can also be specified for actions, collections, properties, parameters, view models and domain services.

Following the don’t repeat yourself principle, we recommend that you only use this attribute when the desired name cannot be used in Java source code. Examples of that include a name that would be a reserved Java keyword (eg "package"), or a name that has punctuation, eg apostrophes.

For example:

@DomainObjectLayout(
   named="Customer"
)
public class CustomerImpl implements Customer{
   ...
}

It’s also possible to specify a plural form of the name, used by the framework when rendering a standalone collection of the domain object.

The framework also provides a separate, powerful mechanism for internationalization.

1.10.6. paged()

The paged() attribute specifies the number of rows to display in a standalone collection, as returned from an action invocation. This attribute can also be applied to collections and view models.

The RestfulObjects viewer currently does not support paging. The Wicket viewer _does support paging, but note that the paging is performed client-side rather than server-side.

We therefore recommend that large collections should instead be modelled as actions (to allow filtering to be applied to limit the number of rows).

For example:

@DomainObjectLayout(paged=15)
public class Order {
    ...
}

It is also possible to specify a global default for the page size of standalone collections, using the configuration property isis.viewer.paged.standalone.

1.10.7. plural()

When Apache Isis displays a standalone collection of several objects, it will label the collection using the plural form of the object type.

By default the plural name will be derived from the end of the singular name, with support for some basic English language defaults (eg using "ies" for names ending with a "y").

The plural() attribute allows the plural form of the class name to be specified explicitly. This attribute is also supported for view models.

For example:

@DomainObjectLayout(plural="Children")
public class Child {
    ...
}

1.11. @DomainService

The @DomainService annotation indicates that the (concrete) class should be automatically instantiated as a domain service.

Domain services with this annotation do NOT need to be registered explicitly in isis.properties; they will be discovered automatically on the CLASSPATH.

The table below summarizes the annotation’s attributes.

Table 14. @DomainService attributes
Attribute Values (default) Description

nature()

VIEW, VIEW_MENU_ONLY, VIEW_CONTRIBUTIONS_ONLY, VIEW_REST_ONLY, DOMAIN (VIEW)

the nature of this service: providing actions for menus, or as contributed actions, or for the RestfulObjects REST API, or neither

repositoryFor()

if this domain service acts as a repository for an entity type, specify that entity type. (This is currently informational only)

menuOrder()

Deprecated in 1.8.0; use instead @DomainServiceLayout#menuOrder()

For example:

@DomainService(
    nature=NatureOfService.DOMAIN,
    repositoryFor=Loan.class
)
public class LoanRepository {
    @Programmatic
    public List<Loan> findLoansFor(Borrower borrower) { ... }
}

1.11.1. nature()

By default, a domain service’s actions will be rendered in the application menu bar and be contributed and appear in the REST API and (of course) be available to invoke programmatically wherever that domain service is injected. This is great for initial prototyping, but later on you may prefer to add a little more structure. This is the purpose of the nature() attribute: to indicates the intent of (all of) the actions defined within the domain service.

The values of the enum are:

  • VIEW

    The default; the service’s actions appear on menu bars, can be contributed, appear in the REST API

  • VIEW_MENU_ONLY

    The service’s actions appear on menus and in the REST API, but are not contributed to domain objects or view models

  • VIEW_CONTRIBUTIONS_ONLY

    The service’s actions are intended only to be used as contributed actions/associations to domain objects and view models.

    The related @ActionLayout#contributedAs() determines whether any given (1-arg) action is contributed as an association rather than an action.

  • VIEW_REST_ONLY

    The service’s actions are intended only to be listed in the REST API exposed by the RestfulObjects viewer.

  • DOMAIN

    The service and its actions are only intended to be invoked programmatically; they are a domain layer responsibility.

The actual class name of the domain service is only rendered for the VIEW, VIEW_MENU_ONLY and VIEW_REST_ONLY natures. Thus, you might also want to adopt naming conventions for your domain classes so you can infer the nature from the class. For example, the naming convention adopted (by and large) by the (non-ASF) Isis Addons is ProgrammaticServices or Repository as a suffix for DOMAIN services, and Contributions as a suffix for VIEW_CONTRIBUTIONS_ONLY services.

For example:

@DomainService(
    nature=NatureOfService.VIEW_CONTRIBUTIONS_ONLY
)
public class LoanContributions {                                  (1)
    @Action(semantics=SemanticsOf.SAFE)                           (2)
    @ActionLayout(contributed=Contributed.AS_ASSOCIATION )
    public List<Loan> currentLoans(Borrower borrower) { ... }
    public Borrower newLoan(Borrower borrower, Book book) { ... }
}
1 Contributions as a suffix for a domain service that contributes a number of actions to Borrowers. Note that Borrower could be a (marker) interface, so this functionality is "mixed in" merely by the class (eg LibraryMember) implementing this interface
2 actions contibuted as associations (a collection in this case) must have safe semantics

Another example:

@DomainService(
    nature=NatureOfService.DOMAIN
)
public class LoanRepository {                                  (1)
    @Programmatic                                              (2)
    public List<Loan> findLoansFor(Borrower borrower) { ... }
}
1 Repository as a suffix for a domain-layer service
2 methods on DOMAIN services are often @Programmatic; they will never be exposed in the UI, so there’s little point in including them in Isis' metamodel

A final example:

@DomainService(
    nature=NatureOfService.VIEW_MENU_ONLY
)
public class Loans {                               (1)
    @Action(semantics=SemanticsOf.SAFE)
    public List<Loan> findOverdueLoans() { ... }
    @Inject
    LoanRepository loanRepository;                 (2)
}
1 name is intended to be rendered in the UI
2 it’s common for domain-layer domain services to be injected into presentation layer services (such as VIEW_MENU_ONLY and VIEW_CONTRIBUTIONS_ONLY).

1.11.2. repositoryFor()

The repositoryFor() attribute is intended for domain services (probably with a nature=DOMAIN) that are intended to act as repositories for domain entities.

For example:

@DomainService(
    nature=NatureOfService.DOMAIN,
    repositoryFor=Loan.class
)
public class LoanRepository {
    @Programmatic
    public List<Loan> findLoansFor(Borrower borrower) { ... }
}

Currently the metadata is unused; one planned use is to infer the icon for the domain service from the icon of the nominated entity.

1.12. @DomainServiceLayout

The @DomainServiceLayout annotation applies to domain services, collecting together all view layout semantics within a single annotation.

You will also find some additional material in the object layout chapter.

The table below summarizes the annotation’s attributes.

Table 15. @DomainServiceLayout attributes
Attribute Values (default) Description

menuBar()

PRIMARY, SECONDARY, TERTIARY (PRIMARY).

the menubar in which the menu that holds this service’s actions should reside.

menuOrder()

the order of the service’s menu with respect to other service’s.

named()

string, eg "Customers"

name of this class (overriding the name derived from its name in code)

For example:

@DomainService
@DomainServiceLayout(
    menuBar=MenuBar.PRIMARY,
    menuOrder="100",
    named="ToDos"
)
public class ToDoItems {
    ...
}

Note that there is (currently) no support for specifying UI hints for domain services through the dynamic .layout.json file (only for properties, collections and actions are supported).

1.12.1. menuBar()

The menuBar() attribute is a hint to specify where on the application menu a domain service’s actions should be rendered.

For example:

@DomainService
@DomainServiceLayout(menuBar=MenuBar.PRIMARY)
public class ToDoItems {
    ...
}

In the Wicket viewer, domain services placed on the PRIMARY menu bar appears to the left:

menuBar primary

Domain services placed on the SECONDARY menu bar appear to the right:

menuBar secondary

Domain services placed on the TERTIARY appear in the menu bar associated with the user’s name (far top-right)

menuBar tertiary

The grouping of multiple domain services actions within a single drop-down is managed by the @DomainServiceLayout#menuOrder() attribute.

The RestfulObjects viewer does not support this attribute.

1.12.2. menuOrder()

The menuOrder() attribute determines the ordering of a domain service’s actions as menu items within a specified menu bar and top-level menu.

The algorithm works as follows:

  • first, the menuBar() determines which of the three menu bars the service’s actions should be rendered

  • then, the domain service’s top-level name (typically explicitly specified using named()) is used to determine the top-level menu item to be rendered on the menu bar

  • finally, if there is more than domain service that has the same name, then the menuOrder attribute is used to order those actions on the menu item drop-down.

For example, the screenshot below shows the "prototyping" menu from the (non-ASF) Isis addons' todoapp:

menuOrder

The Wicket viewer automatically places separators between actions from different domain services. From this we can infer that there are actually five different domain services that are all rendered on the "prototyping" top-level menu.

One of these is the todoapp’s DemoDomainEventSubscriptions service:

@DomainService(
        nature = NatureOfService.VIEW_MENU_ONLY
)
@DomainServiceLayout(
        menuBar = MenuBar.SECONDARY,
        named = "Prototyping",                   (1)
        menuOrder = "500.20")                    (2)
public class DemoDomainEventSubscriptions {
    @ActionLayout(named="Set subscriber behaviour")
    @MemberOrder(sequence = "500.20.1")          (3)
    public void subscriberBehaviour(...) { ... }
    ...
}
1 render on the "Prototyping" menu
2 positioning relative to other service’s on the "Prototyping" menu
3 by convention (nothing more) the @MemberOrder#sequence() attribute continues the same Dewey decimal sequence format (a simple string "1" could in fact have been used instead)

while another comes from the (non-ASF) Isis addons' devutils module:

@DomainServiceLayout(
    menuBar = MenuBar.SECONDARY,
    named = "Prototyping",                    (1)
    menuOrder = "500.600"                     (2)
)
public class DeveloperUtilitiesServiceMenu {
    @MemberOrder( sequence = "500.600.1" )    (3)
    public Clob downloadMetaModel() { ... }
    @MemberOrder( sequence = "500.600.2" )
    public Blob downloadLayouts() { ... }
    @ActionLayout(named = "Rebuild Services Meta Model")
    @MemberOrder( sequence = "500.600.3")
    public void refreshServices() { ... }
    ...
}
1 render on the "Prototyping" menu
2 positioning relative to other service’s on the "Prototyping" menu; this appears after the DemoDomainEventSubscriptions service shown above
3 by convention (nothing more) the @MemberOrder#sequence() attribute continues the same Dewey decimal sequence format (a simple string "1", "2", "3", …​ could in fact have been used instead)

1.12.3. named()

The named() attribute explicitly specifies the domain service’s name, overriding the name that would normally be inferred from the Java source code. This attribute can also be specified for actions, collections, properties, parameters, domain objects and view models.

The value of this attribute also has an important role to play in the positioning of the domain service’s actions relative to the actions of other domain services. See menuOrder() for a full discussion with examples.

For example:

@DomainService
@DomainServiceLayout(
    named="Customers"
)
public class CustomerRepository {
   ...
}

1.13. @Facets

The @Facets annotation allows FacetFactory implementations and so can be used to run install arbitrary Facet`s for a type. Generally this is not needed, but can be useful for overriding a custom programming model where a `FacetFactory is not typically included.

FacetFactory is an important internal API that is used by Isis to

1.14. @HomePage

The @HomePage annotation allows a single (no-arg, query-only) action on a single domain service to be nominated as the action to invoke for the default home page. This often returns a view model that acts as some sort of dashboard, presenting key information and makeing the most commonly used actions easy to invoke.

For example, the (non-ASF) Isis addons' todoapp uses @HomePage to return a dashboard of todo items to complete:

HomePage

The corresponding code is:

@DomainService(nature = NatureOfService.DOMAIN)
public class ToDoAppDashboardService  {
    @Action(
            semantics = SemanticsOf.SAFE
    )
    @HomePage
    public ToDoAppDashboard lookup() {
        return container.injectServicesInto(new ToDoAppDashboard());
    }
    @Inject
    private DomainObjectContainer container;
}

where ToDoAppDashboard is:

@DomainObject(nature = Nature.VIEW_MODEL)
public class ToDoAppDashboard {
    public String title() { return "Dashboard"; }

    public List<ToDoItem> getNotYetComplete() { ... }
    public List<ToDoItem> getComplete() { ... }

    public Blob exportToWordDoc() { ... }  (1)
}
1 associated using dynamic layouts with the notYetComplete collection.

The other two actions shown in the above screenshot — exportAsXml and downloadLayout — are actually contributed to the ToDoAppDashboard through various domain services, as is the downloadLayout action.

1.15. @Inject (javax)

Apache Isis automatically injects domain services into other domain services and also into domain objects and view models. In fact, it also injects domain services into integration tests and fixture scripts.

One omission: Isis (currently) does not inject services into o.a.i.applib.spec.Specification instances (as used by @Property#mustSatisfy() and @Parameter#mustSatisfy() annotations.

Isis supports several syntaxes for injecting domain services. The simplest uses the @javax.inject.Inject annotation on the field, as defined in JSR-330.

For example:

public class Customer {
    public List<Order> findRecentOrders() {    (1)
        return orders.recentOrdersFor(this);
    }
    @javax.inject.Inject
    OrderRepository orders;                    (2)
}
1 an alternative implementation would be to implement findRecentOrders() as a contributed action.
2 we recommend default (rather than private) visibility so that unit tests can easily mock out the service

1.15.1. Alternative syntaxes

Isis also supports setter-based injection:

public class Customer {
    ...
    public void setOrderRepository(OrderRepository orderRepository) { ... }
}

and also supports an additional syntax of using inject…​ as the prefix:

public class Customer {
    ...
    public void injectOrderRepository(OrderRepository orderRepository) { ... }
}

Generally we recommend using @javax.inject.Inject; it involves less code, and is more immediately familiar to most Java developers.

1.15.2. Manually injecting services

Isis performs dependency injection when domain entities are recreated. It will also perform dependency injection if an object is created through the DomainObjectContainer.

For example, to create a new (transient) domain object, the idiom is:

Customer cust = container.newTransientInstance(Customer.class);  (1)
// initialize state of "cust"
container.persist(cust);
1 where container is an instance of DomainObjectContainer.

View models are created similarly:

ToDoAppDashboard dashboard = container.newViewModelInstance(ToDoAppDashboard.class);

If you prefer, though, you can simply instantiate domain objects using "new" and then inject domain services manually:

Customer cust = new Customer();
container.injectServicesInto(cust);
// initialize state of "cust"
container.persist(cust);

or if you prefer:

Customer cust = container.injectServicesInto(new Customer());
// initialize state of "cust"
container.persist(cust);

There is one subtle difference between using DomainObjectContainer#newTransientInstance(…​) and DomainObjectContainer#injectServicesInto(…​), in that with the former Isis will automatically initialize all fields to their default values.

This isn’t a particular useful feature (and indeed can sometimes be rather confusing) so you may well wish to standardize on using injectServicesInto(…​) throughout.

1.16. @MemberGroupLayout

The @MemberGroupLayout annotation specifies how an object’s properties and collections are grouped together into columns, also specifying the relative positioning of those columns. It works in conjunction with the @MemberOrder annotation.

The @MemberOrder annotation is used to specify the relative order of domain object members, that is: properties, collections and actions. It works in conjunction with the @MemberGroupLayout annotation.

The annotation defines two attributes, name() and sequence(). Broadly speaking the name() attribute is used to group or associate members together, while the sequence() attribute orders members once they have been grouped.

As this is an important topic, there is a separate chapter that discussed object layout in full.

1.17. @MemberOrder

The @MemberOrder annotation is used to specify the relative order of domain object members, that is: properties, collections and actions. It works in conjunction with the @MemberGroupLayout annotation.

The annotation defines four attributes:

  • columnSpans() — of type int[] — which specifies the relative column sizes of the three columns that render properties as well as a fourth column that renders only collections

  • left() — of type String[] - that specifies the order of the property groups (inferred from @MemberOrder#name()) as applied to properties) in the left-most column

  • middle() — of type String[] - that specifies the order of the property groups (if any) as applied to properties) in the middle column

  • right() — of type String[] - that specifies the order of the property groups (if any) as applied to properties) in the right-most column

Collections are always rendered in the "last" column. This can appear either below the columns holding properties (if their column spans = 12), or can be rendered to the right of the property columns (if the spans of the property columns come to <12 leaving enough room for the span of the collection column).

As this is an important topic, there is a separate chapter that discussed object layout in full.

1.18. @NotPersistent (javax.jdo)

The @javax.jdo.annotation.NotPersistent annotation is used by JDO/DataNucleus to indicate that a property should not be persisted to the database.

Apache Isis also uses this annotation, though (currently) only in the very minimal way to suppress checking of inconsistent metadata between JDO and Isis annotations (eg @Column#allowsNull() vs @Property#optionality(), or @Column#length() and @Property#maxLength()).

Isis parses the @NotPersistent annotation from the Java source code; it does not query the JDO metamodel. This means that it the @NotPersistent annotation must be used rather than the equivalent <field> XML metadata.

Moreover, while JDO/DataNucleus will recognize annotations on either the field or the getter method, Isis (currently) only inspects the getter method. Therefore ensure that the annotation is placed there.

1.19. @MinLength

The @MinLength annotation is used to specify the minimum number of characters in a search of an autoComplete…​() supporting method.

For example:

public ToDoItem add(
        @TypicalLength(20)
        final ToDoItem toDoItem) {
    getDependencies().add(toDoItem);
    return this;
}
public List<ToDoItem> autoComplete0Add(
        final @MinLength(2)
        String search) {
    final List<ToDoItem> list = toDoItems.autoComplete(search);
    list.removeAll(getDependencies());
    list.remove(this);
    return list;
}

The `@Parameter(minLength=…​)`e can also be used (even though strictly speaking the search argument is not a parameter of the action).

1.20. @Parameter

The @Parameter annotation applies to action parameters collecting together all domain semantics within a single annotation.

The table below summarizes the annotation’s attributes.

Table 16. @Paramter attributes
Attribute Values (default) Description

maxLength()

Positive integer

maximum number of characters for string parameters; ignored otherwise

minLength()

Positive integer

Deprecated; use @MinLength instead.

Can be used to specify the minimum length for autoComplete…​() supporting method; but because this _is a supporting method rather than the action method itself, we now feel it is misleading to use the @Parameter annotation in this situation.

mustSatisfy()

implementation of o.a.i.applib.spec.Specification

allows arbitrary validation to be applied

optionality()

MANDATORY, OPTIONAL (MANDATORY)

specifies a parameter is optional rather than mandatory

regexPattern()

regular expression

validates the contents of a string parameter against the regular expression pattern

regexPatternFlags()

value of flags as normally passed to java.util.regex.
Pattern#compile(…​)

modifies the compilation of the regular expression

regexPatternReplacement()

Unused.

For example:

public class Customer {
    public static class EmailSpecification extends AbstractSpecification<String> {
        public String satisfiesSafely(String proposed) {
            return EmailUtil.ensureValidEmail(proposed);    (1)
        }
    }
    @Action(semantics=SemanticsOf.IDEMPOTENT)
    public Customer updateEmail(
        @Parameter(
            maxLength=30,
            mustSatisfy=EmailSpecification.class,
            optionality=Optionality.OPTIONAL,
            regexPattern = "(\\w+\\.)*\\w+@(\\w+\\.)+[A-Za-z]+",
            regexPatternFlags=Pattern.CASE_INSENSITIVE
        )
        @ParameterLayout(named="New Email Address")
        final String newEmailAddress
        ...
    }
}
1 the (fictitious) EmailUtil.ensureValid(…​) (omitted for brevity) returns a string explaining if an email is invalid

1.20.1. maxLength()

The maxLength() attribute applies only to String parameters, indicating the maximum number of characters that the user may enter (for example in a text field in the UI). It is ignored if applied to parameters of any other type. This attribute can also be applied to properties.

For example:

public class CustomerRepository {
    public Customer newCustomer(
        @Parameter(maxLength=30)
        @ParameterLayout(named="First Name")  (1)
        final String firstName,
        @Parameter(maxLength=50)
        @ParameterLayout(named="Last Name")
        final String lastName) {
        ...
    }
}
1 the @ParameterLayout(named=…​) attribute is required for Java 7; for Java 8 it can be omitted if the (non-ASF) Isis addons' paraname8 metamodel extension is used.

1.20.2. mustSatisfy()

The mustSatisfy() attribute allows arbitrary validation to be applied to parameters using an (implementation of a) org.apache.isis.applib.spec.Specification object. The attribute is also supported on properties.

The specification implementations can (of course) be reused between parameters and properties.

The Specification is consulted during validation, being passed the proposed value. If the proposed value fails, then the value returned is the used as the invalidity reason.

For example:

public class StartWithCapitalLetterSpecification
        extends AbstractSpecification<String> {            (1)
    public String satisfiesSafely(String proposed) {
        return "".equals(proposed)
            ? "Empty string"
            : !Character.isUpperCase(proposed.charAt(0))
                ? "Does not start with a capital letter"
                : null;
    }
}
public class CustomerRepository {
    public Customer newCustomer(
            @Parameter(
                mustSatisfy=StartWithCapitalLetterSpecification.class
            )
            @ParameterLayout(named="First Name")
            final String firstName,
            @Parameter(
                mustSatisfy=StartWithCapitalLetterSpecification.class
            )
            @ParameterLayout(named="Last Name")
            final String lastName) {
        ...
    }
    ...
}
1 the AbstractSpecification class conveniently handles type-safety and dealing with null values. The applib also provides SpecificationAnd and SpecificationOr to allow specifications to be combined "algebraically".

1.20.3. optionality()

By default, Apache Isis assumes that all parameters of an action are required (mandatory). The optionality() attribute allows this to be relaxed. The attribute is also supported for properties.

The attribute has no meaning for a primitive type such as int: primitives will always have a default value (e.g. zero). If optionality is required, then use the corresponding wrapper class (e.g. java.lang.Integer) and annotate with Parameter#optionality() as required.

The values for the attribute are simply OPTIONAL or MANDATORY.

For example:

public class Customer {
    public Order placeOrder(
            final Product product,
            @ParameterLayout(named = "Quantity")
            final int quantity,
            @Parameter(optionality = Optionality.OPTIONAL)
            @ParameterLayout(named = "Special Instructions")
            final String instr) {
        ...
    }
    ...
}

1.20.4. regexPattern()

The regexPattern() attribute validates the contents of any string parameter with respect to a regular expression pattern. It is ignored if applied to parameters of any other type. This attribute can also be specified for properties.

The related regexPatternFlags() attribute specifies flags that modify the handling of the pattern. The values are those that would normally be passed to java.util.regex.Pattern#compile(String,int).

For example:

public class Customer {
    public void updateEmail(
            @Parameter(
                regexPattern = "(\\w+\\.)*\\w+@(\\w+\\.)+[A-Za-z]+",
                regexPatternFlags=Pattern.CASE_INSENSITIVE
            )
            @ParameterLayout(named = "Email")
            final String email) {
        ...
    }
)

The related regexPatternReplacement() attribute is currently unused.

1.21. @ParameterLayout

The @ParameterLayout annotation applies to action parameters, collecting together all UI hints within a single annotation.

The table below summarizes the annotation’s attributes.

Table 17. @ParameterLayout attributes
Attribute Values (default) Description

cssClass()

Any string valid as a CSS class

the css class that a parameter should have, to allow more targetted styling in application.css

describedAs()

String

description of this parameter, eg to be rendered in a tooltip.

labelPosition()

LEFT, TOP, RIGHT, NONE
(LEFT)

in forms, the positioning of the label relative to the property value.

Default is LEFT, unless multiLine in which case TOP. The value RIGHT is only supported for boolean parameters.

multiLine()

Positive integer

for string parameters, render as a text area over multiple lines.

If set > 1, then then labelPosition defaults to TOP.

named()

String

the name of this parameter.

For Java 7 this is generally required. For Java 8, the name can often be inferred from the code so this attribute allows the name to be overridden. A typical use case is if the desired name is a reserved Java keyword, such as default or package.

namedEscaped()

true, false (true)

whether to HTML escape the name of this parameter.

renderedAsDayBefore()

for date parameters only, render the date as one day prior to the actually stored date (eg the end date of an open interval into a closed interval)

typicalLength()

the typical entry length of a field, use to determine the optimum width for display

For example:

public class ToDoItem {
    public ToDoItem updateDescription(
                        @ParameterLayout(
                            cssClass="x-key",
                            describedAs="What needs to be done",
                            labelPosition=LabelPosition.LEFT,
                            named="Description of this <i>item</i>",
                            namedEscaped=false,
                            typicalLength=80)
                        final String description) {
        setDescription(description);
        return this;
    }
    ...
}

Note that there is (currently) no support for specifying UI hints for domain services through the dynamic .layout.json file (only for properties, collections and actions are supported).

1.21.1. cssClass()

The cssClass() attribute can be used to render additional CSS classes in the HTML (a wrapping <div>) that represents the action parameter. Application-specific CSS can then be used to target and adjust the UI representation of that particular element.

This attribute can also be applied to domain objects, view models, actions properties, collections and parameters.

For example:

public class ToDoItem {
    public ToDoItem postpone(
            @ParameterLayout(
                named="until",
                cssClass="x-key"
            )
            LocalDate until
        ) { ... }
    ...
}

1.21.2. describedAs()

The describedAs() attribute is used to provide a short description of the action parameter to the user. In the Wicket viewer it is displayed as a 'tool tip'. The describedAs() attribute can also be specified for collections, properties, actions, domain objects and view models.

Descriptions may be provided for objects, members (properties, collections and actions), and for individual parameters within an action method. @DescribedAs therefore works in a very similar manner to @Named <!--(see ?)-→.

To provide a description for an individual action parameter, use the @DescribedAs annotation in-line i.e. immediately before the parameter declaration.

For example:

public class Customer {
    public Order placeOrder(
                      Product product,
                      @ParameterLayout(
                          named="Quantity",
                          describedAs="The quantity of the product being ordered"
                      )
                      int quantity) {
        ...
    }
    ...
}

1.21.3. labelPosition()

The labelPosition() attribute determines the positioning of labels for parameters. This attribute can also be specified for properties.

The positioning of labels is typically LEFT, but can be positioned to the TOP. The one exception is multiLine() string parameters, where the label defaults to TOP automatically (to provide as much real-estate for the multiline text field as possible).

For boolean parameters a positioning of RIGHT is also allowed; this is ignored for all other types.

It is also possible to suppress the label altogether, using NONE.

For example:

public class Order {
    public Order changeStatus(
                    OrderStatus newStatus
                    @Parameter(
                        optionality=Optionality.OPTIONAL
                    )
                    @ParameterLayout(
                        named="Reason",
                        labelPosition=LabelPosition.TOP
                    )
                    String reason) {
        ...
    }
    ...
}

To get an idea of how these are rendered (in the Wicket viewer), see PropertyLayout#labelPosition().

1.21.4. multiLine()

The multiLine() attribute specifies that the text field for a string parameter should span multiple lines. It is ignored for other parameter types. The attribute is also supported for properties.

For example:

public class BugReport {
    public BugReport updateStepsToReproduce(
                        @Parameter(named="Steps")
                        @ParameterLayout(
                            numberOfLines=10
                        )
                        final String steps) {
        ...
    }
    ...
}

If set > 1 (as would normally be the case), then the default labelPosition defaults to TOP (rather than LEFT, as would normally be the case).

1.21.5. named()

The named() attribute explicitly specifies the action parameter’s name. This attribute can also be specified for actions, collections, properties, domain objects, view models and domain services.

Unlike most other aspects of the Isis metamodel, the name of method parameters cannot (prior to Java 8, at least) be inferred from the Java source code. Without other information, Isis uses the object’s type (int, String etc) as the name instead. This can be sufficient for application-specific reference types (eg ToDoItem) but is generally not sufficient for primitives and other value types.

The named() attribute (or the deprecated @Named annotation) is therefore often required to specify the parameter name.

As of Java 8, the Java reflection API has been extended. The (non-ASF) Isis addons' paraname8 metamodel extension provides support for this. Note that your application must (obviously) be running on Java 8, and be compiled with the -parameters compile flag for javac.

By default the name is HTML escaped. To allow HTML markup, set the related namedEscaped() attribute to false.

For example:

public class Customer {
    public Order placeOrder(
            final Product product
           ,@ParameterLayout(named="Quantity")
            final int quantity) {
        Order order = newTransientInstance(Order.class);
        order.modifyCustomer(this);
        order.modifyProduct(product);
        order.setQuantity(quantity);
        return order;
    }
    ...
}

The framework also provides a separate, powerful mechanism for internationalization.

1.21.6. renderedAsDayBefore()

The renderedAsDayBefore() attribute applies only to date parameters whereby the date will be rendered as the day before the value actually held in the domain object. It is ignored for parameters of other types. This attribute is also supported for properties.

This behaviour might at first glance appear odd, but the rationale is to support the use case of a sequence of instances that represent adjacent intervals of time. In such cases there would typically be startDate and endDate properties, eg for all of Q2. Storing this as a half-closed interval — eg [1-Apr-2015, 1-July-2015) — can substantially simplify internal algorithms; the endDate of one interval will correspond to the startDate of the next.

However, from an end-user perspective the requirement may be to render the interval as a fully closed interval; eg the end date should be shown as 30-Jun-2015.

This attribute therefore bridges the gap; it presents the information in a way that makes sense to an end-user, but also stores the domain object in a way that is easy work with internally.

For example:

public class Tenancy {
    public void changeDates(
        @ParameterLayout(named="Start Date")
        LocalDate startDate,
        @ParameterLayout(
            named="End Date",
            renderedAsDayBefore=true
        )
        LocalDate endDate) {
        ...
    }
}

1.21.7. typicalLength()

The typicalLength() attribute indicates the typical length of a string parameter. It is ignored for parameters of other types. The attribute is also supported for properties.

The information is intended as a hint to the UI to determine the space that should be given to render a particular string parameter. That said, note that the Wicket viewer uses the maximum space available for all fields, so in effect ignores this attribute.

For example:

public class Customer {
    public Customer updateName(
                @Parameter(maxLength=30)
                @ParameterLayout(
                    named="First name",
                    typicalLength=20
                )
                final String firstName,
                @Parameter(maxLength=30)
                @ParameterLayout(
                    named="Last name",
                    typicalLength=20
                )
                final String lastName) {
        ...
    }
    ...
}

1.22. @PersistenceCapable (javax.jdo)

The @javax.jdo.annotation.PersistenceCapable is used by JDO/DataNucleus to indicate that a class is a domain entity to be persisted to the database.

Apache Isis also checks for this annotation, and if the @PersistenceCapable#schema() attribute is present will use it to form the object type.

Isis parses the @PersistenceCapable annotation from the Java source code; it does not query the JDO metamodel. This means that it the @PersistenceCapable annotation must be used rather than the equivalent <class> XML metadata.

Moreover, while JDO/DataNucleus will recognize annotations on either the field or the getter method, Isis (currently) only inspects the getter method. Therefore ensure that the annotation is placed there.

The object type is used internally by Isis to generate a string representation of an objects identity (the Oid). This can appear in several contexts, including:

The actual format of the object type used by Isis for the concatenation of schema() and @PersistenceCapable#table(). If the table() is not present, then the class' simple name is used instead.

For example:

@javax.jdo.annotations.PersistenceCapable(schema="custmgmt")
public class Customer {
    ...
}

has an object type of custmgmt.Customer, while:

@javax.jdo.annotations.PersistenceCapable(schema="custmgmt", table="Address")
public class AddressImpl {
    ...
}

has an object type of custmgmt.Address.

On the other hand:

@javax.jdo.annotations.PersistenceCapable(table="Address")
public class AddressImpl {
    ...
}

does not correspond to an object type, because the schema() attribute is missing.

If the object type has not been specified, then Isis will use the fully qualified class name of the entity.

This might be obvious, but to make explicit: we recommend that you use namespaces of some form or other.

If the object type has not been specified (by either this annotation, or by @Discriminator, or by Isis' own @DomainObject#objectType() annotation/attribute), then (as noted above) Isis will use the fully qualified class name of the entity.

However, chances are that the fully qualified class name is liable to change over time, for example if the code is refactored or (more fundamentally) if your company/organization reorganizes/renames itself/is acquired.

Isis' recognition of @PersistenceCapable#schema() makes namespacing of object types comparatively trivial, and moreover aligns the namespacing with the way in which the tables in the database are namespaced by the database schema.

We therefore strongly recommend that you specify an object type for all entities, one way or another. Using @PersistenceCapable#schema() is probably the best choice in most cases.

If the object type is not unique across all domain classes then the framework will fail-fast and fail to boot. An error message will be printed in the log to help you determine which classes have duplicate object tyoes.

1.23. @PostConstruct (javax)

The @javax.annotation.PostConstruct annotation, as defined in JSR-250, is recognized by Apache Isis as a callback method on domain services to be called just after they have been constructed, in order that they initialize themselves.

For the default application-scoped (singleton) domain services, this means that the method, if present, is called during the bootstrapping of the application. For @RequestScoped domain services, the method is called at the beginning of the request.

The signature of the method is:

@PostConstruct
@Programmatic                          (1)
public void postConstruct() { ... }    (2)
1 The method should be annotated using @Programmatic so that it is not interpreted as an action.
2 the method can have any name, but must have public visibility.

In the form shown above the method can either accept no arguments. Alternatively, the method can accept a parameter of type Map<String,String>:

@PostConstruct
@Programmatic
public void postConstruct(Map<String,String> properties) { ... }

Isis uses argument to pass in the configuration properties read from all configuration files:

Alternatively, you could inject DomainObjectContainer into the service and read configuration properties using DomainObjectContainer#getProperty(…​) and related methods.

A common use case is for domain services that interact with the EventBusService. For example:

@DomainService(nature=NatureOfService.DOMAIN)
public class MySubscribingService {
    @Programmatic
    @PostConstruct
    public void postConstruct() {
        eventBusService.register(this);
    }
    @Programmatic
    @PreDestroy
    public void preDestroy() {
        eventBusService.unregister(this);
    }
    ...
    @javax.inject.Inject
    EventBusService eventBusService;
}

Other use cases include obtaining connections to external datasources, eg subscribing to an ActiveMQ router, say, or initializing/cleaning up a background scheduler such as Quartz.

See also @PreDestroy

1.24. @PreDestroy (javax)

The @javax.annotation.PreDestroy annotation, as defined in JSR-250, recognized by Apache Isis as a callback method on domain services to be called just as they go out of scope.

For the default application-scoped (singleton) domain services, this means that the method, if present, is called just prior to the termination of the application. For @RequestScoped domain services, the method is called at the end of the request.

The signature of the method is:

@PreDestroy
@Programmatic                       (1)
public void preDestroy() { ... }    (2)
1 The method should be annotated using @Programmatic so that it is not interpreted as an action.
2 the method can have any name, but must have public visibility, and accept no arguments.

A common use case is for domain services that interact with the EventBusService. For example:

@DomainService(nature=NatureOfService.DOMAIN)
public class MySubscribingService {
    @Programmatic
    @PostConstruct
    public void postConstruct() {
        eventBusService.register(this);
    }
    @Programmatic
    @PreDestroy
    public void preDestroy() {
        eventBusService.unregister(this);
    }
    ...
    @javax.inject.Inject
    EventBusService eventBusService;
}

Other use cases include obtaining connections to external datasources, eg subscribing to an ActiveMQ router, say, or initializing/cleaning up a background scheduler such as Quartz.

See also @PostConstruct

1.25. @PrimaryKey (javax.jdo)

The @javax.jdo.annotation.PrimaryKey annotation is used by JDO/DataNucleus to indicate that a property is used as the primary key for an entity with application-managed identity.

Apache Isis also uses this annotation in a very minimal way: to ensure that the framework’s own logic to initialize newly instantiated objects (eg using DomainObjectContainer#newTransientInstance(…​) does not touch the primary key, and also to ensure that the primary key property is always disabled (read-only).

Isis parses the @NotPersistent annotation from the Java source code; it does not query the JDO metamodel. This means that it the @NotPersistent annotation must be used rather than the equivalent <field> XML metadata.

Moreover, while JDO/DataNucleus will recognize annotations on either the field or the getter method, Isis (currently) only inspects the getter method. Therefore ensure that the annotation is placed there.

1.26. @Programmatic

The @Programmatic annotation causes the method to be excluded completely from the Apache Isis metamodel. This means it won’t appear in any UI, and it won’t appear in any mementos or snapshots.

A common use-case is to ignore implementation-level artifacts. For example:

public class Customer implements Comparable<Customer> {
    ...
    @Programmatic
    public int compareTo(Customer c) {
        return getSalary() - c.getSalary();
    }
    ...
}

Note that @Programmatic does not simply imply @Hidden; it actually means that the class member will not be part of the Isis metamodel.

1.27. @Property

The @Property annotation applies to properties collecting together all domain semantics within a single annotation.

It is also possible to apply the annotation to actions of domain services that are acting as contributed properties.

Table 18. @Property attributes
Attribute Values (default) Description

domainEvent()

subtype of PropertyDomainEvent
(PropertyDomainEvent.Default)

the event type to be posted to the EventBusService to broadcast the property’s business rule checking (hide, disable, validate) and its modification (before and after).

editing()

ENABLED, DISABLED, AS_CONFIGURED
(AS_CONFIGURED)

whether a property can be modified or cleared from within the UI

hidden()

EVERYWHERE, OBJECT_FORMS, PARENTED_TABLES, STANDALONE_TABLES, ALL_TABLES, NOWHERE
(NOWHERE)

indicates where (in the UI) the property should be hidden from the user.

maxLength()

maximum number of characters for string parameters; ignored otherwise

In many/most cases you should however use @Column#length()

mustSatisfy()

implementation of o.a.i.applib.spec.Specification

allows arbitrary validation to be applied

notPersisted()

true, false
(false)

whether to exclude from snapshots.

Property must also be annotated with @javax.jdo.annotations.NotPersistent in order to not be persisted.

optionality()

specifies a property is optional rather than mandatory

In many/most cases you should however use @Column#allowNulls()

regexPattern()

regular expression

validates the contents of a string parameter against the regular expression pattern

regexPatternFlags()

value of flags as normally passed to java.util.regex.
Pattern#compile(…​)

modifies the compilation of the regular expression

For example:

@DomainObject
public class Customer {
    public static class EmailSpecification extends AbstractSpecification<String> {
        public String satisfiesSafely(String proposed) {
            return EmailUtil.ensureValidEmail(proposed);    (1)
        }
    }
    @javax.jdo.annotations.Column(allowNulls="true")                (2)
    @Property(
        maxLength=30,
        minLength=5,
        mustSatisfy=EmailSpecification.class,
        regexPattern = "(\\w+\\.)*\\w+@(\\w+\\.)+[A-Za-z]+",
        regexPatternFlags=Pattern.CASE_INSENSITIVE
    )
    public String getEmailAddress() { ... }
    public void setEmailAddress(String emailAddress) { ... }
    ...
}
1 the (fictitious) EmailUtil.ensureValid(…​) (omitted for brevity) returns a string explaining if an email is invalid
2 generally use instead of the @Property#optionality() attribute

1.27.1. domainEvent()

Whenever a domain object (or list of domain objects) is to be rendered, the framework fires off multiple domain events for every property, collection and action of the domain object. In the cases of the domain object’s properties, the events that are fired are:

  • hide phase: to check that the property is visible (has not been hidden)

  • disable phase: to check that the property is usable (has not been disabled)

  • validate phase: to check that the property’s arguments are valid (to modify/clear its value)

  • pre-execute phase: before the modification of the property

  • post-execute: after the modification of the property

Subscribers subscribe through the EventBusService using either Guava or Axon Framework annotations and can influence each of these phases.

By default the event raised is PropertyDomainEvent.Default. For example:

public class ToDoItem {
    @Property()
    public LocalDate getDueBy() { ... }
    ...
}

The purpose of the domainEvent() attribute is to allows a custom subclass to be emitted instead. This attribute is also supported for actions and properties.

For example:

public class ToDoItem {
    public static class DueByChangedEvent extends PropertyDomainEvent<ToDoItem, LocalDate> { (1)
        private static final long serialVersionUID = 1L;
        public DueByChangedEvent(                                                            (2)
                ToDoItem source,                                                             (3)
                Identifier identifier,                                                       (4)
                LocalDate oldValue,                                                          (5)
                LocalDate newValue) {                                                        (6)
            super(source, identifier, oldValue, newValue);
        }
    }
    @Property(domainEvent=ToDoItem.DueByChangedEvent)
    public LocalDate getDueBy() { ... }
    ...
}
1 inherit from PropertyDomainEvent<T,P> where T is the type of the domain object being interacted with, and P is the type of the property (LocalDate in this example)
2 constructor called reflectively by the framework
3 populated with the object being interacted with (the source of the event)
4 identifier of the property
5 current value of the property
6 proposed new value for the property; if being cleared then will be null.

The benefit is that subscribers can be more targetted as to the events that they subscribe to.

Subscribers

Subscribers (which must be domain services) subscribe using either the Guava API or (if the EventBusService has been appropriately configured) using the Axon Framework API. The examples below use the Guava API.

Subscribers can be either coarse-grained (if they subscribe to the top-level event type):

@DomainService
public class SomeSubscriber {

    @Programmatic
    @com.google.common.eventbus.Subscribe
    public void on(PropertyInteractionEvent ev) {

        ...
    }

}

or can be fine-grained (by subscribing to specific event subtypes):

@DomainService
public class SomeSubscriber {

    @Programmatic
    @com.google.common.eventbus.Subscribe
    public void on(ToDoItem.DueByChangedEvent ev) {

        ...
    }

}

If the AxonFramework is being used, replace @com.google.common.eventbus.Subscribe with @org.axonframework.eventhandling.annotation.EventHandler.

The subscriber’s method is called (up to) 5 times:

  • whether to veto visibility (hide)

  • whether to veto usability (disable)

  • whether to veto execution (validate)

  • steps to perform prior to the property being modified

  • steps to perform after the property has been modified.

The subscriber can distinguish these by calling ev.getPhase(). Thus the general form is:

@Programmatic
@com.google.common.eventbus.Subscribe
public void on(PropertyDomainEvent ev) {
    switch(ev.getPhase()) {
        case HIDE:
            // call ev.hide() or ev.veto("") to hide the property
            break;
        case DISABLE:
            // call ev.disable("...") or ev.veto("...") to disable the property
            break;
        case VALIDATE:
            // call ev.invalidate("...") or ev.veto("...")
            // if proposed property value is invalid
            break;
        case EXECUTING:
            break;
        case EXECUTED:
            break;
    }
}

It is also possible to abort the transaction during the executing or executed phases by throwing an exception. If the exception is a subtype of RecoverableException then the exception will be rendered as a user-friendly warning (eg Growl/toast) rather than an error.

Raising events programmatically

Normally events are only raised for interactions through the UI. However, events can be raised programmatically by wrapping the target object using the WrapperFactory service.

1.27.2. editing()

The editing() attribute can be used to prevent a property from being modified or cleared, ie to make it read-only. This attribute can also be specified for collections, and can also be specified for the domain object.

The related editingDisabledReason() attribute specifies the a hard-coded reason why the property cannot be modified directly.

Whether a property is enabled or disabled depends upon these factors:

  • whether the domain object has been configured as immutable through the @DomainObject#editing() attribute

  • else (that is, if the domain object’s editability is specified as being AS_CONFIGURED), then the value of the configuration property isis.objects.editing. If set to false, then the object’s properties (and collections) are _not editable

  • else, then the value of the @Property(editing=…​) attribute itself

  • else, the result of invoking any supporting disable…​() supporting methods

Thus, to make a property read-only even if the object would otherwise be editable, use:

public class Customer {
    @Property(
        editing=Editing.DISABLED,
        editingDisabledReason="The credit rating is derived from multiple factors"
    )
    public int getInitialCreditRating(){ ... }
    public void setInitialCreditRating(int initialCreditRating) { ... }
}

To reiterate, it is not possible to enable editing for a property if editing has been disabled at the object-level.

1.27.3. hidden()

Properties can be hidden at the domain-level, indicating that they are not visible to the end-user. This attribute can also be applied to actions and collections.

It is also possible to use @Property#hidden() to hide an action at the domain layer. Both options are provided with a view that in the future the view-layer semantics may be under the control of (expert) users, whereas domain-layer semantics should never be overridden or modified by the user.

For example:

public class Customer {
    @Property(hidden=Where.EVERYWHERE)
    public int getInternalId() { ... }
    @Property(hidden=Where.ALL_TABLES)
    public void updateStatus() { ... }
    ...
}

The acceptable values for the where parameter are:

  • Where.EVERYWHERE or Where.ANYWHERE

    The property should be hidden everywhere.

  • Where.ANYWHERE

    Synonym for everywhere.

  • Where.OBJECT_FORMS

    The property should be hidden when displayed within an object form.

  • Where.PARENTED_TABLES

    The property should be hidden when displayed as a column of a table within a parent object’s collection.

  • Where.STANDALONE_TABLES

    The property should be hidden when displayed as a column of a table showing a standalone list of objects, for example as returned by a repository query.

  • Where.ALL_TABLES

    The property should be hidden when displayed as a column of a table, either an object’s * collection or a standalone list. This combines PARENTED_TABLES and STANDALONE_TABLES.

  • Where.NOWHERE

    The property should not be hidden, overriding any other metadata/conventions that would normally cause the property to be hidden.

For example, if a property is annotated with @Title, then normally this should be hidden from all tables. Annotating with @Property(where=Where.NOWHERE) overrides this.

The RestfulObjects viewer has only partial support for these Where enums.

1.27.4. maxLength()

The maxLength() attribute applies only to String properties, indicating the maximum number of characters that the user may enter (for example in a text field in the UI). The attribute It is ignored if applied to properties of any other type. This attribute can also be applied to parameters.

That said, properties are most commonly defined on persistent domain objects (entities), in which case the JDO @Column will in any case need to be specified. Isis can infer the maxLength semantic directly from the equivalent @Column#length() annotation/attribute.

For example:

public class Customer {
    @javax.jdo.annotations.Column(length=30)
    public String getFirstName() { ... }
    public void setFirstName(String firstName) { ... }
    ...
}

In this case there is therefore no need for the @Property#maxLength() attribute.

Non-persistent properties

Of course, not every property is persistent (it could instead be derived), and neither is every domain object an entity (it could be a view model). For these non persistable properties the maxLength() attribute is still required.

For example:

public class Customer {
    @javax.jdo.annotation.NotPersistent                    (1)
    @Property(maxLength=100)
    public String getFullName() { ... }                    (2)
    public void setFullName(String fullName) { ... }       (3)
    ...
}
1 a non persisted (derived) property
2 implementation would most likely derive full name from constituent parts (eg first name, middle initial, last name)
3 implementation would most likely parse the input and update the constituent parts

1.27.5. mustSatisfy()

The mustSatisfy() attribute allows arbitrary validation to be applied to properties using an (implementation of a) org.apache.isis.applib.spec.Specification object. The attribute is also supported on parameters.

The specification implementations can (of course) be reused between properties and parameters.

The Specification is consulted during validation, being passed the proposed value. If the proposed value fails, then the value returned is the used as the invalidity reason.

For example:

public class StartWithCapitalLetterSpecification
        extends AbstractSpecification<String> {            (1)
    public String satisfiesSafely(String proposed) {
        return "".equals(proposed)
            ? "Empty string"
            : !Character.isUpperCase(proposed.charAt(0))
                ? "Does not start with a capital letter"
                : null;
    }
}
public class Customer {
    @MustSatisfy(StartWithCapitalLetterSpecification.class)
    public String getFirstName() { ... }
    ...
}
1 the AbstractSpecification class conveniently handles type-safety and dealing with null values. The applib also provides SpecificationAnd and SpecificationOr to allow specifications to be combined "algebraically".

1.27.6. notPersisted()

The (somewhat misnamed) notPersisted() attribute indicates that the collection should be excluded from any snapshots generated by the XmlSnapshotService. This attribute is also supported for collections.

This annotation does not specify that a property is not persisted in the JDO/DataNucleus objectstore. See below for details as to how to additionally annotate the property for this.

For example:

public class Order {
    @Property(notPersisted=true)
    public Order getPreviousOrder() {...}
    public void setPreviousOrder(Order previousOrder) {...}
    ...
}

Historically this annotation also hinted as to whether the property’s value contents should be persisted in the object store. However, the JDO/DataNucleus objectstore does not recognize this annotation. Thus, to ensure that a property is actually not persisted, it should also be annotated with @javax.jdo.annotations.NotPersistent.

For example:

public class Order {
    @Property(notPersisted=true)               (1)
    @javax.jdo.annotations.NotPersistent       (2)
    public Order getPreviousOrder() {...}
    public void setPreviousOrder(Order previousOrder) {...}
    ...
}
1 ignored by Isis
2 ignored by JDO/DataNucleus

Alternatively, if the property is derived, then providing only a "getter" will also work:

public class Order {
    public Order getPreviousOrder() {...}
    ...
}

1.27.7. optionality()

By default, Apache Isis assumes that all properties of an domain object or view model are required (mandatory). The optionality() attribute allows this to be relaxed. The attribute is also supported for parameters.

That said, properties are most commonly defined on persistent domain objects (entities), in which case the JDO @Column should be specified. Isis can infer the maxLength directly from the equivalent @Column#length() annotation.

That said, properties are most commonly defined on persistent domain objects (entities), in which case the JDO @Column will in any case need to be specified. Isis can infer the optionality semantic directly from the equivalent @Column#allowNulls() annotation/attribute.

For example:

public class Customer {
    @javax.jdo.annotations.Column(allowNulls="true")
    public String getMiddleInitial() { ... }
    public void setMiddleInitial(String middleInitial) { ... }
    ...
}

In this case there is no need for the @Property#optionality() attribute.

Mismatched defaults

If the @Column#allowNulls() attribute is omitted and the `@Property#optionality() attribute is also omitted, then note that Isis' defaults and JDO’s defaults differ. Specifically, Isis always assumes properties are mandatory, whereas JDO specifies that primitives are mandatory, but all reference types are optional.

When Isis initializes it checks for these mismatches during its metamodel validation phase, and will fail to boot ("fail-fast") if there is a mismatch. The fix is usually to add the @Column#allowsNulls() annotation/attribute.

Superclass inheritance type

There is one case (at least) it may be necessary to annotate the property with both @Column#allowsNull and also @Property#optionality(). If the property is logically mandatory and is in a subclass, but the mapping of the class hierarchy is to store both the superclass and subclass(es) into a single table (ie a "roll-up" mapping using javax.jdo.annotations.InheritanceStrategy#SUPERCLASS_TABLE), then JDO requires that the property is annotated as @Column#allowsNull="true": its value will be not defined for other subclasses.

In this case we therefore require both annotations.

@javax.jdo.annotations.PersistenceCapable
@javax.jdo.annotations.Inheritance(strategy = InheritanceStrategy.NEW_TABLE)
public abstract class PaymentMethod {
    ...
}
@javax.jdo.annotations.PersistenceCapable
@javax.jdo.annotations.Inheritance(strategy = InheritanceStrategy.SUPERCLASS_TABLE)
public class CreditCardPaymentMethod extends PaymentMethod {
    private String cardNumber;
    @javax.jdo.annotations.Column(allowNulls="true")
    @Property(optionality=Optionality.MANDATORY)
    public String getCardNumber() { return this.cardNumber; }
    public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; }
    ...
}

Alternatively, you could rely on the fact that Isis never looks at fields (whereas JDO does) and move the JDO annotation to the field:

@javax.jdo.annotations.PersistenceCapable
@javax.jdo.annotations.Inheritance(strategy = InheritanceStrategy.SUPERCLASS_TABLE)
public class CreditCardPaymentMethod extends PaymentMethod {
    @javax.jdo.annotations.Column(allowNulls="true")
    private String cardNumber;
    public String getCardNumber() { return this.cardNumber; }
    public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; }
    ...
}

However this at first glance this might be read as eing that the property is optional whereas Isis' default (required) applies. Also, in the future Isis may be extended to support reading annotations from fields.

Non-persistent properties

Of course, not every property is persistent (it could instead be derived), and neither is every domain object an entity (it could be a view model). For these non persistable properties the optionality() attribute is still required.

For example:

public class Customer {
    @javax.jdo.annotation.NotPersistent                    (1)
    @Property(optionality=Optionality.OPTIONAL)
    public String getFullName() { ... }                    (2)
    public void setFullName(String fullName) { ... }       (3)
    ...
}
1 a non persisted (derived) property
2 implementation would most likely derive full name from constituent parts (eg first name, middle initial, last name)
3 implementation would most likely parse the input and update the constituent parts

The attribute has no meaning for a primitive type such as int: primitives will always have a default value (e.g. zero). If optionality is required, then use the corresponding wrapper class (e.g. java.lang.Integer) and annotate with Parameter#optionality() as required.

The values for the attribute are simply OPTIONAL or MANDATORY.

For example:

public class Customer {
    public Order placeOrder(
            final Product product,
            @ParameterLayout(named = "Quantity")
            final int quantity,
            @Parameter(optionality = Optionality.OPTIONAL)
            @ParameterLayout(named = "Special Instructions")
            final String instr) {
        ...
    }
    ...
}

1.27.8. regexPattern()

The regexPattern() attribute validates the contents of any string property with respect to a regular expression pattern. It is ignored if applied to properties of any other type. This attribute can also be specified for parameters.

For example:

public class Customer {
    @Property(
        regexPattern = "(\\w+\\.)*\\w+@(\\w+\\.)+[A-Za-z]+",
        regexPatternFlags=Pattern.CASE_INSENSITIVE
    )
    public String getEmail() { ... }
}

The related regexPatternFlags() attribute specifies flags that modify the handling of the pattern. The values are those that would normally be passed to java.util.regex.Pattern#compile(String,int).

1.28. @PropertyLayout

The @PropertyLayout annotation applies to properties collecting together all UI hints within a single annotation.

The table below summarizes the annotation’s attributes.

Table 19. @PropertyLayout attributes
Attribute Values (default) Description

cssClass()

Any string valid as a CSS class

the css class that a property should have, to allow more targetted styling in application.css

describedAs()

String

description of this property, eg to be rendered in a tooltip.

hidden()

EVERYWHERE, OBJECT_FORMS, PARENTED_TABLES, STANDALONE_TABLES, ALL_TABLES, NOWHERE
(NOWHERE)

indicates where (in the UI) the property should be hidden from the user.

labelPosition()

LEFT, TOP, RIGHT, NONE
(LEFT)

in forms, the positioning of the label relative to the property value.

Defaults is LEFT, unless multiLine in which case TOP. The value RIGHT is only supported for boolean properties.

It is also possible to change the default through a configuration property

multiLine()

Positive integer

for string properties, render as a text area over multiple lines.

If set > 1, then labelPosition defaults to TOP.

named()

String

to override the name inferred from the collection’s name in code.

A typical use case is if the desired name is a reserved Java keyword, such as default or package.

namedEscaped()

true, false
(true)

whether to HTML escape the name of this property.

renderedAsDayBefore()

true, false
(false)

for date properties only, render the date as one day prior to the actually stored date.

typicalLength()

Positive integer.

the typical entry length of a field, use to determine the optimum width for display

For example:

public class ToDoItem {
    @PropertyLayout(
        cssClass="x-key",
        named="Description of this <i>item</i>",
        namedEscaped=false,
        describedAs="What needs to be done",
        labelPosition=LabelPosition.LEFT,
        typicalLength=80
    )
    public String getDescription() { ... }
    ...
}

It is also possible to apply the annotation to actions of domain services that are acting as contributed properties.

As an alternative to using the @PropertyLayout annotation, a dynamic layout using .layout.json file can be specified; for example:

"description": {
    "propertyLayout": {
        "cssClass": "x-key",
        "describedAs": "What needs to be done",
        "labelPosition": "LEFT",
        "typicalLength": 80
    }
}

1.28.1. cssClass()

The cssClass() attribute can be used to render additional CSS classes in the HTML (a wrapping <div>) that represents the property. Application-specific CSS can then be used to target and adjust the UI representation of that particular element.

This attribute can also be applied to domain objects, view models, actions collections and parameters.

For example:

public class ToDoItem {
    @PropertyLayout(cssClass="x-key")
    public LocalDate getDueBy() { ... }
}

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"dueBy": {
    "propertyLayout": { "cssClass": "x-key" }
}

1.28.2. describedAs()

The describedAs() attribute is used to provide a short description of the property to the user. In the Wicket viewer it is displayed as a 'tool tip'. The attribute can also be specified for collections, actions, parameters, domain objects and view models.

For example:

public class Customer {
    @DescribedAs("The name that the customer has indicated that they wish to be " +
                 "addressed as (e.g. Johnny rather than Jonathan)")
    public String getFirstName() { ... }
}

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"firstName:" {
    "propertyLayout": {
        "describedAs": "The name that the customer has indicated that they wish to be addressed as (e.g. Johnny rather than Jonathan)"
     }
}

1.28.3. labelPosition()

The labelPosition() attribute determines the positioning of labels for properties. This attribute can also be specified for parameters.

The positioning of labels is typically LEFT, but can be positioned to the TOP. The one exception is multiLine() string properties, where the label defaults to TOP automatically (to provide as much real-estate for the multiline text field as possible).

For boolean properties a positioning of RIGHT is also allowed; this is ignored for all other types.

It is also possible to suppress the label altogether, using NONE.

For example:

public class ToDoItem {
    @PropertyLayout(
        labelPosition=LabelPosition.TOP
    )
    public String getDescription() { ... }
    public void setDescription(String description) { ... }
    ...
}

To get an idea of how these are rendered (in the Wicket viewer), we can look at the (non-ASF) Isis addons' todoapp that happens to have examples of most of these various label positions.

The default LEFT label positioning is used by the cost property:

labelPosition LEFT

The TOP label positioning is used by the category property:

labelPosition TOP

Labels are suppressed, using NONE, for the subcategory property:

labelPosition NONE

The todoapp’s complete (boolean) property renders the label to the LEFT (the default):

labelPosition boolean LEFT

Moving the label to the RIGHT looks like:

labelPosition boolean RIGHT

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"description": {
    "propertyLayout": {
        "labelPosition": "TOP"
    }
}
Specifying a default setting for label positions

If you want a consistent look-n-feel throughout the app, eg all property labels to the top, then it’d be rather frustrating to have to annotate every property.

Instead, a default can be specified using a configuration property in isis.properties:

isis.viewers.propertyLayout.labelPosition=TOP

or

isis.viewers.propertyLayout.labelPosition=LEFT

If these are not present then Isis will render according to internal defaults. At the time of writing, this means labels are to the left for all datatypes except multiline strings.

1.28.4. multiLine()

The multiLine() attribute specifies that the text field for a string property should span multiple lines. It is ignored for other property types. The attribute is also supported for parameters.

For example:

public class BugReport {
    @PropertyLayout(
        numberOfLines=10
    )
    public String getStepsToReproduce() { ... }
    public void setStepsToReproduce(String stepsToReproduce) { ... }
    ...
}

Here the stepsToReproduce will be displayed in a text area of 10 rows.

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"stepsToReproduce": {
    "propertyLayout": {
        "numberOfLines": 10
    }
}

If set > 1 (as would normally be the case), then the default labelPosition defaults to TOP (rather than LEFT, as would normally be the case).

1.28.5. named()

The named() attribute explicitly specifies the property’s name, overriding the name that would normally be inferred from the Java source code. This attribute can also be specified for actions, collections, parameters, domain objects, view models and domain services.

Following the don’t repeat yourself principle, we recommend that you only use this attribute when the desired name cannot be used in Java source code. Examples of that include a name that would be a reserved Java keyword (eg "package"), or a name that has punctuation, eg apostrophes.

By default the name is HTML escaped. To allow HTML markup, set the related namedEscaped() attribute to false.

For example:

public class ToDoItem {
    @PropertyLayout(
        named="Description of this <i>item</i>",
        namedEscaped=false
    )
    public String getDescription() { ... }
    ...
}

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"description": {
    "propertyLayout": {
        "named": "Description of this <i>item</i>",
        "namedEscaped": false
    }
}

The framework also provides a separate, powerful mechanism for internationalization.

1.28.6. renderedAsDayBefore()

The renderedAsDayBefore() attribute applies only to date properties whereby the date will be rendered as the day before the value actually held in the domain object. It is ignored for properties of other types. This attribute is also supported for parameters.

This behaviour might at first glance appear odd, but the rationale is to support the use case of a sequence of instances that represent adjacent intervals of time. In such cases there would typically be startDate and endDate properties, eg for all of Q2. Storing this as a half-closed interval — eg [1-Apr-2015, 1-July-2015) — can substantially simplify internal algorithms; the endDate of one interval will correspond to the startDate of the next.

However, from an end-user perspective the requirement may be to render the interval as a fully closed interval; eg the end date should be shown as 30-Jun-2015.

This attribute therefore bridges the gap; it presents the information in a way that makes sense to an end-user, but also stores the domain object in a way that is easy work with internally.

For example:

public class Tenancy {
    public LocalDate getStartDate() { ... }
    public void setStartDate(LocalDate startDate) { ... }
    @PropertyLayout(
        renderedAsDayBefore=true
    )
    public LocalDate getEndDate() { ... }
    public void setEndDate(LocalDate EndDate) { ... }
    ...
}

As an alternative to using the annotation, the dynamic .layout.json can be used instead, eg:

"endDate": {
    "propertyLayout": {
        "renderedAsDayBefore": true
    }
}

1.28.7. typicalLength()

The typicalLength() attribute indicates the typical length of a string property. It is ignored for properties of other types. The attribute is also supported for parameters.

The information is intended as a hint to the UI to determine the space that should be given to render a particular string property. That said, note that the Wicket viewer uses the maximum space available for all fields, so in effect ignores this attribute.

For example:

public class Customer {
    @javax.jdo.annotations.Column(length=30)
    @ParameterLayout(typicalLength=20)
    public String getFirstName() { ... }
    public void setFirstName(String firstName) { ... }
    ...
}

1.29. @RequestScoped (javax)

The @javax.enterprise.context.RequestScoped JSR-299 CDI annotation is used to specify that a domain service should be request-scoped rather than a singleton.

Although Isis does not (currently) leverage CDI, the semantics are the same as request-scoped service; a new instance is created for each HTTP request, reserved for the exclusive use of all objects interacted with during that request.

One of the built-in domain services that uses this annotation is Scratchpad, intended to allow the arbitrary sharing of data between objects. Here is the full source code of this service is:

@DomainService(
        nature = NatureOfService.DOMAIN
)
@RequestScoped
public class Scratchpad {
    private final Map<Object, Object> userData = Maps.newHashMap();   (1)
    @Programmatic
    public Object get(Object key) {
        return userData.get(key);                                     (2)
    }
    @Programmatic
    public void put(Object key, Object value) {
        userData.put(key, value);                                     (3)
    }
    @Programmatic
    public void clear() {
        userData.clear();                                             (4)
    }
}
1 Provides a mechanism for each object being acted upon to pass data to the next object.
2 Obtain user-data, as set by a previous object being acted upon.
3 Set user-data, for the use of a subsequent object being acted upon.
4 Clear any user data.

The vast majority of domain services in Isis tend to be singletons (which requires no special annotation); but as you can see setting up request-scoped services is very straightforward.

Behind the covers Isis creates a (singleton) wrapper for the domain service; the individual request-scoped instances are held in a thread-local of this wrapper. One consequence of this implementation is that request-scoped methods should not be marked as final.

1.30. @Title

The @Title annotation is used to indicate which property or properties make up the object title. If more than one property is used, the order can be specified (using the same Dewey-decimal notation as used by @MemberOrder) and the string to use between the components can also be specified.

For example:

public void Customer {
    @Title(sequence="1.0")
    public String lastName() { ... }
    ...
    @Title(sequence="1.5", prepend=", ")
    public String firstName() { ... }
    ...
    @Title(sequence="1.7", append=".")
    public String midInitial() { ... }
    ...
}

could be used to create names of the style "Bloggs, Joe K."

It is also possible to annotate reference properties; in this case the title will return the title of the referenced object (rather than, say, its string representation).

An additional convention for @Title properties is that they are hidden in tables (in other words, it implies @Hidden(where=Where.ALL_TABLES). For viewers that support this annotation (for example, the Wicket viewer), this convention excludes any properties whose value is already present in the title column. This convention can be overridden using @Hidden(where=Where.NOWHERE).

1.31. @ViewModel

The @ViewModel annotation, applied to a class, is the simplest way to indicate that the class is a view model.

View models are not persisted to the database, instead their state is encoded within their identity (ultimately represented in the URL). As such, view models are immutable.

For example:

@ViewModel
public class MyViewModel {
    public MyViewModel() {}   (1)
    ...
}
1 must have a no-arg constructor for subsequent "recreation" by the framework.

To instantiate a view model, you can either instantiate directly using its constructor, or indirectly using DomainObjectContainer#newTransientInstance(). If you use the former, also programmatically call DomainObjectContainer#injectServicesInto(…​) to ensure that any dependencies are injected into the service.

Note that there is a DomainObjectContainer#newViewModelInstance(.); this is for view models that implement ViewModel interface and can be safely ignored.

The view model’s memento will be derived from the value of the view model object’s properties. Any @Property#notPersisted() properties will be excluded from the memento, as will any @Programmatic properties. Properties that are merely @Hidden are included in the memento.

Only properties supported by the configured MementoService can be used. The default implementation supports all the value types and persisted entities.

(As of 1.8.0) there are some limitations: * view models cannot hold collections other view models (simple properties are supported, though) * collections (of either view models or entities) are ignored.

1.32. @ViewModelLayout

The @ViewModelLayout annotation is identical to the @DomainObjectLayout, but is provided for symmetry with domain objects that have been annotated using @ViewModel (rather than @DomainObject(nature=VIEW_MODEL)).

The table below summarizes the annotation’s attributes.

Table 20. @ViewModel attributes
Attribute Values (default) Description

cssClass()

Any string valid as a CSS class

the css class that a domain class (type) should have, to allow more targetted styling in application.css

cssClassFa()

Any valid Font awesome icon name

specify a font awesome icon for the action’s menu link or icon.

cssClassFaPosition()

LEFT, RIGHT
(LEFT)

Currently unused.

describedAs()

String.

description of this class, eg to be rendered in a tooltip.

named()

String.

to override the name inferred from the action’s name in code.

A typical use case is if the desired name is a reserved Java keyword, such as default or package.

paged()

Positive integer

the page size for instances of this class when rendered within a table (as returned from an action invocation)

plural()

String.

the plural name of the class

For example:

@ViewModel                                 (1)
@ViewModelLayout(
    cssClass="x-analysis",
    cssClassFa="fa-piechart",
    describedAs="Analysis of todo items by category"
)
public class CategoryPieChart { ... }
1 this annotation is intended for use with @ViewModel. If a view model has been specified using the equivalent @DomainObject(nature=Nature.VIEW_MODEL), then we recommend you use @DomainObjectLayout instead.

Note that there is (currently) no support for specifying UI hints for view models through the dynamic .layout.json file (only for properties, collections and actions are supported).

1.32.1. cssClass()

The cssClass() attribute can be used to render additional CSS classes in the HTML (a wrapping <div>) that represents the view model. Application-specific CSS can then be used to target and adjust the UI representation of that particular element.

This attribute can also be applied to domain objects, actions properties, collections and parameters.

For example:

@ViewModel
@ViewModelLayout(cssClass="x-analysis")
public class CategoryPieChart { ... }

The similar @ViewModelLayout#cssClassFa() annotation attribute is also used as a hint to apply CSS, but in particular to allow Font Awesome icons to be rendered as the icon for classes.

1.32.2. cssClassFa()

The cssClassFa() attribute is used to specify the name of a Font Awesome icon name, to be rendered as the domain object’s icon.

These attribute can also be applied to domain objects to specify the object’s icon, and to actions to specify an icon for the action’s representation as a button or menu item.

If necessary the icon specified can be overridden by a particular object instance using the iconName() method.

For example:

@ViewModel
@ViewModelLayout(
    cssClassFa="fa-piechart"
)
public class CategoryPieChart { ... }

There can be multiple "fa-" classes, eg to mirror or rotate the icon. There is no need to include the mandatory fa "marker" CSS class; it will be automatically added to the list. The fa- prefix can also be omitted from the class names; it will be prepended to each if required.

The related cssClassFaPosition() attribute is currently unused for domain objects; the icon is always rendered to the left.

The similar @ViewModelLayout#cssClass() annotation attribute is also used as a hint to apply CSS, but for wrapping the representation of an object or object member so that it can be styled in an application-specific way.

1.32.3. describedAs()

The describedAs() attribute is used to provide a short description of the view model to the user. In the Wicket viewer it is displayed as a 'tool tip'. The describedAs() attribute can also be specified for collections, properties, actions, parameters and domain objects.

For example:

@ViewModel
@ViewModelLayout(
    cssClass="x-analysis",
    cssClassFa="fa-piechart",
    describedAs="Analysis of todo items by category"
)
public class CategoryPieChart { ... }

1.32.4. named()

The named() attribute explicitly specifies the view model’s name, overriding the name that would normally be inferred from the Java source code. This attribute can also be specified for actions, collections, properties, parameters, domain objects and domain services.

Following the don’t repeat yourself principle, we recommend that you only use this attribute when the desired name cannot be used in Java source code. Examples of that include a name that would be a reserved Java keyword (eg "package"), or a name that has punctuation, eg apostrophes.

For example:

@ViewModel
@ViewModelLayout(
    named="PieChartAnalysis"
)
public class PieChartAnalysisViewModel {
   ...
}

The framework also provides a separate, powerful mechanism for internationalization.

1.32.5. paged()

The paged() attribute specifies the number of rows to display in a standalone collection, as returned from an action invocation. This attribute can also be applied to collections and domain objects.

The RestfulObjects viewer currently does not support paging. The Wicket viewer _does support paging, but note that the paging is performed client-side rather than server-side.

We therefore recommend that large collections should instead be modelled as actions (to allow filtering to be applied to limit the number of rows).

For example:

@ViewModel
@ViewModelLayout(paged=15)
public class OrderAnalysis {
    ...
}

It is also possible to specify a global default for the page size of standalone collections, using the configuration property isis.viewer.paged.standalone.

1.32.6. plural()

When Apache Isis displays a standalone collection of several objects, it will label the collection using the plural form of the object type.

By default the plural name will be derived from the end of the singular name, with support for some basic English language defaults (eg using "ies" for names ending with a "y").

The plural() attribute allows the plural form of the class name to be specified explicitly. This attribute is also supported for domain objects.

For example:

@ViewModel
@ViewModelLayout(plural="Children")
public class Child {
    ...
}

2. Methods

The Apache Isis metamodel is built up from declaratively (ie, annotations) and imperatively, from "supporting" methods and other reserved methods.

This chapter documents the supporting methods and the reserved methods. It also documents (separately) the reserved methods that act as callback hooks into the persistence lifecycle of domain entities.

2.1. Supporting Method Prefixes

Supporting methods are those that are associated with properties, collections and actions, providing additional imperative business rule checking and behaviour to be performed when the user interacts with those object members.

This association is performed by name matching. Thus, a property called "firstName", derived from a method getFirstName() may have supporting methods hideFirstName(), disableFirstName() and validateFirstName(). Supporting methods are, therefore, each characterized by their own particular prefix.

Using name matching to associate supporting methods generally works very well, but of course if an object member’s method is renamed, there’s always the risk that the developer forgets to rename the supporting method; the supporting methods become "orphaned".

Apache Isis checks for this automatically, and will fail-fast (fail to boot) if any orphaned methods are located. A suitable error message is logged so that the issue can be easily diagnosed.

The table below lists the method prefixes that are recognized as part of Apache Isis' default programming model.

Table 21. Recognized Method Prefixes
Prefix Object Property Collection Action Action
Param
Description

addTo…​()

Y

add object to a collection

Directly mutable collections are not currently supported by the Wicket viewer.

See also removeFrom…​()`

autoComplete…​()

Y

Y

Return a list of matching elements for a property or an action parameter.

Alternatively, can specify for a class using @DomainObject
#autoCompleteRepository

See also choices…​()

choices…​()

Y

Y

Provide list of choices for a property or action parameter.

clear…​()

Y

Clear a property (set it to null).

Allows business logic to be placed apart from the setter.

See also modify…​()

default…​()

Y

Y

Default value for a property or an action parameter.

disable…​()

Y

Y

Y

Y

Disables (makes read-only) a property, a collection or an action.

get…​()

Y

Y

Access the value of a property or collection.

See also set…​().

hide…​()

Y

Y

Y

Hides a property, a collection or an action.

modify…​()

Y

Modify a property (set it to a non-null) value.

Allows business logic to be placed apart from the setter.

See also clear…​()`.

removeFrom…​()

Y

remove object from a collection.

Directly mutable collections are not currently supported by the Wicket viewer.

See also addTo…​()`

set…​()

Y

Y

Sets the value of a property or a collection.

validate…​()

Y

Y

Y

Check that a proposed value of a property or a set of action parameters or a single action parameter is valid.

See also validateAddTo…​() and validateRemoveFrom…​() to validate modifications to collections.

validate
AddTo…​()

Y

Check that a proposed object to add to a collection is valid.

Directly mutable collections are not currently supported by the Wicket viewer.

See also validateRemoveFrom…​(), and validate…​() for properties and actions.

validate
RemoveFrom…​()

Y

Check that a proposed object to remove from a collection is valid.

Directly mutable collections are not currently supported by the Wicket viewer.

See also validateAddTo…​(), and validate…​() for properties and actions.

2.1.1. addTo…​()

The addTo…​() supporting method is called whenever an object is added to a collection. Its purpose is to allow additional business logic to be performed.

Directly mutable collections are not currently supported by the Wicket viewer. The suggested workaround is to simply define an action.

For example:

public class LibraryMember {
    public SortedSet<Book> getBorrowed() { ... }
    public void setBorrowed(SortedSet<Book> borrowed) { ... }
    public void addToBorrowed(Book book) {
        getBorrowed().add(book);                                              (1)
        reminderService.addReminder(this, book, clock.today().plusDays(21));  (2)
    }
    public void removeFromBorrowed(Book book) { ... }
    ...
}
1 update the collection
2 perform some additional business logic

See also removeFrom…​()`

2.1.2. autoComplete…​()

The autoComplete…​() supporting method is called for action parameters and for properties to find objects from a drop-down list box. The use case is when the number of candidate objects is expected to be large, so the user is required to enter some characters to narrow the search down.

If the number of candidate objects is comparatively small, then use choices…​() supporting method instead.

The signature of the supporting method depends on whether it is for a parameter or a property.

Parameters

For an action parameter in (0-based) position N, and of type T, the signature is:

public Collection<T> autoCompleteNXxx(String search) { ... }

For example:

public class ShoppingCartItem {
    @Property(editing=Editing.DISABLED)
    public Product getProduct() { ... }
    public void setProduct(Product product) { ... }

    @Property(editing=Editing.DISABLED)
    public int getQuantity() { ... }
    public void setQuantity(int quantity) { ... }

    @Action(semantics=SemanticsOf.IDEMPOTENT)
    public ShoppingCartItem updateProduct(
        Product product,
        @ParameterLayout(named="Quantity")
        final int quantity) {
        setProduct(product);
        setQuantity(quantity);
    }
    public Collection<Product> autoComplete0UpdateProduct(  (1)
        @MinLength(3) String search                         (2)
    ) {
        ...
    }
    ...
}
1 product is the 0th argument of the action.
2 the @MinLength annotation specifies the minimum number of characters that must be entered before a search is performed for matching objects
Properties

For a property of type T, the signature is:

public Collection<T> autoCompleteXxx(String search) { ... }

For example:

public class ShoppingCartItem {
    public Product getProduct() { ... }
    public void setProduct(Product product) { ... }

    public Collection<Product> autoCompleteProduct(
        @MinLength(3) String search                     (1)
    ) {
        ...
    }
    ...
}
1 the @MinLength annotation specifies the minimum number of characters that must be entered before a search is performed for matching objects

2.1.3. choices…​()

The choices…​() supporting method is called for both action parameters and for properties, to find objects from a drop-down list box. Unlike autoComplete…​(), the use case is when the number of objects is comparatively small and can be selected from a drop-down without any additional filtering.

The signature of the supporting method depends on whether it is for an action parameter or a property.

Parameters

For an action parameter in (0-based) position N, and of type T, the signature is:

public Collection<T> choicesNXxx() { ... }

For example:

public class ShoppingCartItem {
    @Property(editing=Editing.DISABLED)
    public Product getProduct() { ... }
    public void setProduct(Product product) { ... }

    @Property(editing=Editing.DISABLED)
    public int getQuantity() { ... }
    public void setQuantity(int quantity) { ... }

    @Action(semantics=SemanticsOf.IDEMPOTENT)
    public ShoppingCartItem updateProduct(
        Product product,
        @ParameterLayout(named="Quantity")
        final Integer quantity) {
        setProduct(product);
        setQuantity(quantity);
    }
    public Collection<Integer> choices1UpdateProduct() {
        return Arrays.asList(1,2,3,5,10,25,50,100);
    }
    ...
}
Dependent Choices

Action parameters also support the notion of dependent choices, whereby the list of choices is dependent upon the value of some other argument.

An example can be found in the (non-ASF) Isis addons' todoapp, whereby `ToDoItem`s are categorized and then can also be subcategorized:

dependent

This functionality is actually implemented as a contributed action, so the code for this is:

@DomainService(nature = NatureOfService.VIEW_CONTRIBUTIONS_ONLY)
public class UpdateCategoryContributions ... {
    @ActionLayout(
            describedAs = "Update category and subcategory"
    )
    @Action(semantics = SemanticsOf.IDEMPOTENT)
    public Categorized updateCategory(
            final Categorized item,                              (1)
            final Category category,
            @Parameter(optionality = Optionality.OPTIONAL)
            final Subcategory subcategory) {
        item.setCategory(category);
        item.setSubcategory(subcategory);
        return item;
    }
    public List<Subcategory> choices2UpdateCategory(             (2)
            final Categorized item,                              (3)
            final Category category) {                           (4)
        return Subcategory.listFor(category);
    }
    ...
}
1 ToDoItem implements Categorized
2 subcategory is the 2-th argument (0-based)
3 the item contributed to
4 the category selected

Dependent choices are not restricted to enums, however. Going back to the shopping cart example shown above, the choices for the quantity parameter could be dependent upon the selected Product:

public class ShoppingCartItem {
    ...
    @Action(semantics=SemanticsOf.IDEMPOTENT)
    public ShoppingCartItem updateProduct(
        Product product,
        @ParameterLayout(named="Quantity")
        final Integer quantity) {
        setProduct(product);
        setQuantity(quantity);
    }
    public Collection<Integer> choices1UpdateProduct(Product product) {
        return productService.quantityChoicesFor(product);                 (1)
    }
    ...
}
1 productService is a (fictitous) injected service that knows what the quantity choices should be for any given product
Properties

For a property of type T, the signature is:

public Collection<T> choicesXxx() { ... }

For example:

public class ShoppingCartItem {
    public Product getProduct() { ... }
    public void setProduct(Product product) { ... }

    public Collection<Product> choicesProduct() {
        ...
    }

2.1.4. clear…​()

The clear…​() supporting method is called — instead of the setter — whenever an (optional) property is to be set to null. Its purpose is to allow additional business logic to be performed.

For example:

public class LibraryMember {
    public Title getFavoriteTitle() { ... }
    public void setFavoriteTitle(Title title) { ... }
    public void modifyFavoriteTitle(Title title) { ... }
    public void clearFavoriteTitle() {
        if(getTitle() == null) { return; }
        setFavoriteTitle(null);                         (1)
        titleFavoritesService.decrement(title);         (2)
    }
    ...
}
1 update the property
2 perform some additional business logic

See also modify…​()`

2.1.5. default…​()

The default…​() supporting method is called for action parameters to return the initial argument value. This may be some sensible default (eg today’s date, or 0 or 1), or — for an action that is modifying the state of an object — might default to the current value of a corresponding property.

The method is also called for properties in the case when an object is newly instantiated using DomainObjectContainer#newTransientInstance(…​). This is a much less common use case. If a default is not specified then properties are initialized to a default based on their type (eg 0 or false).

The signature of the supporting method depends on whether it is for an action parameter or a property.

Parameters

For an action parameter in (0-based position n), and of type T, the signature is:

public T defaultNXxx() { ... }

For example:

public class ShoppingCartItem {
    @Property(editing=Editing.DISABLED)
    public Product getProduct() { ... }
    public void setProduct(Product product) { ... }

    @Property(editing=Editing.DISABLED)
    public int getQuantity() { ... }
    public void setQuantity(int quantity) { ... }

    @Action(semantics=SemanticsOf.IDEMPOTENT)
    public ShoppingCartItem updateProduct(
        Product product,
        @ParameterLayout(named="Quantity")
        final Integer quantity) {
        setProduct(product);
        setQuantity(quantity);
    }
    public Product default0UpdateProduct() {    (1)
        return getProduct();
    }
    public int default1UpdateProduct() {        (2)
        return getQuantity();
    }
    ...
}
1 default the 0-th parameter using the current value of the product property
2 default the 1-th parameter using the current value of the quantity property

Defaults are also supported (of course) for contributed actions. For example, here is a contributed action for updating category/subcategory of the (non-ASF) Isis addons' todoapp:

@DomainService(nature = NatureOfService.VIEW_CONTRIBUTIONS_ONLY)
public class UpdateCategoryContributions ... {
    @ActionLayout(
            describedAs = "Update category and subcategory"
    )
    @Action(semantics = SemanticsOf.IDEMPOTENT)
    public Categorized updateCategory(
            final Categorized item,                              (1)
            final Category category,
            @Parameter(optionality = Optionality.OPTIONAL)
            final Subcategory subcategory) {
        item.setCategory(category);
        item.setSubcategory(subcategory);
        return item;
    }
    public Category default1UpdateCategory(                     (2)
            final Categorized item) {
        return item != null? item.getCategory(): null;
    }
    public Subcategory default2UpdateCategory(                  (3)
            final Categorized item) {
        return item != null? item.getSubcategory(): null;
    }
}
1 ToDoItem implements Categorized
2 defaults the 1-th parameter using the item’s category property
3 defaults the 2-th parameter using the item’s subcategory property
Properties

For a property of type T, the signature is:

public T defaultXxx() { ... }

For example:

public class ShoppingCartItem {
    public int getQuantity() { ... }
    public void setQuantity(int quantity) { ... }

    public int defaultProduct() {
        return 1;
    }
Alternatives

There are, in fact, two other ways to set properties of a newly instantiated object to default values.

The first is to use the created() callback, called by the framework when DomainObjectContainer#newTransientInstance(…​) is called. This method is called after any dependencies have been injected into the service.

The second is more straightforward: simply initialize properties in the constructor. However, this cannot use any injected services as they will not have been initialized.

2.1.6. disable…​()

The disable…​() supporting method is called for properties, collections and actions. It allows the modification of the property/collection to be vetoed (ie made read-only) and to prevent the invocation of the action ("grey it out").

Directly mutable collections are not currently supported by the Wicket viewer; they are always implicitly disabled.

Typically modification/invocation is vetoed based on the state of the domain object being interacted with, though it could be any reason at all (eg the current date/time of the interaction, or the state of some other related data such as stock levels, or the identity of the calling user).

The reason for vetoing a modification/invocation is normally returned as a string. However, Isis' i18n support extends this so that reasons can be internationalized.

Actions

For an action the signature of the supporting method is:

public String disableXxx(...) { ... }

where the returned string is the reason the action invocation is vetoed (or null if not vetoed), and the supporting method takes the same parameter types as the action itself.

For example:

public class Customer {
    public boolean isBlacklisted() { ... }

    public Order placeOrder(
            final Product product,
            @ParameterLayout(named="Quantity")
            final int quantity) {
        ...
    }
    public String disablePlaceOrder(
            final Product product,
            final int quantity
            ) {
        return isBlacklisted()
                    ? "Blacklisted customers cannot place orders"
                    : null;
    }
    ...
}
Properties and Collections

For both properties and collections the signature of the supporting method is:

public String disableXxx() { ... }

where the returned string is the reason the modification is vetoed (or null if not vetoed).

For example:

public class Customer {
    public boolean isBlacklisted() { ... }

    public BigDecimal getCreditLimit() { ... }
    public void setCreditLimit(BigDecimal creditLimit) { ... }
    public String disableCreditLimit() {
        return isBlacklisted()
                    ? "Cannot change credit limit for blacklisted customers"
                    : null;
    }
    ...
}

2.1.7. get…​()

The get…​() prefix is simply the normal JavaBean getter prefix that denotes properties or collections.

When Apache Isis builds its metamodel, it first searches for the getter methods, characterizing them as either properties or collections based on the return type. It then refines the metamodel based on the presence of annotations and supporting methods.

All remaining public methods (that do not use one of the Isis prefixes) are interpreted as actions.

Any methods "left over" that do use one of the Isis prefixes, are interpreted to be orphaned. Isis "fails-fast" and will not boot, instead printing an error message to the log so that the issue can be easily diagnosed.

See also set…​().

2.1.8. hide…​()

The hide…​() supporting method is called for properties, collections and actions. It allows the property/collection to be completely hidden from view.

It’s comparatively rare for properties or collections to be imperatively hidden from view, but actions are sometimes hidden or shown visible (as opposed to being just disabled, ie greyed out).

Actions

For an action the signature of the supporting method is either:

public bool hideXxx(...) { ... }

where the supporting method takes the same parameter types as the action itself, or more simply:

public bool hideXxx() { ... }

with no parameters at all. Returning true will hide the action, returning false leaves it visible.

For example:

public class Customer {
    public boolean isBlacklisted() { ... }

    public Order placeOrder(
            final Product product,
            @ParameterLayout(named="Quantity")
            final int quantity) {
        ...
    }
    public boolean hidePlaceOrder() {
        return isBlacklisted();
    }
    ...
}
Properties and Collections

For both properties and collections the signature of the supporting method is:

public boolean hideXxx() { ... }

where returning true will hide the property/collection, returning false leaves it visible.

For example:

public class Customer {
    public boolean isBlacklisted() { ... }

    public BigDecimal getCreditLimit() { ... }
    public void setCreditLimit(BigDecimal creditLimit) { ... }
    public boolean hideCreditLimit() {
        return isBlacklisted();
    }
    ...
}

2.1.9. modify…​()

The modify…​() supporting method is called — instead of the setter — whenever a property has been set to be set to a new value. Its purpose is to allow additional business logic to be performed.

For example:

public class LibraryMember {
    public Title getFavoriteTitle() { ... }
    public void setFavoriteTitle(Title title) { ... }
    public void modifyFavoriteTitle(Title title) {
        if(getTitle() != null) {
            titleFavoritesService.decrement(getTitle());    (1)
        }
        setFavoriteTitle(title);                            (2)
        titleFavoritesService.decrement(title);             (3)
    }
    public void clearFavoriteTitle() { ... }
    ...
}
1 perform some additional business logic
2 update the property
3 perform some additional business logic

See also clear…​()`

2.1.10. removeFrom…​()

The removeFrom…​() supporting method is called whenever an object is removed from a collection. Its purpose is to allow additional business logic to be performed.

Directly mutable collections are not currently supported by the Wicket viewer. The suggested workaround is to simply define an action.

For example:

public class LibraryMember {
    public SortedSet<Book> getBorrowed() { ... }
    public void setBorrowed(SortedSet<Book> borrowed) { ... }
    public void addToBorrowed(Book book) { ... }
    public void removeFromBorrowed(Book book) {
        getBorrowed().remove(book);                         (1)
        reminderService.removeReminder(this, book);         (2)
    }
    ...
}
1 update the collection
2 perform some additional business logic

See also addTo…​()`

2.1.11. set…​()

The set…​() prefix is simply the normal JavaBean setter prefix that denotes writeable properties or collections.

See also get…​().

2.1.12. validate…​()

The validate…​() supporting method is called for properties, actions and action parameters. It allows the proposed new value for a property to be rejected, or the proposed argument of an action parameter to be rejected, or to reject a whole set of action arguments for an actio invocation.

The reason for vetoing a modification/invocation is normally returned as a string. However, Isis' i18n support extends this so that reasons can be internationalized if required.

Action Parameter

For an action parameter in (0-based) position N, and of type T, the signature is:

public String validateNXxx(T proposed) { ... }

where the returned string is the reason why the argument is rejected (or null if not vetoed).

For example:

public class Customer {
    public Order placeOrder(
            final Product product,
            @ParameterLayout(named="Quantity")
            final int quantity) {
        ...
    }
    public String validate0PlaceOrder(
            final Product product) {
        return product.isDiscontinued()
                    ? "Product has been discontinued"
                    : null;
    }
    ...
}
Action Parameter Set

In addition to validating a single single action argument, it is also possible to validate a complete set of action arguments. The signature is:

public String validateXxx(...) { ... }

where the returned string is the reason why the argument is rejected (or null if not vetoed), and the supporting method takes the same parameter types as the action itself.

For example:

public class Customer {
    public Order placeOrder(
            final Product product,
            @ParameterLayout(named="Quantity")
            final int quantity) {
        ...
    }
    public String validatePlaceOrder(
            final Product product,
            final int quantity) {
        return quantity > product.getOrderLimit()
                    ? "May not order more than " + product.getOrderLimit() + " items for this product"
                    : null;
    }
    ...
}
Properties

For properties of type T the signature of the supporting method is:

public String validateXxx(T proposed) { ... }

where the returned string is the reason the modification is vetoed (or null if not vetoed).

For example:

public class Customer {
    public BigDecimal getCreditLimit() { ... }
    public void setCreditLimit(BigDecimal creditLimit) { ... }
    public validateCreditLimit(BigDecimal creditLimit) {
        return creditLimit.compareTo(BigDecimal.ZERO) < 0
                    ? "Credit limit cannot be negative"
                    : null;
    }
    ...
}

2.1.13. validateAddTo…​()

The validateAddTo…​() supporting method is called whenever an object is to be added to a collection. Its purpose is to validate the proposed object and possibly veto the change.

Directly mutable collections are not currently supported by the Wicket viewer. The suggested workaround is to simply define an action.

The signature of the supporting method for a collection with element type E is:

public String validateAddToXxx(E element) { ... }

where the returned string is the reason the collection modification invocation is vetoed (or null if not vetoed). Isis' i18n support extends this so that reasons can be internationalized if required.

For example:

public class LibraryMember {
    public SortedSet<Book> getBorrowed() { ... }
    public void setBorrowed(SortedSet<Book> borrowed) { ... }
    public String validateAddToBorrowed(Book book) {
        return book.isReference()? "Reference books cannot be borrowed": null;
    }
    public void validateRemoveFromBorrowed(Book book) { ... }
    ...
}

2.1.14. validateRemoveFrom…​()

The validateRemoveFrom…​() supporting method is called whenever an object is to be removed from a collection. Its purpose is to validate the proposed object removal and possibly veto the change.

Directly mutable collections are not currently supported by the Wicket viewer. The suggested workaround is to simply define an action.

The signature of the supporting method for a collection with element type E is:

public String validateRemoveFromXxx(E element) { ... }

where the returned string is the reason the collection modification invocation is vetoed (or null if not vetoed). Isis' i18n support extends this so that reasons can be internationalized if required.

For example:

public class LibraryMember {
    public SortedSet<Book> getBorrowed() { ... }
    public void setBorrowed(SortedSet<Book> borrowed) { ... }
    public String validateAddToBorrowed(Book book) { ... }
    public void validateRemoveFromBorrowed(Book book) {
        return !book.hasBeenReadBy(this)? "You didn't read this book yet": null;
    }
    ...
}

2.2. Reserved Methods

The table below lists the reserved methods that are recognized as part of Apache Isis' default programming model.

Table 22. Reserved Methods
Method Description

cssClass()

Provides a CSS class for this object instance. In conjunction with application.css, can therefore provide custom styling of an object instance wherever it is rendered.

See also title() and iconName().

disable(…​)

Disable all or some of an object’s properties

getId()

Provides an optional unique identifier of a service.

If not provided, the service’s fully-qualified class name is used.

hide(…​)

Hide all or some of an object’s properties

iconName()

Provides the name of the image to render, usually alongside the title, to represent the object. If not provided, then the class name is used to locate an image.

See also title() and cssClass()

title()

Provides a title for the object.

See also iconName() and cssClass()

validate()

Validate the object’s state prior to persisting.

2.2.1. cssClass()

The cssClass() returns a CSS class for a particular object instance.

The Wicket viewer wraps the object’s representation in a containing <div> with the class added. This is done both for rendering the object either in a table or when rendering the object on its own page.

In conjunction with application.css, can therefore provide custom styling of an object instance wherever it is rendered.

For example, the (non-ASF) Isis addons' todoapp uses this technique to add a strikethrough for completed todo items. This is shown on the home page:

strikethrough

The code to accomplish this is straightforward:

public class ToDoItem ... {
    public String cssClass() {
        return !isComplete() ? "todo" : "done";
    }
    ...
}

In the application.css, the following styles were then added:

tr.todo {
}
tr.done {
    text-decoration: line-through;
    color: #d3d3d3;
}

See also title() and iconName().

2.2.2. disable()

One use case that Isis supports is that of a domain object with a lifecycle whereby at some stage it should become immutable: all its properties/collections should be disabled, and/or its actions become not invokable.

It would be painful to have to write a separate disable…​() method for each and every member, so instead Isis allows a single disable…​(…​) method to be implemented that is applied to all members.

The signature of the method is:

public String disable(Identifier.Type identifierType) { ... }

where Identifier.Type is part of the Isis applib (nested static class of o.a.i.applib.Identifier) to distinguish between an interaction with an action, a property or an action.

Note that Isis' i18n support extends this so that the returned reason can also be internationalized.

For example:

public String disable(Identifier.Type identifierType) {
    return !calendarService.isOfficeHours(clock.today()
            ? "Cannot modify objects outside of office hours"
            : null;
}

See also the similar methods to hide() object members en-masse.

Alternatives

An alternative design — and one that could be easily argued is actually more flexible — is to leverage domain events with vetoing subscribers.

With this approach we define, for a given domain class, a base PropertyDomainEvent, CollectionDomainEvent and ActionDomainEvent. A good pattern is to make these nested static classes. For example:

public class ToDoItem ... {
    public static abstract class PropertyDomainEvent<T>
            extends ToDoAppDomainModule.PropertyDomainEvent<ToDoItem, T> {
        ...
    }
    ...
}

where in turn:

public final class ToDoAppDomainModule {
    private ToDoAppDomainModule(){}
    public abstract static class PropertyDomainEvent<S,T>
            extends org.apache.isis.applib.services.eventbus.PropertyDomainEvent<S,T> {
        ...
    }
    ...
}

Then, each property/collection/action emits either these base domain events or their own subclass:

public class ToDoItem ... {
    public static class DescriptionDomainEvent
            extends PropertyDomainEvent<String> {
        ...
    }
    @Property(
        domainEvent = DescriptionDomainEvent.class
    )
    public String getDescription() { ... }
    ...
}

A vetoing subscriber can then subscribe to the domain events and veto access, eg:

@DomainObject
public class VetoOutOfOfficeHours {
    @Subscribe
    public void on(ToDoItem.PropertyDomainEvent ev) {
        if(!calendarService.isOfficeHours(clock.today()) {
            ev.veto("Cannot modify objects outside of office hours");
        }
    }
    ...
}

Obviously there’s an awful lot more boilerplate here, but there’s also a lot more flexibility.

2.2.3. getId()

The getId() method applies only to domain services, and allows a unique identifer to be provided for that service.

This identifier corresponds in many ways to the objectType() attribute for domain objects; it is used as an internal identifier but also appears in URLs within the RestfulObjects viewer's REST API.

If the identifier is omitted, the services fully qualified class name is used.

Unlike domain objects, where the use of an object type is strongly encouraged (eg using @PersistenceCapable), it matters much less if an id is specified for domain services. The principle benefit is shorter URLs in the REST API.

2.2.4. hide()

One use case that Isis supports is that of a domain object with a lifecycle whereby at some stage some number of the object’s members should be hidden. For example, for an object that at some stage is logically immutable, we might want to make all its properties/collections unmodifiable and hide all its actions.

While we could write a separate hide…​() method for each and every action, this could become painful. So instead Isis allows a single hide…​(…​) method to be implemented that is applied to all members.

The signature of the method is:

public boolean hide(Identifier.Type identifierType) { ... }

where Identifier.Type is part of the Isis applib (nested static class of o.a.i.applib.Identifier) to distinguish between an interaction with an action, a property or an action.

For example:

public boolean hide(Identifier.Type identifierType) {
    return identifierType == Identifier.Type.ACTION && isFrozen();
}

See also the similar method to disable() object members en-masse.

Alternatives

An alternative design — and one that could be easily argued is actually more flexible — is to leverage domain events with vetoing subscribers.

There is further discussion on this approach in here.

2.2.5. iconName()

Every object is represented by an icon; this is based on the domain object’s simple name. The Wicket viewer searches for the image in the same package as the .class file for the domain object.

The iconName() allows the icon that to be used to change for individual object instances. These are usually quite subtle, for example to reflect the particular status of an object. The value returned by the iconName() method is added as a suffix to the base icon name.

For example, the (non-ASF) Isis addons' todoapp uses this technique to add an overlay for todo items that have been completed:

differing

The screenshot below shows the location of these png icon files:

png files

The code to accomplish this is straightforward:

public class ToDoItem ... {
    public String iconName() {
        return !isComplete() ? "todo" : "done";
    }
    ...
}

See also title() and cssClass()

2.2.6. title()

Every object is represented by a title. This appears both as a main header for the object when viewed as well as being used as a hyperlink within properties and collections. It therefore must contain enough information for the end-user to distinguish the object from any others.

This is most commonly done by including some unique key within the title, for example a customer’s SSN, or an order number, and so forth. However note that Isis itself does not require the title to be unique; it is merely recommended in most cases.

An object’s title can be constructed in various ways, but the most flexible is to use the title() method. The signature of this method is usually:

public String title() { ... }

Note that Isis' i18n support extends this so that titles can also be internationalized.

For example, the (non-ASF) Isis addons' todoapp uses this technique to add an overlay for todo items that have been completed:

public String title() {
    final TitleBuffer buf = new TitleBuffer();                              (1)
    buf.append(getDescription());
    if (isComplete()) {                                                     (2)
        buf.append("- Completed!");
    } else {
        try {
            final LocalDate dueBy = wrapperFactory.wrap(this).getDueBy();   (3)
            if (dueBy != null) {
                buf.append(" due by", dueBy);
            }
        } catch(final HiddenException ignored) {                            (4)
        }
    }
    return buf.toString();
}
1 simple tility class to help construct the title string
2 imperative conditional logic
3 using the WrapperFactory to determine if the dueBy field is visible for this user …​
4 …​ but ignore if not

As the example above shows, the implementation can be as complex as you like.

In many cases, though, you may be able to use the @Title annotation.

See also iconName() and cssClass()

2.2.7. validate()

The validate() method is used to specify that invariants pertaining to an object’s state are enforced.

(As of 1.8.0) there are known limitations with this functionality. Invariants are enforced when an object is initially created and when it is edited, however invariants are currently not enforced if an action is invoked.

The signature of the method is:

public String validate() { ... }

where the returned string is the reason that the invocation is vetoed.

Note that Isis' i18n support extends this so that the returned reason can also be internationalized.

2.3. Lifecycle Methods

The lifecycle callback methods notify a domain entity about its interaction within the persistence lifecycle. For example, the entity is notified immediately prior to being persisted, or when it is about to be updated.

Note that these callbacks are fired by Isis rather than JDO. In the future we may deprecate them because they duplicate functionality already available in JDO.

You may therefore want to consider using the JDO API directly; see here for further discussion.

The lifecycle callback methods supported by Isis are:

Table 23. Lifecycle methods (partial support)
Method Description

created()

called when an object has just been created using newTransientInstance()

loaded()

called when a (persistent) object has just been loaded from the object store.

persisted()

called when object has just been persisted from the object store.

persisting()

called when a (not-yet-persistent) object is just about to be persisted from the object store

removed()

called when a (persistent) object has just been deleted from the object store

removing()

called when a (persistent) object is just about to be deleted from the object store

updated()

called when a (persistent) object has just been updated in the object store

updating()

called when a (persistent) object is just about to be updated in the object store

Some lifecycle methods have been deprecated:

Table 24. Deprecated lifecycle methods
Method Notes

deleted()

Replaced by removed()

deleting()

Replaced by removing()

loading()

callback for when the (persistent) object is just about to be loaded from the object store.

This method is never called.

saved()

Replaced by persisted()

saving()

Replaced by persisting()

2.3.1. created()

The created() lifecycle callback method is called when an object has just been created using newTransientInstance()

2.3.2. loaded()

The loaded() lifecycle callback method is called when a (persistent) object has just been loaded from the object store.

2.3.3. persisted()

The persisted() lifecycle callback method is called when object has just been persisted from the object store.

See also persisting().

2.3.4. persisting()

The persisting() lifecycle callback method is called when a (not-yet-persistent) object is just about to be persisted from the object store

See also persisted().

2.3.5. removed()

The removed() lifecycle callback method is called when a (persistent) object has just been deleted from the object store

See also removing().

2.3.6. removing()

The removing() lifecycle callback method is called when a (persistent) object is just about to be deleted from the object store

See also removed().

2.3.7. updated()

The updated() lifecycle callback method is called when a (persistent) object has just been updated in the object store

See also updating().

2.3.8. updating()

The updating() lifecycle callback method is called when a (persistent) object is just about to be updated in the object store

See also updated().

2.3.9. Using the JDO API

As an alternative to relying on Isis to call lifecycle callback methods, you could instead use the JDO lifecycle listener API directly.

We may decide to deprecate the Isis callbacks in the future because they merely duplicate this functionality already available in JDO.

You can gain access to the relevant JDO API using the IsisJdoSupport domain service.

For example:

@DomainService(nature=NatureOfService.DOMAIN)
public class ObjectChangedListenerService
        implements javax.jdo.listener.StoreLifecycleListener {   (1)
    @Programmatic
    @PostConstruct
    public void init() {
        getPmFactory().addInstanceLifecycleListener(this);
    }
    @Programmatic
    @PreDestroy
    public void tidyUp() {
        getPmFactory().removeInstanceLifecycleListener(this);
    }
    private PersistenceManagerFactory getPmFactory() {
        return jdoSupport.getPersistenceManager()
               .getPersistenceManagerFactory();                 (2)
    }
    @Programmatic
    public void preStore (InstanceLifecycleEvent event) { ... }
    @Programmatic
    public void postStore (InstanceLifecycleEvent event) { ... }
    @Inject
    IsisJdoSupport jdoSupport;
}
1 implement whichever callback lifecycle listeners are of interest
2 register on the factory so set up for every subsequent session.

3. Domain Services API

Apache Isis includes an extensive number of domain services for your domain objects to use; simply define the service as an annotated field and Isis will inject the service into your object. These services' API are all defined in Isis' applib (o.a.i.core:isis-core-applib module); this minimizes the coupling between your code and Isis. It also allows you to easily mock out these services in your unit tests.

In addition there are a number of domain services that constitute an SPI; if present they are used by Isis itself rather than by your domain objects. A good example of this is the AuditingService3 service. The SPI services are covered in the Domain Services (SPI) chapter.

The table below summarizes all the APIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 25. Domain Services
API Description Implementation Notes

o.a.i.applib.
services.actinv
ActionInvocation-
Context

Request-scoped access to whether action is invoked on object and/or on collection of objects

ActionInvocationContext
o.a.i.core
isis-core-applib

API is also concrete class

o.a.i.applib.
services.background
BackgroundService

Programmatic persistence of commands to be persisted (so can be executed by a background mechanism, eg scheduler)

BackgroundServiceDefault
o.a.i.core
isis-core-runtime

o.a.i.applib.
services.bookmark
BookmarkService

Convert object reference to a serializable "bookmark", and vice versa

BookmarkServiceDefault
o.a.i.core
isis-core-metamodel

related services: BookmarkHolder-
ActionContributions, BookmarkHolder-
Association-
Contributions

o.a.i.applib.
services.clock
ClockService

Access the current time (and for testing, allow the time to be changed)

ClockService
o.a.i.core
isis-core-applib

API is also a concrete class.

o.a.i.applib.
services.command
CommandContext

Request-scoped access the current action that is being invoked, represented as a command object

CommandContext
o.a.i.core
isis-core-applib

API is also a concrete class.
depends on:
CommandService for persistent Command, else in-memory impl. used

o.a.i.applib
services.deeplink
DeepLinkService

Obtain a URL to a domain object (eg for use within an email or report)

DeepLinkServiceWicket
o.a.i.viewer
isis-viewer-wicket-impl

Implementation only usable within Wicket viewer

o.a.i.applib
DomainObjectContainer

Generic repository to search, create and delete objects. Also miscellaneous other functions, eg obtain title of object.

DomainObjectContainer-
Default
o.a.i.core
isis-core-metamodel

o.a.i.applib.
services.email
EmailService

Send a HTML email, optionally with attachments.

EmailServiceDefault
o.a.i.core
isis-core-runtime

o.a.i.applib.
services.eventbus
EventBusService

Programmatically post events to the internal event bus. Also used by Isis itself to broadcast domain events:

EventBusServiceJdo
o.a.i.core
isis-core-objectstore-jdo-datanucleus

o.a.i.applib.
services.guice
GuiceBeanProvider

Access to internal framework services initialized using Guice DI.

GuiceBeanProviderWicket
o.a.i.core
isis-viewer-wicket-impl

o.a.i.applib.
services.jdosupport
IsisJdoSupport

Lower level access to the JDO Persistence API.

IsisJdoSupportImpl
o.a.i.core
isis-core-objectstore-jdo-datanucleus

o.a.i.applib.
services.memento
MementoService

Capture a serializable memento of a set of primitives or bookmarks. Primarily used internally, eg in support of commands/auditing.

MementoServiceDefault
o.a.i.core
isis-core-runtime

o.a.i.applib.
services.metamodel
MetaModelService

Access to certain information from the Isis metamodel.

MetaModelServiceDefault
o.a.i.core
isis-core-metamodel

o.a.i.applib.
services.
queryresultscache
QueryResultsCache

Request-scoped caching of the results of queries (or any data set generated by a given set of input arguments).

QueryResultsCache
o.a.i.core
isis-core-applib

API is also a concrete class

o.a.i.applib.
services.scratchpad
Scratchpad

Request-scoped service for interchanging information between and aggregating over multiple method calls; in particular for use by "bulk" actions (invoking of an action for all elements of a collection)

Scratchpad
o.a.i.core
isis-core-applib

API is also a concrete class

o.a.i.applib.
services.sudo
SudoService

For use in testing while running fixture scripts, allows a block of code to run as a specified user account.

SudoServiceDefault
o.a.i.core
isis-core-runtime

API is also a concrete class

o.a.i.applib.
services.wrapper
WrapperFactory

Interact with another domain object "as if" through the UI (enforcing business rules, firing domain events)

WrapperFactoryDefault
o.a.i.core
isis-core-wrapper

o.a.i.applib.
services.xmlsnapshot
XmlSnapshotService

Generate an XML representation of an object and optionally a graph of related objects.

XmlSnapshotServiceDefault
o.a.i.core
isis-core-runtime

Key:

  • o.a.i is an abbreviation for org.apache.isis

  • o.ia.m is an abbreviation for org.isisaddons.module

There is also a number of deprecated domain services.

Table 26. Deprecated Domain Services
API Description Implementation Notes

o.a.i.applib.
annotation Bulk.InteractionContext

Request-scoped access to whether action is invoked on object and/or on collection of objects

Bulk.InteractionContext
o.a.i.core
isis-core-applib

Key:

  • o.a.i is an abbreviation for org.apache.isis

  • o.ia.m is an abbreviation for org.isisaddons.module

3.1. ActionInvocationContext

The ActionInvocationContext domain service is a @RequestScoped service intended to support the implementation of "bulk" actions annotated with @Action#invokeOn(). This allows the user to select multiple objects in a table and then invoke the same action against all of them.

When an action is invoked in this way, this service allows each object instance to "know where it is" in the collection; it acts a little like an iterator. In particular, an object can determine if it is the last object to be called, and so can perform special processing, eg to return a summary calculated result.

3.1.1. API

The API defined by the service is:

@DomainService(nature = NatureOfService.DOMAIN)
@RequestScoped                                        (1)
public static class ActionInvocationContext {
    public InvokedOn getInvokedOn() { ... }           (2)
    public List<Object> getDomainObjects() { ... }    (3)
    public int getSize() { ... }
    public int getIndex() { ... }                     (4)
    public boolean isFirst() { ... }
    public boolean isLast() { ... }
}
1 is @RequestScoped, so this domain service instance is scoped to a particular request and is then destroyed
2 an enum set to either OBJECT (if action has been invoked on a single object) or COLLECTION (if has been invoked on a collection).
3 returns the list of domain objects which are being acted upon
4 is the 0-based index to the object being acted upon.

3.1.2. Usage

For actions that are void or that return null, Isis will return to the list once executed. But for bulk actions that are non-void, Isis will render the returned object/value from the last object invoked (and simply discards the object/value of all actions except the last).

One idiom is for the domain objects to also use the Scratchpad service to share information, for example to aggregate values. The ActionInvocationContext#isLast() method can then be used to determine if all the information has been gathered, and then do something with it (eg derive variance across a range of values, render a graph etc).

More prosaically, the ActoinInvocationContext can be used to ensure that the action behaves appropriately depending on how it has been invoked (on a single object and/or a collection) whether it is called in bulk mode or regular mode. Here’s a snippet of code from the bulk action in the Isis addon example todoapp (not ASF):

public class ToDoItem ... {
    @Action(invokeOn=InvokeOn.OBJECTS_AND_COLLECTIONS)
    public ToDoItem completed() {
        setComplete(true);
        ...
        return actionInteractionContext.getInvokedOn() == InvokedOn.OBJECT
                ? this  (1)
                : null; (2)
    }
    @Inject
    ActionInteractionContext actionInteractionContext;
}
1 if invoked as a regular action, return this object;
2 otherwise (if invoked on collection of objects), return null, so that the Wicket viewer will re-render the list of objects

3.1.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' default implementation of ActionInvocationContext class is automatically registered (it is annotated with @DomainService) so no further configuration is required.

3.1.4. Unit testing support

The ActionInvocationContext class also has a couple of static factory methods intended to support unit testing:

@DomainService(nature = NatureOfService.DOMAIN)
@RequestScoped
public class ActionInvocationContext {
    public static ActionInvocationContext onObject(final Object domainObject) {
        return new ActionInvocationContext(InvokedOn.OBJECT, Collections.singletonList(domainObject));
    }
    public static ActionInvocationContext onCollection(final Object... domainObjects) {
        return onCollection(Arrays.asList(domainObjects));
    }
    public static ActionInvocationContext onCollection(final List<Object> domainObjects) {
        return new ActionInvocationContext(InvokedOn.COLLECTION, domainObjects);
    }
    ...
}

3.2. BackgroundService

The BackgroundService, and the companion BackgroundCommandService SPI service (used by Isis itself), enable action invocations to be persisted such that they may be invoked in the background.

The BackgroundService is responsible for capturing a memento representing the action invocation in a typesafe way, and persisting it rather than executing it directly.

The default BackgroundServiceDefault implementation (provided by isis-core) works by using a proxy wrapper around the target so that it can capture the action to invoke and its arguments using (a private copy of) MementoService. The persistence delegates the persistence of the memento to an appropriate implementation of the companion BackgroundCommandService. One such implementation of BackgroundCommandService is provided by (non-ASF) Isis addons' command module.

The persisting of commands is only half the story; there needs to be a separate process to read the commands and execute them. The BackgroundCommandExecution abstract class (discussed below) provides infrastructure to do this; the concrete implementation of this class depends on the configured BackgroundCommandService (in order to query for the persisted (background) Commands.

3.2.1. API & Implementation

Returns a proxy around the object which is then used to obtain the signature of the action to be invoked in the background.

public interface BackgroundService {
    @Programmatic
    <T> T execute(final T object);
}

The default implementation is provided by core (o.a.i.core.runtime.services.background.BackgroundServiceDefault).

3.2.2. Usage

Using the service is very straight-forward; wrap the target domain object using BackgroundService#execute(…​) and invoke the method on the object returned by that method.

For example:

public void submitCustomerInvoices() {
    for(Customer customer: customerRepository.findCustomersToInvoice()) {
        backgroundService.execute(customer).submitInvoice();
    }
    container.informUser("Calculating...");
}

This will create a bunch of background commands executing the submitInvoice() action for each of the customers returned from the customer repository.

The action method invoked must be part of the Isis metamodel, which is to say it must be public, accept only scalar arguments, and must not be annotated with @Programmatic or @Ignore. However, it may be annotated with @Action#hidden() or @ActionLayout#hidden() and it will still be invoked.

In fact, when invoked by the background service, no business rules (hidden, disabled, validation) are enforced; the action method must take responsibility for performing appropriate validation and error checking.

If you want to check business rules, you can use @WrapperFactory#wrapNoExecute(…​).

3.2.3. End-user experience

For the end-user, executing an action that delegates work off to the BackgroundService raises the problem of how does the user know the work is complete?

One option is for the background jobs to take responsibility to notify the user themselves. In the above example, this would be the submitInvoice() method called upon each customer. One could imagine more complex designs where only the final command executed notifies the user.

However, an alternative is to rely on the fact that the BackgroundService will automatically hint that the Command representing the original interaction (to submitCustomerInvoices() in the example above) should be persisted. This will be available if the related CommandContext and CommandService domain services are configured, and the CommandService supports persistent commands. Note that (non-ASF) Isis addons' command module does indeed provide such an implementation of CommandService (as well as of the required BackgroundCommandService).

Thus, the original action can run a query to obtain it corresponding Command, and return this to the user. The upshot is that the child Commands created by the BackgroundService will then be associated with Command for the original action.

We could if we wanted write the above example as follows:

public Command submitCustomerInvoices() {
    for(Customer customer: customerRepository.findCustomersToInvoice()) {
        backgroundService.execute(customer).submitInvoice();
    }
    return commandContext.getCommand();
}
@Inject
CommandContext commandContext;  (1)
1 the injected CommandContext domain service.

The user would be returned a domain object representing their action invocation.

3.2.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of BackgroundService is automatically registered (it is annotated with @DomainService) so no further configuration is required.

This service is closely related to the CommandContext and also that service’s supporting CommandService service.

The CommandContext service is responsible for providing a parent Command with which the background Commands can then be associated as children, while the CommandService is responsible for persisting those parent Command`s. The latter is analogous to the way in which the `BackgroundCommandService persists the child background `Command`s.

The implementations of CommandService and BackgroundCommandService go together; typically both parent Command`s and child background `Command`s will be persisted in the same way. The (non-ASF) Isis addons' command module provides implementations of both (see `CommandService and BackgroundCommandService).

3.2.6. BackgroundCommandExecution abstract class

The BackgroundCommandExecution (in isis-core) is an abstract template class provided by isis-core that defines an abstract hook method to obtain background `Command`s to be executed:

public abstract class BackgroundCommandExecution
                         extends AbstractIsisSessionTemplate {
    ...
    protected abstract List<? extends Command> findBackgroundCommandsToExecute();
    ...
}

The developer is required to implement this hook method in a subclass.

3.2.7. Quartz Scheduler Configuration

The last part of the puzzle is to actually run the (appropriate implementation of) `BackgroundCommandExecution). This could be run in a batch job overnight, or run continually by, say, the Quartz scheduler or by http://camel.apache.org]Apache Camel]. This section looks at configuring Quartz.

If using (non-ASF) Isis addons' command module, then note that this already provides a suitable concrete implementation, namely org.isisaddons.module.command.dom.BackgroundCommandExecutionFromBackgroundCommandServiceJdo. We therefore just need to schedule this to run as a Quartz job.

First, we need to define a Quartz job, for example:

import org.isisaddons.module.command.dom.BackgroundCommandExecutionFromBackgroundCommandServiceJdo;
public class BackgroundCommandExecutionQuartzJob extends AbstractIsisQuartzJob {
    public BackgroundCommandExecutionQuartzJob() {
        super(new BackgroundCommandExecutionFromBackgroundCommandServiceJdo());
    }
}

where AbstractIsisQuartzJob is in turn the following boilerplate:

package domainapp.webapp.quartz;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
...
public class AbstractIsisQuartzJob implements Job {
    public static enum ConcurrentInstancesPolicy {
        SINGLE_INSTANCE_ONLY,
        MULTIPLE_INSTANCES
    }

    private final AbstractIsisSessionTemplate isisRunnable;
    private final ConcurrentInstancesPolicy concurrentInstancesPolicy;
    private boolean executing;

    public AbstractIsisQuartzJob(AbstractIsisSessionTemplate isisRunnable) {
        this(isisRunnable, ConcurrentInstancesPolicy.SINGLE_INSTANCE_ONLY);
    }
    public AbstractIsisQuartzJob(
            AbstractIsisSessionTemplate isisRunnable,
            ConcurrentInstancesPolicy concurrentInstancesPolicy) {
        this.isisRunnable = isisRunnable;
        this.concurrentInstancesPolicy = concurrentInstancesPolicy;
    }

    public void execute(final JobExecutionContext context)
            throws JobExecutionException {
        final AuthenticationSession authSession = newAuthSession(context);
        try {
            if(concurrentInstancesPolicy == ConcurrentInstancesPolicy.SINGLE_INSTANCE_ONLY &&
               executing) {
                return;
            }
            executing = true;

            isisRunnable.execute(authSession, context);
        } finally {
            executing = false;
        }
    }

    AuthenticationSession newAuthSession(JobExecutionContext context) {
        String user = getKey(context, SchedulerConstants.USER_KEY);
        String rolesStr = getKey(context, SchedulerConstants.ROLES_KEY);
        String[] roles = Iterables.toArray(
                Splitter.on(",").split(rolesStr), String.class);
        return new SimpleSession(user, roles);
    }

    String getKey(JobExecutionContext context, String key) {
        return context.getMergedJobDataMap().getString(key);
    }
}

This job can then be configured to run using Quartz' quartz-config.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<job-scheduling-data
    xmlns="http://www.quartz-scheduler.org/xml/JobSchedulingData"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.quartz-scheduler.org/xml/JobSchedulingData
http://www.quartz-scheduler.org/xml/job_scheduling_data_1_8.xsd"
    version="1.8">
    <schedule>
       <job>
         <name>BackgroundCommandExecutionJob</name>
         <group>Isis</group>
         <description>
                Poll and execute any background actions persisted by the BackgroundActionServiceJdo domain service
            </description>
         <job-class>domainapp.webapp.quartz.BackgroundCommandExecutionQuartzJob</job-class>
         <job-data-map>
          <entry>
              <key>webapp.scheduler.user</key>
              <value>scheduler_user</value>
          </entry>
          <entry>
              <key>webapp.scheduler.roles</key>
              <value>admin_role</value>
          </entry>
         </job-data-map>
       </job>
       <trigger>
         <cron>
          <name>BackgroundCommandExecutionJobEveryTenSeconds</name>
          <job-name>BackgroundCommandExecutionJob</job-name>
          <job-group>Isis</job-group>
          <cron-expression>0/10 * * * * ?</cron-expression>
         </cron>
       </trigger>
    </schedule>
</job-scheduling-data>

The remaining two pieces of configuration are the quartz.properties file:

org.quartz.scheduler.instanceName = SchedulerQuartzConfigXml
org.quartz.threadPool.threadCount = 1
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
org.quartz.plugin.jobInitializer.class =org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin
org.quartz.plugin.jobInitializer.fileNames = webapp/scheduler/quartz-config.xml
org.quartz.plugin.jobInitializer.failOnFileNotFound = true

and the entry in web.xml for the Quartz servlet:

<servlet>
     <servlet-name>QuartzInitializer</servlet-name>
     <servlet-class>org.quartz.ee.servlet.QuartzInitializerServlet</servlet-class>
     <init-param>
         <param-name>config-file</param-name>
         <param-value>webapp/scheduler/quartz.properties</param-value>
     </init-param>
     <init-param>
         <param-name>shutdown-on-unload</param-name>
         <param-value>true</param-value>
     </init-param>
     <init-param>
         <param-name>start-scheduler-on-load</param-name>
         <param-value>true</param-value>
     </init-param>
     <load-on-startup>1</load-on-startup>
 </servlet>

3.3. BookmarkService

The BookmarkService provides the ability to obtain a serializable o.a.i.applib.bookmarks.Bookmark for any (persisted) domain object, and to lookup domain objects given a Bookmark. This can then in turn be converted to and from a string.

For example, a Customer object with:

could correspond to a Bookmark with a string representation of custmgmt.Customer|123.

A Bookmark is little more than an API equivalent of Isis' internal Oid (object identifier). Nevertheless, the ability to uniquely address any domain object within an Isis system — to in effect provide a URN — is immensely useful.

For example, a Bookmark could be converted into a barcode, and then this used for automated scanning of correspondence from a customer.

Bookmarks are used by several other domain services as a means of storing areference to an arbitrary object (a polymorphic relationship). For example, the (non-ASF) Isis addons' auditing module’s implementation of AuditingService uses bookmarks to capture the object that is being audited.

One downside of using Bookmarks is that there is no way for the JDO/DataNucleus objectstore to enforce any kind of referental integrity. However, the (non-ASF) Isis addons' poly module describes and supports a design pattern to address this requirement.

3.3.1. API & Implementation

The API defined by BookmarkService is:

public interface BookmarkService {
    @Programmatic
    Object lookup(BookmarkHolder bookmarkHolder);
    @Programmatic
    Object lookup(Bookmark bookmark);
    @Programmatic
    <T> T lookup(Bookmark bookmark, Class<T> cls);   (1)
    @Programmatic
    Bookmark bookmarkFor(Object domainObject);
    @Programmatic
    Bookmark bookmarkFor(Class<?> cls, String identifier);
}
1 same as lookup(Bookmark bookmark), but downcasts to the specified type.

The core framework provides a default implementation of this API, namely o.a.i.core.metamodel.services.bookmarks.BookmarkServiceDefault

3.3.2. BookmarkHolder

The BookmarkHolder interface is intended to be implemented by domain objects that use a Bookmark to reference a (single) domain object; an example might be a class such as the audit entry, mentioned above. The interface is simply:

public interface BookmarkHolder {
    @Programmatic
    Bookmark bookmark();
}

There are two services that will contribute to this interface:

  • BookmarkHolderActionContributions will provide a lookup(…​) action

  • BookmarkHolderAssociationContributions provides an object property.

Either of these can be suppressed, if required, using a vetoing subscriber. For example, to suppress the object property (so that only the lookup(…​) action is ever shown for implementations of BookmarkHolder, define:

@DomainObject
public class AlwaysHideBookmarkHolderAssociationsObjectProperty {
    @Subscribe
    public void on(BookmarkHolderAssociationContributions.ObjectDomainEvent ev) {
        ev.hide();
    }
}

A more sophisticated implementation could look inside the passed ev argument and selectively hide or not based on the contributee.

3.3.3. Usage by other services

Bookmarks are used by the (non-ASF) Isis addons' command module’s implementation of BackgroundCommandService, which uses a bookmark to capture the target object on which an action will be invoked subsequently.

Bookmarks are also used by the (non-ASF) Isis addons' publishing module’s implementation of PublishingService, and by the (non-ASF) Isis addons' auditing module’s implementation of AuditingService.

3.3.4. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of BookmarkService is automatically registered (it is annotated with @DomainService) so no further configuration is required.

3.4. ClockService

Most applications deal with dates and times in one way or another. For example, if an Order is placed, then the Customer may have 30 days to pay the Invoice, otherwise a penalty may be levied.

However, such date/time related functionality can quickly complicate automated testing: "today+30" will be a different value every time the test is run.

Even disregarding testing, there may be a requirement to ensure that date/times are obtained from an NNTP server (rather than the system PC). While instantiating a java.util.Date to current the current time is painless enough, we would not want complex technical logic for querying an NNTP server spread around domain logic code.

Therefore it’s common to provide a domain service whose responsibility is to provide the current time. This service can be injected into any domain object (and can be mocked out for unit testing). Isis provides such a facade through the ClockService.

3.4.1. API & Implementation

The API defined by ClockService is:

@DomainService(nature = NatureOfService.DOMAIN)
public class ClockService {
    @Programmatic
    public LocalDate now() { ... }
    @Programmatic
    public LocalDateTime nowAsLocalDateTime() { ... }
    @Programmatic
    public DateTime nowAsDateTime() { ... }
    @Programmatic
    public Timestamp nowAsJavaSqlTimestamp() { ... }
    @Programmatic
    public long nowAsMillis() { ... }
}

This class (o.a.i.applib.services.clock.ClockService) is also the default implementation. The time provided by this default implementation is based on the system clock.

3.4.2. Testing Support

The default ClockService implementation in fact simply delegates to another class defined in the API, namely the o.a.i.applib.clock.Clock, an abstract singleton class. It is not recommended that your code use the Clock directly, but you should understand how this all works:

  • there are two subclasses implementations Clock, namely SystemClock and FixtureClock.

  • the first implementation that is instantiated registers itself as the singleton.

  • if running in production (server) mode, then (unless another implementation has beaten it to the punch) the framework will instantiate the `SystemClock. Once instantiated this cannot be replaced.

  • if running in prototype mode, then the framework will instead instantiate FixtureClock. This _can be replaced if required.

The FixtureClock will behave as the system clock, unless its is explicitly set using FixtureClock#setDate(…​) or FixtureClock#setTime(…​) and so forth.

Alternative Implementations

Suppose you want (as discussed in the introduction to this service) to use a clock that delegates to NNTP. For most domain services this would amount to implementing the appropriate service and registering in isis.properties so that it is used in preference to any implementations provided by default by the framework.

In the case of the ClockService, though, this approach (unfortunately) will not work, because parts of Isis (still) delegate to the Clock singleton rather than using the ClockService domain service.

The workaround, therefore, is to implement your functionality as a subclass of Clock. You can write a domain service that will ensure that your implementation is used ahead of any implementations provided by the framework.

For example:

@DomainService(nature=NatureOfService.DOMAIN)
public class NntpClockServiceInitializer  {
    @Programmatic
    @PostConstruct
    public void postConstruct(Map<String,String> properties) {
        new NntpClock(properties);                       (1)
    }
    private static class NntpClock extends Clock {
        NntpClock(Map<String,String> properties) { ... } (2)
        protected long time() { ... }                    (3)
            ... NNTP stuff here ...
        }
    }
}
1 enough to simply instantiate the Clock; it will register itself as singleton
2 connect to NNTP service using configuration properties from isis.properties
3 call to NNTP service here

3.4.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of ClockService is automatically registered (it is annotated with @DomainService) so no further configuration is required.

If you want to use a different implementation of Clock, eg delegating to NNTP, then do not register directly, but instead subclass from o.a.i.applib.clock.Clock singleton (as described in the section above).

3.5. CommandContext

The CommandContext service is a request-scoped service that reifies the invocation of an action on a domain object into an object itself. This reified information is encapsulated within the Command object.

By default, the Command is held in-memory only; once the action invocation has completed, the Command object is gone. The optional supporting CommandService enables the implementation of Command to be pluggable. With an appropriate implementation (eg as provided by the (non-ASF) Isis addons' command module’s CommandService) the Command may then be persisted.

Persistent Commands support several use cases:

  • they enable profiling of the running application (which actions are invoked then most often, what is their response time)

  • they act as a parent to any background commands that might be invoked through the BackgroundService

  • if AuditingService is configured, they provide better audit information, since the Command (the 'cause' of an action) can be correlated to the audit records (the "effect" of the action) through the unique transactionId GUID

  • if PublishingService is configured, they provide better traceability as the Command is also correlated with any published events, again through the unique transactionId GUID

Assuming that the CommandService supports persistent Command`s, the associated @Action#command() annotation also allows action invocations to be performed in the background. In this case the act of invoking the action on an object instead returns the `Command to the user.

3.5.1. Screencast

The screencast provides a run-through of the command (profiling) service, auditing service, publishing service. It also shows how commands can be run in the background either explicitly by scheduling through the background service or implicitly by way of a framework annotation.

Note that this screencast shows an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

3.5.2. API & Implementation

The CommandContext request-scoped service defines the following very simple API:

@RequestScoped
public class CommandContext {
    @Programmatic
    public Command getCommand() { ... }
}

This class (o.a.i.applib.services.CommandContext) is also the default implementation. Under normal circumstances there shouldn’t be any need to replace this implementation with another.

The Command type referenced above is in fact an interface, defined as:

public interface Command extends HasTransactionId {

    public abstract String getUser();               (1)
    public abstract Timestamp getTimestamp();       (2)

    public abstract Bookmark getTarget();           (3)
    public abstract String getMemberIdentifier();   (4)
    public abstract String getTargetClass();        (5)
    public abstract String getTargetAction();       (6)
    public String getArguments();                   (7)
    public String getMemento();                     (8)

    public ExecuteIn getExecuteIn();                (9)
    public Executor getExecutor();                  (10)
    public Persistence getPersistence();            (11)
    public boolean isPersistHint();                 (12)

    public abstract Timestamp getStartedAt();       (13)
    public abstract Timestamp getCompletedAt();     (14)
    public Command getParent();                     (15)

    public Bookmark getResult();                    (16)
    public String getException();                   (17)
}
1 getUser() - is the user that initiated the action.
2 getTimestamp() - the date/time at which this action was created.
3 getTarget() - bookmark of the target object (entity or service) on which this action was performed
4 getMemberIdentifier() - holds a string representation of the invoked action
5 getTargetClass() - a human-friendly description of the class of the target object
6 getTargetAction() - a human-friendly name of the action invoked on the target object
7 getArguments() - a human-friendly description of the arguments with which the action was invoked
8 getMemento() - a formal (XML or similar) specification of the action to invoke/being invoked
9 getExecuteIn() - whether this command is executed in the foreground or background
10 getExecutor() - the (current) executor of this command, either user, or background service, or other (eg redirect after post).
11 getPersistence()- the policy controlling whether this command should ultimately be persisted (either "persisted", "if hinted", or "not persisted")
12 isPersistHint() - whether that the command should be persisted, if persistence policy is "if hinted".
13 getStartedAt() - the date/time at which this action started (same as timestamp property for foreground commands)
14 getCompletedAt() - the date/time at which this action completed.
15 getParent() - for actions created through the BackgroundService, captures the parent action
16 getResult() - bookmark to object returned by action, if any
17 getException() - exception stack trace if action threw exception

3.5.3. Usage

The typical way to indicate that an action should be treated as a command is to annotate it with the @Action#command() annotation.

For example:

public class ToDoItem ... {
    @Action(command=CommandReification.ENABLED)
    public ToDoItem completed() { ... }
}

As an alternative to annotating every action with @Action#command(), alternatively this can be configured as the default using isis.services.command.actions configuration property.

See @Action#command() and runtime configuration for further details.

The @Action#command() annotation can also be used to specify whether the command should be performed in the background, for example:

public class ToDoItem ... {
    @Command(executeIn=ExecuteIn.BACKGROUND)
    public ToDoItem scheduleImplicitly() {
        completeSlowly(3000);
        return this;
    }
}

When a background command is invoked, the user is returned the command object itself (to provide a handle to the command being invoked).

This requires that an implementation of CommandService that persists the commands (such as the (non-ASF) Isis addons' command module’s CommandService) is configured. It also requires that a scheduler is configured to execute the background commands, see BackgroundCommandService).

3.5.4. Interacting with the services

Typically the domain objects have little need to interact with the CommandContext and Command directly; what is more useful is that these are persisted in support of the various use cases identified above.

One case however where a domain object might want to obtain the Command is to determine whether it has been invoked in the foreground, or in the background. It can do this using the getExecutedIn() method:

Although not often needed, this then allows the domain object to access the Command object through the CommandContext service. To expand th above example:

public class ToDoItem ... {
    @Action(
        command=CommandReification.ENABLED,
        commandExecuteIn=CommandExecuteIn.BACKGROUND
    )
    public ToDoItem completed() {
        ...
        Command currentCommand = commandContext.getCommand();
        ...
    }
    @Inject
    CommandContext commandContext;
}

If run in the background, it might then notify the user (eg by email) if all work is done.

This leads us onto a related point, distinguishing the current effective user vs the originating "real" user. When running in the foreground, the current user can be obtained from the DomainObjectContainer, using:

String user = container.getUser().getName();

If running in the background, however, then the current user will be the credentials of the background process, for example as run by a Quartz scheduler job.

The domain object can still obtain the original ("effective") user that caused the job to be created, using:

String user = commandContext.getCommand().getUser();

3.5.5. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of CommandContext service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

As discussed above, the supporting CommandService enables Command objects to be persisted. Other related services are the BackgroundService and BackgroundCommandService). For BackgroundService captures commands for execution in the background, while the [BackgroundCommandService] persists such commands for execution.

The implementations of CommandService and BackgroundCommandService are intended to go together, so that persistent parent `Command`s can be associated with their child background `Command`s.

3.6. DeepLinkService

The DeepLinkService provides the ability to obtain a java.net.URI that links to a representation of any (persisted) domain entity or view model.

A typical use case is to generate a clickable link for rendering in an email, PDF, tweet or other communication.

3.6.1. API & Implementation

The API defined by DeepLinkService is:

public interface DeepLinkService {
    URI deepLinkFor(Object domainObject); (1)
}
1 Creates a URI that can be used to obtain a representation of the provided domain object in one of the Isis viewers.

The Wicket viewer this provides an implementation for accessing the representation through this viewer. (For the RestfulObjects viewer, a URL can be constructed according to the Restful Objects spec in conjunction with a Bookmark obtained via the BookmarkService).

3.6.2. Usage within the framework

The EmailNotificationService uses this service in order to generate emails as part of user registration.

3.6.3. Implementations

The Wicket viewer core framework provides a default implementation of this API:

  • org.apache.isis.viewer.wicket.viewer.services.DeepLinkServiceWicket

3.6.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

and that the Wicket viewer is being used then an implementation of DeepLinkService is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

3.7. DomainObjectContainer

The DomainObjectContainer service provides a set of general purpose functionality for domain objects to call. Principal amongst these are a generic APIs for querying objects and creating and persisting objects. In addition, the service provides access to security context (the "current user"), allows information and warning messages to be raised, and various other miscellaneous functions.

Compared to other services provided by Isis, the DomainObjectContainer provides by far the most number of methods, addressing several quite distinct responsibilities. This is a historical accident: in early versions of Isis the DomainObjectContainer was the one and only domain service provided by the framework.

In the future we may deprecate this service and break out its responsibilities into a number of small services. However, given how important this service was in days past, we are unlikely to delete it as a service even once it has been deprecated.

The sections below discuss the functions provided by the service, broken out into categories.

3.7.1. Object Creation API

The object creation APIs are used to instantiate new domain objects or view models.

public interface DomainObjectContainer {
    @Programmatic
    <T> T newTransientInstance(final Class<T> ofType);                              (1)
    @Programmatic
    <T> T newViewModelInstance(final Class<T> ofType, final String memento);        (2)
    ...
}
1 create a new non-persisted domain entity. Any services will be automatically injected into the service
2 create a new view model, with the specified memento (as per ViewModel#viewModelMemento(). In general it is easier to just annotate with @ViewModel and let Isis manage the memento automatically.

For example:

Customer cust = container.newTransientInstance(Customer.class);
cust.setFirstName("Freddie");
cust.setLastName("Mercury");
container.persist(cust);

As an alternative to using newTransientInstance(…​), you could also simply new() up the object. Doing this will not inject any domain services, but they can be injected manually using #injectServicesInto(…​)`.

Calling new(…​) also this circumvents Isis' created() callback, and in addition any default values for properties (either explicitly set by default…​() or defaulted implicitly according to Isis' own conventions) will not be called either. If you don’t intend to use these features, though, the net effect is code that has less coupling to Isis and is arguably easier to understand (has "less magic" happening).

3.7.2. Generic Repository API

The repository API acts as an abstraction over the JDO/DataNucleus objectstore. You can use it during prototyping to write naive queries (find all rows, then filter using the Guava Predicate API, or you can use it to call JDO named queries using JDOQL.

As an alternative, you could also use JDO typesafe queries through the IsisJdoSupport service.

public interface DomainObjectContainer {
    @Programmatic
    public <T> List<T> allInstances(Class<T> ofType, long... range);                        (1)
    @Programmatic
    <T> List<T> allMatches(Query<T> query);                                                 (2)
    @Programmatic
    <T> List<T> allMatches(Class<T> ofType, Predicate<? super T> predicate, long... range); (3)
    @Programmatic
    <T> List<T> allMatches(Class<T> ofType, String title, long... range);                   (4)
    @Programmatic
    <T> List<T> allMatches(Class<T> ofType, T pattern, long... range);                      (5)
    ...
}
1 all persisted instances of specified type. Mostly for prototyping, though can be useful to obtain all instances of domain entities if the number is known to be small. The optional varargs parameters are for paging control; more on this below.
2 all persistence instances matching the specified Query. Query itself is an Isis abstraction on top of JDO/DataNucleus' Query API. This is the primary API used for querying
3 all persistenced instances of specified type matching Predicate. Only really intended for prototyping because in effect constitutes a client-side WHERE clause
4 all persisted instances with the specified string as their title. Only very occasionally used
5 all persisted instances matching object (query-by-example). Only very occasionally used

There are various implementations of the Query API, but these either duplicate functionality of the other overloads of allMatches(…​) or they are not supported by the JDO/DataNucleus object store. The only significant implementation of Query to be aware of is QueryDefault, which identifies a named query and a set of parameter/argument tuples.

For example, in the (non-ASF) Isis addons' todoapp the ToDoItem is annotated:

@javax.jdo.annotations.Queries( {
    @javax.jdo.annotations.Query(
            name = "findByAtPathAndComplete", language = "JDOQL",               (1)
            value = "SELECT "
                    + "FROM todoapp.dom.module.todoitem.ToDoItem "
                    + "WHERE atPath.indexOf(:atPath) == 0 "                     (2)
                    + "   && complete == :complete"),                           (3)
    ...
})
public class ToDoItem ... {
    ...
}
1 name of the query
2 defines the atPath parameter
3 defines the complete parameter

This JDO query definitions are used in the ToDoItemRepositoryImplUsingJdoql service:

@DomainService(nature = NatureOfService.DOMAIN)
public class ToDoItemRepositoryImplUsingJdoql implements ToDoItemRepositoryImpl {
    @Programmatic
    public List<ToDoItem> findByAtPathAndCategory(final String atPath, final Category category) {
        return container.allMatches(
                new QueryDefault<>(ToDoItem.class,
                        "findByAtPathAndCategory",                              (1)
                        "atPath", atPath,                                       (2)
                        "category", category));                                 (3)
    }
    ...
    @javax.inject.Inject
    DomainObjectContainer container;
}
1 corresponds to the "findByAtPathAndCategory" JDO named query
2 provide argument for the atPath parameter. The pattern is parameter, argument, parameter, argument, …​ and so on.
3 provide argument for the category parameter. The pattern is parameter, argument, parameter, argument, …​ and so on.

Other JDOQL named queries (not shown) follow the exact same pattern.

With respect to the other query APIs, the varargs parameters are optional, but allow for (client-side and managed) paging. The first parameter is the start (0-based, the second is the count.

It is also possible to query using DataNucleus' type-safe query API. For more details, see IsisJdoSupport.

3.7.3. Object Persistence API

The persistence API is used to persist newly created objects (as per #newTransientInstance(…​), above and to delete (remove) objects that are persistent.

Note that there is no API for updating existing objects; the framework (or rather, JDO/DataNucleus) performs object dirty tracking and so any objects that are modified in the course of a request will be automatically updated).

public interface DomainObjectContainer {
    @Programmatic
    boolean isPersistent(Object domainObject);          (1)
    @Programmatic
    boolean isViewModel(Object domainObject);           (2)

    @Programmatic
    void persist(Object domainObject);                  (3)
    @Programmatic
    void persistIfNotAlready(Object domainObject);      (4)

    @Programmatic
    void remove(Object persistentDomainObject);         (5)
    @Programmatic
    void removeIfNotAlready(Object domainObject);       (6)

    @Programmatic
    boolean flush();                                    (7)
    ...
}
1 test whether a particular domain object is persistent or not
2 test whether a particular domain object is a view model or not. Note that this includes any domain objects annotated with @DomainObject#nature=Nature.EXTERNAL_ENTITY) or @DomainObject#nature=Nature.INMEMORY_ENTITY
3 persist a transient object. Note though that this will throw an exception if the object is already persistent; this can happen if JDO/DataNucleus’s persistence-by-reachability is in effect. For this reason it is generally better to use:
4 persist an object but only if know to not have been persistent. But if the object is persistent, is a no-op
5 remove (ie DELETE) a persistent object. For similar reasons to the persistence, it is generally better to use:
6 remove (ie DELETE) an object only if known to be persistent. But if the object has already been deleted, then is a no-op.
7 flushes all pending changes to the objectstore. Explained further below.

For example:

Customer cust = container.newTransientInstance(Customer.class);
cust.setFirstName("Freddie");
cust.setLastName("Mercury");
container.persistIfNotAlready(cust);

You should be aware that by default Isis queues up calls to #persist() and #remove(). These are then executed either when the request completes (and the transaction commits), or if the queue is flushed. This can be done either implicitly by the framework, or as the result of a direct call to #flush().

By default the framework itself will cause #flush() to be called whenever a query is executed by way of #allMatches(Query), as documented above. However, this behaviour can be disabled using the configuration property isis.services.container.disableAutoFlush.

3.7.4. Messages API

The DomainObjectContainer allows domain objects to raise information, warning or error messages. These messages can either be simple strings, or can be translated.

public interface DomainObjectContainer {
    @Programmatic
    void informUser(String message);                                                            (1)
    @Programmatic
    String informUser(TranslatableString message, Class<?> contextClass, String contextMethod); (2)

    @Programmatic
    void warnUser(String message);                                                              (3)
    @Programmatic
    String warnUser(TranslatableString message, Class<?> contextClass, String contextMethod);   (4)

    @Programmatic
    void raiseError(String message);                                                            (5)
    @Programmatic
    String raiseError(TranslatableString message, Class<?> contextClass, String contextMethod); (6)
    ...
}
1 display as a transient message to the user (not requiring acknowledgement). In the Wicket viewer this is implemented as a toast that automatically disappears after a period of time.
2 ditto, but with translatable string, for i18n support.
3 warn the user about a situation with the specified message. In the Wicket viewer this is implemented as a toast that must be closed by the end-user.
4 ditto, but with translatable string, for i18n support.
5 show the user an unexpected application error. In the Wicket viewer this is implemented as a toast (with a different colour) that must be closed by the end-user.
6 ditto, but with translatable string, for i18n support.

For example:

public Order addItem(Product product, @ParameterLayout(named="Quantity) int quantity) {
    if(productRepository.stockLevel(product) == 0) {
        container.warnUser(
            product.getDescription() + " out of stock; order fulfillment may be delayed");
    }
    ...
}

3.7.5. Security API

The security API allows the domain object to obtain the identity of the user interacting with said object.

public interface DomainObjectContainer {
    @Programmatic
    UserMemento getUser();
    ...
}

where in turn (the essence of) UserMemento is:

public final class UserMemento {
    public String getName() { ... }
    public boolean isCurrentUser(final String userName) { ... }

    public List<RoleMemento> getRoles() { ... }
    public boolean hasRole(final RoleMemento role) { ... }
    public boolean hasRole(final String roleName) { ... }
    ...
}

and RoleMemento is simpler still:

public final class RoleMemento {
    public String getName() { ... }
    public String getDescription() { ... }
    ...
}

The roles associated with the UserMemento will be based on the configured security (typically Shiro).

In addition, when using the Wicket viewer there will be an additional "org.apache.isis.viewer.wicket.roles.USER" role; this is used internally to restrict access to web pages without authenticating.

3.7.6. Presentation API

A responsibility of every domain object is to return a title. This can be done declaratively using the @Title annotation on property/ies, or it can be done imperatively by writing a title() method.

It’s quite common for titles to be built up of the titles of other objects. If using building up the title using @Title then Isis will automatically use the title of the objects referenced by the annotated properties. We also need programmatic access to these titles if going the imperative route.

Similarly, it often makes sense if raising messages to use the title of an object in a message rather (than a some other property of the object), because this is how end-users will be used to identifying the object.

The API defined by DomainObjectContainer is simply:

public interface DomainObjectContainer {
    @Programmatic
    String titleOf(Object domainObject);                (1)
    ...
}
1 return the title of the object, as rendered in the UI by the Isis viewers.

By way of example, here’s some code from the (non-ASF) Isis addons' todoapp showing the use of the API in an message:

    public List<ToDoItem> delete() {
        final String title = container.titleOf(this);   (1)
        ...
        container.removeIfNotAlready(this);
        container.informUser(
                TranslatableString.tr(
                    "Deleted {title}", "title", title), (2)
                    this.getClass(), "delete");
        ...
    }
1 the title is obtained first, because we’re not allowed to reference object after it’s been deleted
2 use the title in an i18n TranslatableString

3.7.7. Validation API

The validation API allows a domain object to programmatically validate itself or any other domain object. Specifically, this means the validating the current state of all properties, as well as any object-level validation defined by validate().

The API provided is:

public interface DomainObjectContainer {
    @Programmatic
    boolean isValid(Object domainObject);
    @Programmatic
    String validate(Object domainObject);
    ...
}

If an object is changed by editing its properties, then (using the Wicket viewer at least) Isis will check that the state of the object is valid. There is currently however no equivalent guarantee for actions.

The intent of this API was (originally at least) to provide a fallback whereby actions could at least be able to perform this validation programmatically (eg in their own validate…​() or disable…​() methods. However, this feature should be considered experimental; your mileage may vary.

3.7.8. Properties API

The properties API allows domain objects to read the configuration properties aggregated from the various configuration files.

public interface DomainObjectContainer {
    @Programmatic
    String getProperty(String name);                        (1)
    @Programmatic
    String getProperty(String name, String defaultValue);   (2)
    @Programmatic
    List<String> getPropertyNames();                        (3)
}
1 Return the configuration property with the specified name; else return null.
2 Return the configuration property with the specified name; if it doesn’t exist then return the specified default value.
3 Return the names of all the available properties.

For example, here’s a fictitious service that might wrap Twitter4J. say:

@DomainService(nature=NatureOfService.DOMAIN)
public class TweetService {
    @Programmatic
    @PostConstruct
    public void init() {
        this.oauthConsumerKey = container.getProperty("tweetservice.oauth.consumerKey");
        this.oauthConsumerSecret = container.getProperty("tweetservice.oauth.consumerSecret");
        this.oauthAccessToken = container.getProperty("tweetservice.oauth.accessToken");
        this.oauthAccessTokenSecret = container.getProperty("tweetservice.oauth.accessTokenSecret");
    }
    ...
    @Inject
    DomainObjectContainer container;
}

If you do have a domain service that needs to access properties, then note that an alternative is to define a @PostConstruct method and pass in a Map<String,String> of properties. The two techniques are almost identical; it’s mostly a matter of taste.

3.7.9. Services API

The services API allows your domain objects to programmatically inject services into arbitrary objects, as well as to look up services by type:

public interface DomainObjectContainer {
    @Programmatic
    <T> T injectServicesInto(final T domainObject);     (1)
    @Programmatic
    <T> T lookupService(Class<T> service);              (2)
    @Programmatic
    <T> Iterable<T> lookupServices(Class<T> service);   (3)
    ...
}
1 injects services into domain object; used extensively internally by the framework (eg to inject to other services, or to entities, or integration test instances, or fixture scripts). Service injection is done automatically if objects are created using #newTransientInstance(), described above
2 returns the first registered service that implements the specified class
3 returns an Iterable in order to iterate over all registered services that implement the specified class

The primary use case is to instantiate domain objects using a regular constructor ("new is the new new") rather than using the #newTransientInstance() API, and then using the #injectServicesInto(…​) API to set up any dependencies.

For example:

Customer cust = container.injectServicesInto( new Customer());
cust.setFirstName("Freddie");
cust.setLastName("Mercury");
container.persist(cust);

3.7.10. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of DomainObjectContainer service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

3.8. EmailService

The EmailService provides the ability to send HTML emails, with attachments, to one or more recipients.

Apache Isis provides a default implementation to send emails using an external SMTP provider. Note that this must be configured (using a number of configuration properties) before it can be used. The that sends email as an HTML message, using an external SMTP provider.

3.8.1. API & Implementation

The API for the service is:

public interface EmailService {
    @Programmatic
    boolean send(                                                   (1)
        List<String> to, List<String> cc, List<String> bcc,         (2)
        String subject,
        String body,                                                (3)
        DataSource... attachments);
    @Programmatic
    boolean isConfigured();                                         (4)
  }
1 is the main API to send the email (and optional attachments). Will return false if failed to send
2 pass either null or Collections.emptyList() if not required
3 should be HTML text
4 indicates whether the implementation was configured and initialized correctly. If this returns false then any attempt to call send(…​) will fail.

As noted in the introduction, the core framework provides a default implementation (EmailServiceDefault) that sends email as an HTML message, using an external SMTP provider.

3.8.2. Configuration

To use this service the following properties must be configured:

  • isis.service.email.sender.address

  • isis.service.email.sender.password

and these properties may optionally be configured (each has a default to use gmail, documented here):

  • isis.service.email.sender.hostname

  • isis.service.email.port

  • isis.service.email.tls.enabled

These configuration properties can be specified either in isis.properties or in an external configuration file.

If prototyping (that is, running the app using org.apache.isis.WebServer), the configuration properties can also be specified as system properties. For example, if you create a test email account on gmail, you can configure the service using:

-Disis.service.email.sender.address=xxx@gmail.com -Disis.service.email.sender.password=yyy

where "xxx" is the gmail user account and "yyy" is its password

3.8.3. Alternative Implementations

If you wish to write an alternative implementation, be aware that it should process the message body as HTML (as opposed to plain text or any other format).

Also, note that (unlike most Isis domain services) the implementation is also instantiated and injected by Google Guice. This is because EmailService is used as part of the user registration functionality and is used by Wicket pages that are accessed outside of the usual Isis runtime. This implies a couple of additional constraints:

  • first, implementation class should also be annotated with @com.google.inject.Singleton

  • second, there may not be any Isis session running. (If necessary, one can be created on the fly using IsisContext.doInSession(…​))

To ensure that your alternative implementation takes the place of the default implementation, register it explicitly in isis.properties.

3.8.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of EmailService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

If you have written an alternative implementation, then register your implementation in isis.properties under the "isis.services" key and it will be used instead.

The email service is used by the EmailNotificationService which is, in turn, used by UserRegistrationService.

3.9. EventBusService

The EventBusService allows domain objects to emit events to subscribing domain services using an in-memory event bus.

The primary user of the service is the framework itself, which automatically emit events for actions, properties and collections. Multiple events are generated:

  • when an object member is to be viewed, an event is fired; subscribers can veto (meaning that the member is hidden)

  • when an object member is to be enabled, the same event instance is fired; subscribers can veto (meaning that the member is disabled, ie cannot be edited/invoked)

  • when an object member is being validated, then a new event instance is fired; subscribers can veto (meaning that the candidate values/action arguments are rejected)

  • when an object member is about to be changed, then the same event instance is fired; subscribers can perform pre-execution operations

  • when an object member has been changed, then the same event instance is fired; subscribers can perform post-execution operations

If a subscriber throws an exception in the first three steps, then the interaction is vetoed. If a subscriber throws an exception in the last two steps, then the transaction is aborted. For more on this topic, see @Action#domainEvent(), @Property#domainEvent() and @Collection#domainEvent().

It is also possible for domain objects to programmatically generate domain events. However the events are published, the primary use case is to decoupling interactions from one module/package/namespace and another.

Two implementations are available, using either Guava's EventBus, or alternatively using the AxonFramework's SimpleEventBus. It is also possible to plug in a custom implementation.

3.9.1. API & Implementation

The API defined by EventBusService is:

public abstract class EventBusService {
    @Programmatic
    public void post(Object event) { ... }                          (1)
    @Programmatic
    public void register(final Object domainService) { ... }        (2)
    @Programmatic
    public void unregister(final Object domainService) { ... }      (3)
}
1 posts the event onto event bus
2 allows domain services to register themselves. This should be done in their @PostConstruct initialization method (for both singleton and @RequestScoped domain services.
3 exists for symmetry, but need never be called (it is in fact deliberately a no-op).

Isis provides a default implementation of the service, o.a.i.objectstore.jdo.datanucleus.service.eventbus.EventBusServiceJdo.

3.9.2. Registering Subscribers

The register() method should be called in the @PostConstruct lifecycle method. It is valid and probably the least confusing to readers to also "unregister" in the @PreDestroy lifecycle method (though as noted above, unregistering is actually a no-op).

For example:

@DomainService(nature=NatureOfService.DOMAIN)
public class MySubscribingDomainService {
    @PostConstruct
    public void postConstruct() {
        eventBusService.register(this);
    }
    @PreDestroy
    public void preDestroy() {
        eventBusService.unregister(this);
    }
    ...
    @javax.inject.Inject
    EventBusService eventBusService;
}

This works for both singleton (application-scoped) and also @RequestScoped domain services.

3.9.3. Annotating Members

As discussed in the introduction, the framework will automatically emit domain events for all of the object members (actions, properties or collections) of an object whenever that object is rendered or (more generally) interacted with.

For example:

public class Customer {
    @Action
    public Customer placeOrder(Product product, @ParameterLayout(named="Quantity") int qty) { ... }
    ...
}

will propagate an instance of the default o.a.i.applib.services.eventbus.ActionDomainEvent.Default class. If using the Guava event bus this can be subscribed to using:

@DomainService(nature=NatureOfService.DOMAIN)
public class MySubscribingDomainService
    @Programmatic
    @com.google.common.eventbus.Subscribe
    public void on(ActionDomainEvent ev) { ... }
    ...
}

or if using Axonframework, the subscriber uses a different annotation:

@DomainService(nature=NatureOfService.DOMAIN)
public class MySubscribingDomainService
    @Programmatic
    @org.axonframework.eventhandling.annotation.EventHandle
    public void on(ActionDomainEvent ev) { ... }
    ...
}

More commonly though you will probably want to emit domain events of a specific subtype. As a slightly more interesting example, suppose in a library domain that a LibraryMember wants to leave the library. A letter should be sent out detailing any books that they still have out on loan:

In the LibraryMember class, we publish the event by way of an annotation:

public class LibraryMember {
    @Action(domainEvent=LibraryMemberLeaveEvent.class)  (1)
    public void leave() { ... }
    ...
}
1 LibraryMemberLeaveEvent is a subclass of o.a.i.applib.eventbus.ActionDomainEvent. The topic of subclassing is discussed in more detail below.

Meanwhile, in the BookRepository domain service, we subscribe to the event and act upon it. For example:

public class BookRepository {
    @Programmatic
    @com.google.common.eventbus.Subscribe
    public void onLibraryMemberLeaving(LibraryMemberLeaveEvent e) {
        LibraryMember lm = e.getLibraryMember();
        List<Book> lentBooks = findBooksOnLoanFor(lm);
        if(!lentBooks.isEmpty()) {
            sendLetter(lm, lentBooks);
        }
    }
    ...
}

This design allows the libraryMember module to be decoupled from the book module.

3.9.4. Event hierarchy

By creating domain event subtypes we can be more semantically precise and in turn providesmore flexibility for subscribers: they can choose whether to be broadly applicable (by subscribing to a superclass) or to be tightly focussed (by subscribing to a subclass).

We recommend that you define event classes at (up to) four scopes:

  • at the top "global" scope is the Isis-defined o.a.i.applib.event.ActionDomainEvent

  • for the "module" scope, create a static class to represent the module itself, and creating nested classes within

  • for each "class" scope, create a nested static event class in the domain object’s class for all of the domain object’s actions

  • for each "action" scope, create a nested static event class for that action, inheriting from the "domain object" class.

To put all that into code; at the module level we can define:

package com.mycompany.modules.libmem;
...
public static class LibMemModule {
    private LibMemModule() {}
    public abstract static class ActionDomainEvent<S>
                extends org.apache.isis.applib.event.ActionDomainEvent<S> {
        private static final long serialVersionUID = 1L;
        public ActionDomainEvent(
            final S source,
            final Identifier identifier,
            final Object... arguments) {
            super(source, identifier, arguments);
        }
    }
    ...         (1)
}
1 similar events for properties and collections should also be defined

For the class-level we can define:

public static class LibraryMember {
    public abstract static class ActionDomainEvent extends LibMemModule.ActionDomainEvent<LibraryMember> {
        private static final long serialVersionUID = 1L;
        public ActionDomainEvent(
            final LibraryMember source,
            final Identifier identifier,
            final Object... arguments) {
            super(source, identifier, arguments);
        }
    }
    ...         (1)
}
1 similar events for properties and collections should also be defined

and finally at the action level we can define:

public class LibraryMember {
    ...
    public static class LeaveEvent extends LibraryMember.ActionDomainEvent {
        private static final long serialVersionUID = 1L;
        public PlaceOrderEvent(
            final Customer source,
            final Identifier identifier,
            final Object... arguments) {
        super(source, identifier, arguments);
        }
    }
    @Action(domainEvent=LeaveEvent.class)
    public void leave() { ... }
    ...
}

The subscriber can subscribe either to the general superclass (as before), or to any of the classes in the hierarchy.

3.9.5. Programmatic posting

To programmatically post an event, simply call #post().

The LibraryMember example described above could for example be rewritten into:

public class LibraryMember {
    ...
    public void leave() {
        ...
        eventBusService.post(new LibraryMember.LeaveEvent(...));    (1)
    }
    ...
}
1 LibraryMember.LeaveEvent could be any class, not just a subclass of o.a.i.applib.event.ActionDomainEvent.

In practice we suspect there will be few cases where the programmatic approach is required rather than the declarative approach afforded by @Action#domainEvent() et al.

3.9.6. Using WrapperFactory

An alternative way to cause events to be posted is through the WrapperFactory. This is useful when you wish to enforce a (lack-of-) trust boundary between the caller and the callee.

For example, suppose that Customer#placeOrder(…​) emits a PlaceOrderEvent, which is subscribed to by a ReserveStockSubscriber. This subscriber in turn calls StockManagementService#reserveStock(…​). Any business rules on #reserveStock(…​) should be enforced.

In the ReserveStockSubscriber, we therefore use the WrapperFactory:

@DomainService(nature=NatureOfService.DOMAIN)
public class ReserveStockSubscriber {
    @Programmatic
    @Subscribe
    public void on(Customer.PlaceOrderEvent ev) {
        wrapperFactory.wrap(stockManagementService)
                      .reserveStock(ev.getProduct(), ev.getQuantity());
    }
    ...
    @Inject
    StockManagementService stockManagementService;
    @Inject
    WrapperFactory wrapperFactory;
}

3.9.7. Implementation SPI

The implementation of EventBusService provided by Isis will by default use Guava's EventBus as the underlying in-memory event bus. Alternatively the AxonFramework's SimpleEventBus can be used. Which is used is specified through configuration property (described below).

Guava vs Axon, which to use?

Guava actually queues up events; they are not guaranteed to be dispatched immediately. This generally is not problem, but can be for cases where the subscriber may in turn want to post its own events (using WrapperFactory).

The Axon SimpleEventBus-based implementation on the other hand is fully synchronous; events are dispatched as soon as they are posted. This works well in all scenarios (that we have tested).

It is also possible to use some other implementation.

public interface EventBusImplementation {
    void register(Object domainService);
    void unregister(Object domainService);
    void post(Object event);
}

As is probably obvious, the EventBusService just delegates down to these method calls when its own similarly named methods are called.

If you do provide your own implementation of this SPI, be aware that your subscribers will need to use whatever convention is required (eg different annotations) such that the events are correctly routed through to your subscribers.

3.9.8. Configuration

The implementation of EventBusService provided by Isis will by default use Guava's EventBus as the underlying in-memory event bus. Alternatively the AxonFramework's SimpleEventBus can be used.

To specify which, add the configuration property isis.services.eventbus.implementation:

isis.services.eventbus.implementation=guava

or

isis.services.eventbus.implementation=axon

If you have written your own implementation of the EventBusServiceImplementation SPI, then specify instead its fully-qualified class name:

isis.services.eventbus.implementation=com.mycompany.isis.MyEventBusServiceImplementation

In addition, there is one further configuration property, whether to allow "late registration":

isis.services.eventbus.allowLateRegistration=false

Late registration refers to the idea that a domain service can register itself with the EventBusService after events have been posted. Since domain services are set up at boot time, this almost certainly constitutes a bug in the code and so by default late registration is not allowed. Setting the above property to true disables this check.

3.9.9. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of EventBusService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

The EventBusService is intended for fine-grained publish/subscribe for object-to-object interactions within an Isis domain object model. The event propagation is strictly in-memory, and there are no restrictions on the object acting as the event (it need not be serializable, for example).

The PublishingService meanwhile is intended for coarse-grained publish/subscribe for system-to-system interactions, from Isis to some other system. Here the only events published are those that action invocations (for actions annotated with @Action#publishing()) and of changed objects (for objects annotated with @DomainObject#publishing()).

3.10. GuiceBeanProvider

The GuiceBeanProvider domain service acts as a bridge between Isis' Wicket viewer internal bootstrapping using Google Guice.

This service operates at a very low-level, and you are unlikely to have a need for it. It is used internally by the framework, in the default implementation of the DeepLinkService.

Currently Isis uses a combination of Guice (within the Wicket viewer only) and a home-grown dependency injection framework. In future versions we intended to refactor the framework to use CDI throughout. At that time this service is likely to become redundant because we will allow any of the internal components of Isis to be injected into your domain object code.

3.10.1. API & Implementation

The API defined by this service is:

public interface GuiceBeanProvider {
    @Programmatic
    <T> T lookup(Class<T> beanType);
    @Programmatic
    <T> T lookup(Class<T> beanType, final Annotation qualifier);
}

The Wicket viewer this provides an implementation of this service.

3.10.2. Usage

Using the Wicket viewer requires subclassing of IsisWicketApplication. In the subclass it is commonplace to override newIsisWicketModule(), for example:

@Override
protected Module newIsisWicketModule() {
    final Module isisDefaults = super.newIsisWicketModule();
    final Module overrides = new AbstractModule() {
        @Override
        protected void configure() {
            bind(String.class).annotatedWith(Names.named("applicationName"))
                              .toInstance("ToDo App");
            bind(String.class).annotatedWith(Names.named("applicationCss"))
                              .toInstance("css/application.css");
            bind(String.class).annotatedWith(Names.named("applicationJs"))
                              .toInstance("scripts/application.js");
            ...
        }
    };
    return Modules.override(isisDefaults).with(overrides);
}

This "module" is in fact a Guice module, and so the GuiceBeanProvider service can be used to lookup any of the components bound into it.

For example:

public class SomeDomainObject {
    private String lookupApplicationName() {
        return guiceBeanProvider.lookup(String.class, Names.named("applicationName"));
    }
    @Inject
    GuiceBeanProvider guiceBeanProvider;
}

should return "ToDo App".

3.10.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

and that the Wicket viewer is being used then an implementation of GuiceBeanProvider is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

3.11. IsisJdoSupport

The IsisJdoSupport service provides a number of general purpose methods for working with the JDO/DataNucleus objectstore. In general these act at a lower-level of abstraction than the APIs normally used (specifically, those of DomainObjectContainer), but nevertheless deal with some of the most common use cases. For service also provides access to the underlying JDO PersistenceManager for full control.

The following sections discuss the functionality provided by the service, broken out into categories.

3.11.1. Executing SQL

You can use the IsisJdoSupportService to perform arbitrary SQL SELECTs or UPDATEs:

public interface IsisJdoSupport {
    @Programmatic
    List<Map<String, Object>> executeSql(String sql);
    @Programmatic
    Integer executeUpdate(String sql);
    ...
}

The executeSql(…​) method allows arbitrary SQL SELECT queries to be submitted:

List<Map<String, Object>> results = isisJdoSupport.executeSql("select * from custMgmt.customers");

The result set is automatically converted into a list of maps, where the map key is the column name.

In a similar manner, the executeUpdate(…​) allows arbitrary SQL UPDATEs to be performed.

int count = isisJdoSupport.executeUpdate("select count(*) from custMgmt.customers);

The returned value is the number of rows updated.

As an alternative, consider using DataNucleus' type-safe JDO query API, discussed below.

3.11.2. Type-safe JDOQL Queries

DataNucleus provides an extension to JDO, so that JDOQL queries can be built up and executed using a set of type-safe classes.

The types in question for type safe queries are not the domain entities, but rather are companion "Q…​" query classes. These classes are generated dynamically by an annotation processor as a side-effect of compilation, one "Q…​" class for each of the @PersistenceCapable domain entity in your application. For example, a ToDoItem domain entity will give rise to a QToDoItem query class. These "Q…​" classes mirror the structure of domain entity, but expose properties that allow predicates to be built up for querying instances, as well as other functions in support of order by. group by and other clauses.

Most IDEs (including IntelliJ and Eclipse) enable annotation processing by default, as does Maven. The DataNucleus' documentation offers some guidance on confirming that APT is enabled.

The IsisJdoSupport service offers two methods at different levels of abstraction:

public interface IsisJdoSupport {
    @Programmatic
    <T> List<T> executeQuery(final Class<T> cls, final BooleanExpression be);
    @Programmatic
    <T> TypesafeQuery<T> newTypesafeQuery(Class<T> cls);
    ...
}

The executeQuery(…​) method supports the common case of obtaining a set of objects that meet some criteria, filtered using the provided BooleanExpression. To avoid memory leaks, the returned list is cloned and the underlying query closed.

For example, in the (non-ASF) Isis addons' todoapp there is an implementation of ToDoItemRepository using type-safe queries. The following JDOQL:

SELECT
FROM todoapp.dom.module.todoitem.ToDoItem
WHERE atPath.indexOf(:atPath) == 0
   && complete == :complete"

can be expressed using type-safe queries as follows:

public List<ToDoItem> findByAtPathAndCategory(final String atPath, final Category category) {
    final QToDoItem q = QToDoItem.candidate();
    return isisJdoSupport.executeQuery(ToDoItem.class,
            q.atPath.eq(atPath).and(
            q.category.eq(category)));
}

You can find the full example of the JDOQL equivalent in the DomainObjectContainer

The newTypesafeQuery(…​) method is a lower-level API that allows a type safe query to be instantiated for most sophisticated querying, eg using group by or order by clauses. See the DataNucleus documentation for full details of using this.

One thing to be aware of is that after the query has been executed, it should be closed, using query.closeAll(). If calling query.executeList() we also recommend cloning the resultant list first. The following utility method does both of these tasks:

private static <T> List<T> executeListAndClose(final TypesafeQuery<T> query) {
    final List<T> elements = query.executeList();
    final List<T> list = Lists.newArrayList(elements);
    query.closeAll();
    return list;
}

3.11.3. Fixture support

When writing integration tests you’ll usually need to tear down some/all mutable transactional data before each test. One way to do that is to use the executeUpdate(…​) method described above.

Alternatively, the deleteAll(…​) method will let your test delete all instances of a class without resorting to SQL:

public interface IsisJdoSupport {
    @Programmatic
    void deleteAll(Class<?>... pcClasses);
    ...
}

For example:

public class TearDownAll extends FixtureScriptAbstract {
    @Override
    protected void execute(final ExecutionContext ec) {
        isisJdoSupport.deleteAll(Order.class);
        isisJdoSupport.deleteAll(CustomerAddress.class);
        isisJdoSupport.deleteAll(Customer.class);
    }
    @Inject
    IsisJdoSupport isisJdoSupport;
}

It can occasionally be the case that Isis' internal adapter for the domain object is still in memory. JDO/DataNucleus seems to bump up the version of the object prior to its deletion, which under normal circumstances would cause Isis to throw a concurrency exception. Therefore to prevent this from happening (ie to force the deletion of all instances), concurrency checking is temporarily disabled while this method is performed.

3.11.4. Reloading entities

An (intentional) limitation of JDO/DataNucleus is that persisting a child entity (in a 1:n bidirectional relationship) does not cause the parent’s collection to be updated.

public interface IsisJdoSupport {
    @Programmatic
    <T> T refresh(T domainObject);
    @Programmatic
    void ensureLoaded(Collection<?> collectionOfDomainObjects);
    ...
}

The refresh(T domainObject) method can be used to reload the parent object (or indeed any object). Under the covers it uses the JDO PersistenceManager#refresh(…​) API.

For example:

@DomainService(nature=NatureOfService.VIEW_CONTRIBUTIONS_ONLY)
public class OrderContributions {
    public Order newOrder(final Customer customer) {
        Order order = newTransientInstance(Order.class);
        order.setCustomer(customer);
        container.persist(customer);
        container.flush();                  (1)
        isisJdoSupport.refresh(customer);   (2)
        return order;
    }
    @Inject
    DomainObjectContainer container;
    @Inject
    IsisJdoSupport isisJdoSupport;
}
1 flush to database, ensuring that the database row corresponding to the Order exists in its order table.
2 reload the parent (customer) from the database, so that its collection of Orders is accurate.

The particular example that led to this method being added was a 1:m bidirectional relationship, analogous to Customer 1←→* Order. Persisting the child Order object did not cause the parent Customer's collection of orders to be updated. In fact, JDO does not make any such guarantee to do so. Options are therefore either to maintain the collection in code, or to refresh the parent.

The ensureLoaded(…​) method allows a collection of domain objects to be loaded from the database in a single hit. This can be valuable as a performance optimization to avoid multiple roundtrips to the database. Under the covers it uses the PersistenceManager#retrieveAll(…​) API.

3.11.5. JDO PersistenceManager

The functionality provided by IsisJdoSupport focus only on the most common use cases. If you require more flexibility than this, eg for dynamically constructed queries, then you can use the service to access the underlying JDO PersistenceManager API:

public interface IsisJdoSupport {
    @Programmatic
    PersistenceManager getJdoPersistenceManager();
    ...
}

For example:

public List<Order> findOrders(...) {
    javax.jdo.PersistenceManager pm = isisJdoSupport.getPersistenceManager();

    // knock yourself out...

    return someListOfOrders;
}

3.11.6. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of IsisJdoSupport service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

3.12. MementoService

The MementoService was originally introduced to simplify the implementation of ViewModels which are required by the framework to return string representation of all of their backing state, moreover which is safe for use within a URL.

However, it can also be used to create a memento of arbitrary objects. Indeed, it is used internally by the core implementation of BackgroundService to capture the state of action invocations so that they can be executed by a background process.

3.12.1. API & Implementation

The API defined by MementoService is:

public interface MementoService {
    public static interface Memento {
        @Programmatic
        public Memento set(String name, Object value);
        @Programmatic
        public <T> T get(String name, Class<T> cls);
        @Programmatic
        public String asString();
        public Set<String> keySet();
    }
    @Programmatic
    public Memento create();
    @Programmatic
    public Memento parse(final String str);
    @Programmatic
    public boolean canSet(Object input);
}

The core framework provides a default implementation of this API, namely o.a.i.core.runtime.services.memento.MementoServiceDefault. The string returned (from Memento#asString()) is a base-64 URL encoded representation of the underlying format (an XML string).

In fact, the MementoServiceDefault implementation does provide a mechanism to disable the URL encoding, but this is not part of the MementoService public API. Note also that the encoding method is not pluggable.

However, you are of course free to write some other implementation of MementoService, perhaps based on MementoServiceDefault code if you wish.

The types of objects that are supported by the MementoService are implementation-specific, but would typically include all the usual value types as well as Isis' Bookmark class (to represent references to arbitrary entities). Nulls can also be set.

In the case of the default implementation provided by the core framework, the types supported are:

  • java.lang.String

  • java.lang.Boolean, boolean

  • java.lang.Byte, byte

  • java.lang.Short, short

  • java.lang.Integer, int

  • java.lang.Long, long

  • java.lang.Float, float

  • java.lang.Double, double

  • java.lang.Character, char

  • java.math.BigDecimal

  • java.math.BigInteger

  • org.joda.time.LocalDate

  • org.apache.isis.applib.services.bookmark.Bookmark

If using another implementation, the canSet(…​) method can be used to check if the candidate object’s type is supported.

3.12.2. Usage

As noted in the introduction, a common use case for this service is in the implementation of the ViewModel interface.

Rather than implementing ViewModel, it’s usually easier to annotate your view models with @ViewModel (or equivalently @DomainObject#nature=EXTERNAL_ENTITY or @DomainObject#nature=INMEMORY_ENTITY.

For example, suppose you were implementing a view model that represents an external entity in a SOAP web service. To access this service the view model needs to store (say) the hostname, port number and an id to the object.

Using an injected MementoService the view model can roundtrip to and from this string, thus implementing the ViewModel API:

public class ExternalEntity implements ViewModel {
    private String hostname;
    private int port;
    private String id;
    public String viewModelMemento() {              (1)
        return mementoService.create()
                .set("hostname", hostname)
                .set("port", port)
                .set("id", id)
                .asString();
    }
    public void viewModelInit(String mementoStr) {  (2)
        Memento memento = mementoService.parse(mementoStr);
        hostname = memento.get("hostname", String.class);
        port = memento.get("port", int.class);
        id = memento.get("id", String.class);
    ...
    @Inject
    MementoService mementoService;
}
1 part of the ViewModel API
2 part of the ViewModel API

The memento service is used by the CommandContext service and also BackgroundCommandService. These both use a memento to capture a representation of an action invocation.

3.12.4. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of MementoService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

3.13. MetamodelService

The MetaModelService provides access (albeit currently extremely limited) to aspects of Isis' internal metamodel.

Currently this is limited to looking up the object type (as specified in @DomainObject#objectType() and equivalent mechanisms, and as used in Bookmarks and elsewhere) from an object’s class, and vice versa. In the future we expect other aspects of the metamodel to also be formally surfaced through this API.

3.13.1. API & Implementation

The API defined by the service is:

public interface MetaModelService {
    @Programmatic
    Class<?> fromObjectType(final String objectType);   (1)
    @Programmatic
    String toObjectType(final Class<?> domainType);     (2)
}
1 reverse lookup of a domain class' object type
2 lookup of a domain class' object type

3.13.2. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of MetamodelService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

3.14. QueryResultsCache

The purpose of the QueryResultsCache is to improve response times to the user, by providing a short-term (request-scoped) cache of the value of some (safe or idempotent) method call. This will typically be as the result of running a query, but could be any expensive operation.

Caching such values is useful for code that loops "naively" through a bunch of stuff, performing an expensive operation each time. If the data is such that the same expensive operation is made many times, then the query cache is a perfect fit.

This service was inspired by similar functionality that exists in relational databases, for example Sybase’s subquery results cache and Oracle’s result_cache hint.

3.14.1. API & Implementation

The API defined by QueryResultsCache is:

@RequestScoped
public class QueryResultsCache {
    public static class Key {
        public Key(Class<?> callingClass, String methodName, Object... keys) {...}
        public Class<?> getCallingClass() { ... }
        public String getMethodName() { ... }
        public Object[] getKeys() { ... }
    }
    public static class Value<T> {
        public Value(T result) { ... }
        private T result;
        public T getResult() {
            return result;
        }
    }
    @Programmatic
    public <T> T execute(
        final Callable<T> callable,
        final Class<?> callingClass, final String methodName, final Object... keys) { ... }
    @Programmatic
    public <T> T execute(final Callable<T> callable, final Key cacheKey) { ... }
    @Programmatic
    public <T> Value<T> get(
        final Class<?> callingClass, final String methodName, final Object... keys) { ... }
    @Programmatic
    public <T> Value<T> get(final Key cacheKey) { ... }
    @Programmatic
    public <T> void put(final Key cacheKey, final T result) { ... }
}

This class (o.a.i.applib.services.queryresultscache.QueryResultsCache) is also the implementation.

3.14.2. Usage

Suppose that there’s a TaxService that calculates tax on Taxable items, with respect to some TaxType, and for a given LocalDate. To calculate tax it must run a database query and then perform some additional calculations.

Our original implementation is:

@DomainService
public class TaxService {
    public BigDecimal calculateTax(
            final Taxable t, final TaxType tt, final LocalDate d) {
        // query against DB using t, tt, d
        // further expensive calculations
    }
}

Suppose now that this service is called in a loop, for example iterating over a bunch of orders, where several of those orders are for the same taxable products, say. In this case the result of the calculation would always be the same for any given product.

We can therefore refactor the method to use the query cache as follows:

public class TaxService {
    public BigDecimal calculateTax(
            final Taxable t, final TaxType tt, final LocalDate d) {
        return queryResultsCache.execute(
            new Callable<BigDecimal>(){                         (1)
                public BigDecimal call() throws Exception {
                     // query against DB using t, tt, d
                     // further expensive calculations
                }
            },
            TaxService.class,                                   (2)
            "calculateTax",
            t, tt, d);
        }
}
1 the Callable is the original code
2 the remaining parameters in essence uniquely identify the method call.

This refactoring will be worthwhile provided that enough of the orders being processed reference the same taxable products. If however every order is for a different product, then no benefit will be gained from the refactoring.

3.14.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of QueryResultsCache service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

The Scratchpad service is also intended for actions that are called many times, allowing arbitrary information to be shared between them. Those methods could be called from some outer loop in domain code, or by the framework itself if the action invoked has the @Action#invokeOn() annotation attribute set to OBJECT_AND_COLLECTION or COLLECTION_ONLY.

3.15. Scratchpad

The Scratchpad service is a request-scoped service to allow objects to exchange information even if they do not directly call each other.

3.15.1. API & Implementation

The API of Scratchpad service is:

@RequestScoped
public class Scratchpad {
    @Programmatic
    public Object get(Object key) { ... }
    @Programmatic
    public void put(Object key, Object value) { ... }
    @Programmatic
    public void clear() { ... }
}

This class (o.a.i.applib.services.scratchpad.Scratchpad) is also the implementation. And, as you can see, the service is just a request-scoped wrapper around a java.util.Map.

3.15.2. Usage

The most common use-case is for bulk actions that act upon multiple objects in a list. The (same) Scratchpad service is injected into each of these objects, and so they can use pass information.

For example, the Isis addons example todoapp (not ASF) demonstrates how the Scratchpad service can be used to calculate the total cost of the selected `ToDoItem`s:

@ActionSemantics(Of.SAFE)
@Bulk(AppliesTo.BULK_ONLY)
public BigDecimal totalCost() {
    BigDecimal total = (BigDecimal) scratchpad.get("runningTotal");
    if(getCost() != null) {
        total = total != null ? total.add(getCost()) : getCost();
        scratchpad.put("runningTotal", total);
    }
    return total.setScale(2);
}
@Inject
Scratchpad scratchpad;

A more complex example could use a view model to enable bulk updates to a set of objects. The view model’s job is to gather track of the items to be updated:

public class ToDoItemUpdateBulkUpdate extends AbstractViewModel {
    private List<ToDoItem> _items = ...;
    public ToDoItemBulkUpdate add(ToDoItem item) {
        _items.add(item);
        return this;
    }
    ...                 (1)
}
1 not shown - the implementation of ViewModel for converting the list of _items into a string.

The bulk action in the objects simply adds the selected item to the view model:

@Action(
    invokeOn=InvokeOn.COLLECTIONS_ONLY
    semantics=SemanticsOf.SAFE
)
public ToDoItemBulkUpdate bulkUpdate() {
    return lookupBulkUpdateViewModel().add(this);
}
private ToDoItemBulkUpdate lookupBulkUpdateViewModel() {
    ToDoItemBulkUpdate bulkUpdate =
        (ToDoItemBulkUpdate) scratchpad.get("bulkUpdateViewModel");     (1)
    if(bulkUpdate == null) {
        bulkUpdate = container.injectServicesInto(new ToDoItemBulkUpdate());
        scratchpad.put("bulkUpdateViewModel", bulkUpdate);              (2)
    }
    return bulkUpdate;
}
@Inject
Scratchpad scratchpad;
1 look for the ToDoItemBulkUpdate in the scratchpad…​
2 …​ and add one if there isn’t one (ie for the first object returned).

If using the Wicket viewer, the ToDoItemBulkUpdate view model returned from the last action invoked will be displayed. Thereafter this view model can be used to perform a bulk update of the "enlisted" items.

3.15.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of Scratchpad service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

The ActionInteractionContext service allows bulk actions to co-ordinate with each other.

The QueryResultsCache is useful for caching the results of expensive method calls.

3.16. SudoService

The SudoService allows the current user reported by the DomainObjectContainer to be temporarily changed to some other user. This is useful both for integration testing (eg if testing a workflow system whereby objects are moved from one user to another) and while running fixture scripts (eg setting up objects that would normally require several users to have acted upon the objects).

3.16.1. API & Implementation

The API provided by the service is:

public interface SudoService {
    @Programmatic
    void sudo(String username, final Runnable runnable);
    @Programmatic
    <T> T sudo(String username, final Callable<T> callable);
    @Programmatic
    void sudo(String username, List<String> roles, final Runnable runnable);
    @Programmatic
    <T> T sudo(String username, List<String> roles, final Callable<T> callable);
}

which will run the provided block of code (a Runnable or a Callable) in a way such that calls to DomainObjectContainer#getUser() will return the specified user (and roles, if specified)

The core framework provides a default implementation of this service (o.a.i.core.runtime.services.sudo.SudoServiceDefault)

3.16.2. Usage

A good example can be found in the (non-ASF) Isis addons' todoapp which uses the SudoService in a fixture script to set up ToDoItem objects:

protected void execute(final ExecutionContext ec) {
    ...
    sudoService.sudo(getUsername(),
            new Runnable() {
                @Override
                public void run() {
                    wrap(toDoItem).completed();
                }
            });
    ...
}

3.16.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of SudoService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

3.17. WrapperFactory

The WrapperFactory provides the ability to enforce business rules for programmatic interactions between domain objects. If there is a (lack-of-) trust boundary between the caller and callee — eg if they reside in different modules — then the wrapper factory is a useful mechanism to ensure that any business constraints defined by te callee are honoured.

For example, if the calling object attempts to modify an unmodifiable property on the target object, then an exception will be thrown. Said another way: interactions are performed "as if" they are through the viewer.

For a discussion of the use of the WrapperFactory within integration tests (the primary or at least original use case for this service) can be found here

This capability goes beyond enforcing the (imperative) constraints within the hideXxx(), disableXxx() and validateXxx() supporting methods; it also enforces (declarative) constraints such as those represented by annotations, eg @MaxLength or @Regex.

This capability is frequently used within integration tests, but can also be used in production code. (There are analogies that can be drawn here with the way that JEE beans can interact through an EJB local interface).

3.17.1. API

The API provided by the service is:

public interface WrapperFactory {
    @Programmatic
    <T> T wrap(T domainObject);                             (1)
    @Programmatic
    <T> T unwrap(T possibleWrappedDomainObject);            (2)
    @Programmatic
    <T> boolean isWrapper(T possibleWrappedDomainObject);   (3)

    public static enum ExecutionMode {                      (4)
        EXECUTE(true,true),
        SKIP_RULES(false, true),                            (5)
        NO_EXECUTE(true, false);                            (6)
    }
    @Programmatic
    <T> T wrap(T domainObject, ExecutionMode mode);         (7)
    @Programmatic
    <T> T wrapNoExecute(T domainObject);                    (8)
    @Programmatic
    <T> T wrapSkipRules(T domainObject);                    (9)
    ...
 }
1 wraps the underlying domain object. If it is already wrapped, returns the object back unchanged.
2 Obtains the underlying domain object, if wrapped. If the object is not wrapped, returns back unchanged.
3 whether the supplied object has been wrapped.
4 enumerates how the wrapper interacts with the underlying domain object.
5 validate all business rules and then execute.
6 skip all business rules and then execute (including creating commands and firing pre- and post-execute domain events).
7 validate all business rules (including those from domain events) but do not execute.
8 convenience method to invoke wrap(…​) with ExecuteMode#NO_EXECUTE (make this feature more discoverable)
9 convenience method to invoke wrap(…​) with ExecuteMode#SKIP_RULES (make this feature more discoverable)

The service works by returning a "wrapper" around a supplied domain object (a javassist proxy), and it is this wrapper that ensures that the hide/disable/validate rules implies by the Isis programming model are enforced. The wrapper can be interacted with as follows:

  • a get…​() method for properties or collections

  • a set…​() method for properties

  • an addTo…​() or removeFrom…​() method for collections

  • any action

Calling any of the above methods may result in a (subclass of) InteractionException if the object disallows it. For example, if a property is annotated with @Hidden then a HiddenException will be thrown. Similarly if an action has a validateXxx() method and the supplied arguments are invalid then an InvalidException will be thrown.

In addition, the following methods may also be called:

An exception will be thrown if any other methods are thrown.

3.17.2. Usage

The caller will typically obtain the target object (eg from some repository) and then use the injected WrapperFactory to wrap it before interacting with it.

For example:

public class CustomerAgent {
    @Action
    public void refundOrder(final Order order) {
        final Order wrappedOrder = wrapperFactory.wrap(order);
        try {
            wrappedOrder.refund();
        } catch(InteractionException ex) {          (1)
            container.raiseError(ex.getMessage());  (2)
            return;
        }
    }
    ...
    @Inject
    WrapperFactory wrapperFactory;
    @Inject
    DomainObjectContainer container;
}
1 if any constraints on the Order’s `refund() action would be violated, then …​
2 …​ these will be trapped and raised to the user as a warning.

It ought to be possible to implement an ExceptionRecognizers that would allow the above boilerplate to be removed. This recognizer service would recognize the InteractionException and convert to a suitable message.

At the time of writing Isis does not provide an out-of-the-box implementation of such an ExceptionRecognizer; but it should be simple enough to write one…

3.17.3. Listener API

The WrapperFactory also provides a listener API to allow other services to listen in on interactions.

public interface WrapperFactory {
    ...
    @Programmatic
    List<InteractionListener> getListeners();                               (1)
    @Programmatic
    public boolean addInteractionListener(InteractionListener listener);    (2)
    @Programmatic
    public boolean removeInteractionListener(InteractionListener listener); (3)
    @Programmatic
    public void notifyListeners(InteractionEvent ev);                       (4)
}
1 all InteractionListeners that have been registered.
2 registers an InteractionListener, to be notified of interactions on all wrappers. The listener will be notified of interactions even on wrappers created before the listener was installed. (From an implementation perspective this is because the wrappers delegate back to the container to fire the events).
3 remove an InteractionListener, to no longer be notified of interactions on wrappers.
4 used by the framework itself

The original intent of this API was to enable test transcripts to be captured (in a BDD-like fashion) from integration tests. No such feature has yet been implemented however. Also, the capabilities have by and large been superceded by Isis' support for domain events. We may therefore deprecate this API in the future.

3.17.4. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of WrapperFactory service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

3.18. XmlSnapshotService

The XmlSnapshotService provides the capability to generate XML snapshots (and if required corresponding XSD schemas) based on graphs of domain objects.

Typical use cases include creating mementos for business-focused auditing, such that a report could be generated as to which end-user performed a business action (perhaps for legal reasons). For one system that we know of, a digest of this snapshot of data is signed with the public encryption key so as to enforce non-repudiation.

Another use case is to grab raw data such that it could be merged into a report template or communication.

The service offers a basic API to create a snapshot of a single object, and an more flexible API that allows the size of the graph to be customized.

The core framework provides an implementation of this service (o.a.i.core.runtime.services.xmlsnapshot.XmlSnapshotServiceDefault).

3.18.1. Standard API

The (basic) API of XmlSnapshotService is:

public interface XmlSnapshotService {
    public interface Snapshot {
        Document getXmlDocument();
        Document getXsdDocument();
        String getXmlDocumentAsString();
        String getXsdDocumentAsString();
    }
    @Programmatic
    public XmlSnapshotService.Snapshot snapshotFor(Object domainObject);
    ...
}

The most straight-forward usage of this service is simply:

XmlSnapshot snapshot = xmlsnapshotService.snapshotFor(customer);
Element customerAsXml = snapshot.getXmlElement();

This will return an XML (document) element that contains the names and values of each of the customer’s value properties, along with the titles of reference properties, and also the number of items in collections.

As well as obtaining the XML snapshot, it is also possible to obtain an XSD schema that the XML snapshot conforms to.

XmlSnapshot snapshot = ...;
Element customerAsXml = snapshot.getXmlElement();
Element customerXsd = snapshot.getXsdElement();

This can be useful for some tools. For example, Altova Stylevision can use the XML and XSD to transform into reports. Please note that this link does not imply endorsement (nor even a recommendation that this is a good design).

3.18.2. Builder API

The contents of the snapshot can be adjusted by including "paths" to other references or collections. To do this, the builder is used. The API for this is:

public interface XmlSnapshotService {
    ...
    public interface Builder {
        void includePath(final String path);
        void includePathAndAnnotation(String path, String annotation);
        XmlSnapshotService.Snapshot build();
    }
    @Programmatic
    public XmlSnapshotService.Builder builderFor(Object domainObject);
}

We start by obtaining a builder:

XmlSnapshot.Builder builder = xmlsnapshotService.builderFor(customer);

Suppose now that we want the snapshot to also include details of the customer’s address, where address in this case is a reference property to an instance of the Address class. We can "walk-the-graph" by including these references within the builder.

builder.includePath("address");

We could then go further and include details of every order in the customer’s orders collection, and details of every product of every order:

builder.includePath("orders/product");

When all paths are included, then the builder can build the snapshot:

XmlSnapshot snapshot = builder.build();
Element customerAsXml = snapshot.getXmlElement();

All of this can be strung together in a fluent API:

Element customerAsXml = xmlsnapshotService.builderFor(customer)
                        .includePath("address")
                        .includePath("orders/product")
                        .build()
                        .getXmlElement();

As you might imagine, the resultant XML document can get quite large very quickly with only a few "include"s.

If an XSD schema is beng generated (using snapshot.getXsdElement() then note that for the XSD to be correct, the object being snapshotted must have non-null values for the paths that are `include()’d. If this isn’t done then the XSD will not be correct reflect for another snapshotted object that does have non-null values.

3.18.3. Automatic inclusions

If the domain object being snapshotted implements the SnapshottableWithInclusions interace, then this moves the responsibility for determining what is included within the snapshot from the caller to the snapshottable object itself:

public interface SnapshottableWithInclusions extends Snapshottable {
    List<String> snapshotInclusions();
}

If necessary, both approaches can be combined.

As an alternative to using include(), you might consider building a view model domain object which can reference only the relevant information required for the snapshot. For example, if only the 5 most recent Orders for a Customer were required, a CustomerAndRecentOrders view model could hold a collection of just those 5 Orders. Typically such view models would implement SnapshottableWithInclusions.

One reason for doing this is to provide a stable API between the domain model and whatever it is that might be consuming the XML. With a view model you can refactor the domain entities but still preserve a view model such that the XML is the same.

3.18.4. Convenience API

The XmlSnapshotService also provides some API for simply manipulating XML:

public interface XmlSnapshotService {
    ...
    @Programmatic
    public Document asDocument(String xmlStr);                          (1)
    @Programmatic
    public <T> T getChildElementValue(                                  (2)
                    Element el, String tagname, Class<T> expectedCls);
    @Programmatic
    public Element getChildElement(                                     (3)
                    Element el, String tagname);
    @Programmatic
    public String getChildTextValue(Element el);                        (4)
}
1 is a convenience method to convert xml string back into a W3C Document
2 is a convenience method to extract the value of an XML element, based on its type.
3 is a convenience method to walk XML document.
4 is a convenience method to obtain value of child text node.

3.18.5. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of XmlSnapshotService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

The BookmarkService provides a mechanism for obtaining a string representations of a single domain object.

The MementoService also provides a mechanism for generating string representations of domain objects.

4. Domain Services SPI

Apache Isis includes an extensive number of domain services for your domain objects to use; these are listed in Reference: Domain Services (API) chapter.

There are other domain services that constitute not an API but an SPI; if present they are used by Isis itself rather than by your domain objects. A good example of this is the AuditingService service; if an implementation is present then Isis will call that service in order to record audit log entries. If no implementation is available then Isis will carry on regardless.

Some domain services can be considered both API and SPI; a good example is the EmailService that is of direct use for domain objects wishing to send out emails, but is also used by the framework to support the user registration functionality supported by the Wicket viewer. The same is true of the EventBusService; this can be used by domain objects to broadcast arbitrary events, but is also used by the framework to automatically emit events for @Action#domainEvent() etc.

For these hybrid services we have categorized the service as an "API" service. This chapter therefore contains only the strictly SPI services.

The table below lists the SPIs that are defined in the Isis applib (o.a.i.core:isis-core-applib module).

Table 27. Applib SPI Services
SPI Description Implementation Notes

o.a.i.applib.
services.audit
AuditingService3

Create an audit record for every changed property of every changed object within a transaction.

AuditingService
o.ia.m.audit
isis-module-audit

related services: AuditingService-
Contributions, AuditingService-
Repository

o.a.i.applib.
services.background
BackgroundCommandService

Persisted a memento of an action invocation such that it can be executed asynchronously ("in the background") eg by a scheduler.

BackgroundCommandServiceJdo
o.ia.m.command
isis-module-command

related services: BackgroundCommandService-
JdoContributions, BackgroundCommandService-
JdoRepository

o.a.i.applib.
services.classdiscovery
ClassDiscoveryService

Mechanism to locate (from the classpath) classes with a specific annotation (eg @DomainService)

Subtypes of a given type (eg FixtureScript).

ClassDiscoveryService-
UsingReflections
o.a.i.core
isis-core-applib

requires org.reflections:reflections as Maven dependency

o.a.i.applib.
services.command.spi
CommandService

Service to act as a factory and repository (create and save) of command instances, ie representations of an action invocation. Used for command/auditing and background services.

CommandServiceJdo
o.ia.m.command
isis-module-command

related services:
CommandService- `JdoContributions`, `CommandService-` JdoRepository

o.a.i.applib.
services.userreg
EmailNotificationService

Notify a user during self-registration of users.

EmailNotificationService-
Default
o.a.i.core
isis-core-runtime

depends on:
a configured EmailService

o.a.i.applib.
services.publish
EventSerializer

Creates a representation of either an action invocation or a changed object being published through the PublishingService.

RestfulObjects-
SpecEventSerializer
o.ia.m.publishing
isis-module-publishing

o.a.i.applib.
services.exceprecog
ExceptionRecognizer2

Convert certain exceptions (eg foreign or unique key violation in the database) into a format that can be rendered to the end-user.

ExceptionRecognizer-
CompositeFor-
JdoObjectStore
o.a.i.core
isis-core-applib

Extensible using composite pattern if required

o.a.i.applib.
services.i18n
LocaleProvider

Request-scoped service to return the locale of the current user, in support of i18n (ie so that the app’s UI, messages and exceptions can be translated to the required locale by the TranslationService.

LocaleProviderWicket
o.a.i.viewer
isis-viewer-wicket-impl

o.a.i.applib.
services.publish
PublishingService

Publish any action invocations and changed objects, typically for interchange with an external system in a different bounded context.

PublishingService
o.ia.m.publishing
isis-module-publishing

related services: PublishingService- `Contributions`, `PublishingService-` Repository.
depends on:
EventSerializer

o.a.i.applib.
services.i18n
TranslationService

Translate an app’s UI, messages and exceptions for the current user (as per the locale provided by LocalProvider.

TranslationServicePo
o.a.i.core
isis-core-runtime

related services: TranslationServicePoMenu
depends on:
TranslationsResolver, LocaleProvider

o.a.i.applib.
services.i18n
TranslationsResolver

Obtain translations for a particuar phrase and locale, in support of i18n (ie so that the app’s UI, messages and exceptions can be translated to the required locale by the TranslationService

TranslationsResolverWicket
o.a.i.viewer
isis-viewer-wicket-impl

o.a.i.applib.
services.userprof
UserProfileService

Obtain an alternative (usually enriched/customized) name for the current user, to render in the UI.

o.a.i.applib.
services.userreg
UserRegistrationService

Create a new user account with the configured security mechanism.

SecurityModule-
AppUserRegistrationService
o.ia.m.security
isis-module-security

depends (implicitly) on:
a configured EmailService

Key:

  • o.a.i is an abbreviation for org.apache.isis

  • o.ia.m is an abbreviation for org.isisaddons.module

Where an implementation is available (on the classpath) then it is always registered automatically (that is, they are all (with one exception) annotated with @DomainService. The one exception is ExceptionRecognizer, which must be registered explicitly in isis.properties; this makes the service extensible (for new exceptions to be recognized).

There are also some "internal" SPI services whose definitions depend on the internals of the framework (rather than the applib).

We do not guarantee that semantic versioning will be honoured for these APIs.

Table 28. "Internal" SPI Services
SPI Maven Module
Impl’n (g: a:)
Implementation Notes

o.a.i.v.ro.
rendering.service.conneg.
ContentMappingService

(Attempt to) map the returned data into the representation required by the client’s HTTP Accept header.

No default implementation.

o.a.i.v.ro.
rendering.service.conneg.
ContentNegotiationService

Encodes the algorithm that delegates to any registered ContentMappingServices.

ContentNegotiationService-
XRoDomainType
o.a.i.core
isis-core-viewer-restfulobjects-rendering

o.a.i.v.ro.
rendering.service.
RepresentationService

Generates the representations, delegating to any registered ContentNegotiationServices.

RepresentationService-
ForRestfulObjects
o.a.i.core
isis-core-viewer-restfulobjects-rendering

Key:

  • o.a.i.v.ro is an abbreviation for org.apache.isis.viewer.restfulobjects

4.1. AuditingService3

The AuditingService3 auditing service provides a simple mechanism to capture changes to data. It is called for each property that has changed on any domain object, as a set of pre- and post-values.

In case you are wondering what happened to AuditingService and AuditingService2, these were earlier versions of the SPI that have since been deprecated and removed.

4.1.1. SPI

The SPI for the service is:

public interface AuditingService3 {

    @Programmatic
    public void audit(
            final UUID transactionId, String targetClassName, final Bookmark target,
            String memberIdentifier, final String propertyName,
            final String preValue, final String postValue,
            final String user, final java.sql.Timestamp timestamp);
  }

The framework will call this for each and every domain object property that is modified within a transaction.

4.1.2. Implementation

The most full-featured available implementation is the (non-ASF) Isis addons' Audit module. This creates an audit records for each changed property (ie every time that AuditingService3#audit(…​) is called. The implementation is org.isisaddons.module.audit.dom.AuditingService.

The module also provides:

  • AuditingServiceMenu service which provides actions to search for AuditEntrys, underneath an 'Activity' menu on the secondary menu bar.

  • AuditingServiceRepository service to to search for persisted AuditEntry``s. None of its actions are visible in the user interface (they are all `@Programmatic).

  • AuditingServiceContributions which contrbutes collections to the HasTransactionId interface. This will therefore display all audit entries that occurred in a given transaction, in other words whenever a command, a published event or another audit entry is displayed.

If you just want to debug (writing to stderr), you can instead configure o.a.i.applib.services.audit.AuditingService3$Stderr

4.1.3. Usage

The typical way to indicate that an object should be audited is to annotate it with the @DomainObject#auditing() annotation.

4.1.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then adding the (non-ASF) Isis addons' audit module to the classpath will automatically register the AuditingService, AuditingServiceMenu, AuditingServiceRepository and AuditingServiceContributions services. If menu items or contributions are not required in the UI, these can be suppressed either using security or by implementing a vetoing subscriber.

The auditing service works very well with the CommandService. The CommandService captures the _cause of an interaction (an action was invoked, a property was edited), while the AuditingService3 captures the effect of that interaction in terms of changed state.

You may also want to configure the PublishingService.

All three of these services collaborate implicitly by way of the HasTransactionId interface.

4.2. BackgroundCommandService

The BackgroundCommandService (SPI) service supports the BackgroundService (API) service, persisting action invocations as commands such that they can subsequently be invoked in the background.

The BackgroundService is responsible for capturing a memento representing the action invocation, and then hands off to the BackgroundCommandService BackgroundCommandService to actually persist it.

The persisting of commands is only half the story; there needs to be a separate process to read the commands and execute them. The abstract BackgroundCommandExecution provides a mechanism to execute such commands. This can be considered an API, albeit "internal" because the implementation relies on internals of the framework.

4.2.1. SPI

The SPI of the BackgroundCommandService is:

public interface BackgroundCommandService {
    void schedule(
            ActionInvocationMemento aim,        (1)
            Command parentCommand,              (2)
            String targetClassName,
            String targetActionName,
            String targetArgs);

}
1 is a wrapper around a MementoService's Memento, capturing the details of the action invocation to be retained (eg persisted to a database) so that it can be executed at a later time
2 reference to the parent Command requesting the action be performed as a background command. This allows information such as the initiating user to be obtained.

The API of ActionInvocationMemento in turn is:

public class ActionInvocationMemento {
    public String getActionId() { ... }
    public String getTargetClassName() { ... }
    public String getTargetActionName() { ... }
    public Bookmark getTarget() { ... }
    public int getNumArgs() { ... }
    public Class<?> getArgType(int num) throws ClassNotFoundException { ... }
    public <T> T getArg(int num, Class<T> type) { ... }

    public String asMementoString() { ... }     (1)
}
1 lets the BackgroundCommandService implementation convert the action invocation into a simple string.

4.2.2. "Internal" SPI

The BackgroundCommandExecution (in isis-core) is an abstract template class for headless access, that defines an abstract hook method to obtain background `Command`s to be executed:

public abstract class BackgroundCommandExecution
                         extends AbstractIsisSessionTemplate {
    ...
    protected abstract List<? extends Command> findBackgroundCommandsToExecute();
    ...
}

The developer is required to implement this hook method in a subclass.

4.2.3. Implementation

The (non-ASF) Isis addons' command module provides an implementation (org.isisaddons.module.command.dom.BackgroundCommandServiceJdo) that persists Commands using the JDO/DataNucleus object store. It further provides a number of supporting services:

  • org.isisaddons.module.command.dom.BackgroundCommandServiceJdoRepository is a repository to search for persisted background Commands

  • org.isisaddons.module.command.dom.BackgroundCommandServiceJdoContributions contributes actions for searching for persisted child and sibling Commands.

The module also provides a concrete subclass of BackgroundCommandExecution that knows how to query for persisted (background) `Command`s such that they can be executed by a scheduler.

Details of setting up the Quartz scheduler to actually execute these persisted commands can be found on the BackgroundService page.

4.2.4. Usage

Background commands can be created either declaratively or imperatively.

The declarative approach involves annotating an action using @Action#command() with @Action#commandExecuteIn=CommandExecuteIn.BACKGROUND.

The imperative approach involves explicitly calling the BackgroundService from within domain object’s action.

4.2.5. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then adding the (non-ASF) Isis addons' command module to the classpath will automatically register the BackgroundCommandServiceJdo, BackgroundCommandJdoRepository and BackgroundCommandServiceJdoContributions services. If contributions are not required in the UI, these can be suppressed either using security or by implementing a vetoing subscriber.

Note that this module also provides service implementations of the CommandService.

As discussed above, this service supports the BackgroundService , persisting `Command`s such that they can be executed in the background.

There is also a tie-up with the CommandContext and its supporting CommandService domain service. The CommandContext service is responsible for providing a parent Command with which the background Command`s can then be associated as children, while the `CommandService is responsible for persisting those parent Command`s (analogous to the way in which the `BackgroundCommandService persists the child background Command`s). The `BackgroundCommandService ensures that these background Command`s are associated with the parent "foreground" `Command.

What that means is that the implementations of CommandService and BackgroundCommandService go together, hence both implemented in the (non-ASF) Isis addons' command module.).

4.3. ClassDiscoveryService

The ClassDiscovery service is used to automatically discover subclasses of any given type on the classpath. The primary use case is to support "convention-over-configuration" designs that work with a minimum of configuration.

This service is used by the FixtureScripts service to automatically locate any FixtureScript implementations.

4.3.1. SPI

The SPI defined by the service is:

public interface ClassDiscoveryService2  {
    @Programmatic
    <T> Set<Class<? extends T>> findSubTypesOfClasses(Class<T> type, String packagePrefix);
    @Deprecated
    @Programmatic
    <T> Set<Class<? extends T>> findSubTypesOfClasses(Class<T> type);       (1)
}
1 no longer used

4.3.2. Implementation

Isis provides an implementation of this service, namely o.a.i.applib.services.classdiscovery.ClassDiscoveryServiceUsingReflections.

This implementation is also used to discover domain services annotated with @DomainService. Currently this logic uses the implementation directly, so is not pluggable. However, the entire ServicesInstaller

4.3.3. Usage

The usage will vary depending upon the conventions of the design. In the case of fixture scripts, the convention is to implement scripts by subclassing FixtureScript, and to specify the package prefix in the constructor for FixtureScripts.

For example, the (non-ASF) Isis addons' todoapp has its fixture scripts in the todoapp.fixture package. the ToDoAppFixturesService (its subclass of FixtureScripts is in the same package, so its constructor is:

package todoapp.fixture;
...
@DomainService(nature = NatureOfService.VIEW_MENU_ONLY)
public class ToDoAppFixturesService extends FixtureScripts {
    public ToDoAppFixturesService() {
        super(
                ToDoAppFixturesService.class.getPackage().getName(),    (1)
                MultipleExecutionStrategy.EXECUTE);
    }
    ...
}
1 specify the package prefix to search under for fixture scripts

Other designs using this service are likely to work in a similar way, requiring some sort of scope to be specified.

4.3.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of ClassDiscoveryService2 service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

4.4. CommandService

The CommandService service supports the CommandContext service such that Command objects (that reify the invocation of an action on a domain object into an object) can be persisted.

Persistent `Command`s support several use cases:

  • they enable profiling of the running application (which actions are invoked then most often, what is their response time)

  • they act as a parent to any background commands that might be invoked through the BackgroundService

  • if auditing is configured, they provide better audit information, since the Command (the 'cause' of an action) can be correlated to the audit records (the "effect" of the action) by way of the transactionId

  • if publishing is configured, they provide better traceability as the Command is also correlated with any published events, again through the unique transactionId

  • the associated @Action#command() annotation attribute also allows action invocations to be performed in the background. In this case the act of invoking the action on an object instead returns the Command to the user.

The screencast below provides a run-through of the command (profiling) service, auditing service, publishing service. It also shows how commands can be run in the background either explicitly by scheduling through the background service or implicitly by way of a framework annotation.

Note that this screencast shows an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

4.4.1. SPI

The CommandService service defines the following very simple API:

public interface CommandService {
    @Programmatic
    Command create();
    @Programmatic
    void startTransaction(Command command, final UUID transactionId);
    @Programmatic
    void complete(Command command);
    @Programmatic
    boolean persistIfPossible(Command command);
}

where Command is defined as defined by the CommandContext service.

4.4.2. Implementation

The (non-ASF) Isis addons' command module provides an implementation (org.isisaddons.module.command.dom.CommandServiceJdo) that persists Commands using the JDO/DataNucleus object store. It further provides a number of supporting services:

  • org.isisaddons.module.command.dom.CommandServiceJdoRepository is a repository to search for persisted Commands

  • org.isisaddons.module.command.dom.CommandServiceJdoContributions contributes actions for searching for persisted child and sibling Commands.

4.4.3. Usage

The typical way to indicate that an action should be reified into a Command is by annotating the action using @Action#command().

4.4.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then adding the (non-ASF) Isis addons' command module to the classpath will automatically register the CommandServiceJdo, CommandJdoRepository and CommandServiceJdoContributions services. If contributions are not required in the UI, these can be suppressed either using security or by implementing a vetoing subscriber.

Note that this module also provides service implementations of the BackgroundCommandService.

As discussed above, this service supports the CommandContext, providing the ability for Command objects to be persisted. This is closely related to the BackgroundCommandServicethat allows the BackgroundService to schedule commands for background/asynchronous execution.

The implementations of CommandService and BackgroundCommandService are intended to go together, so that persistent parent `Command`s can be associated with their child background `Command`s.

The services provided by this module combines very well with the AuditingService. The CommandService captures the _cause of an interaction (an action was invoked, a property was edited), while the AuditingService3 captures the effect of that interaction in terms of changed state.

You may also want to configure the PublishingService.

All three of these services collaborate implicitly by way of the HasTransactionId interface.

4.5. ContentMappingService

The ContentMappingService supports the (default implementation of the) ContentNegotiationService allowing the RestfulObjects viewer to allow domain objects to be transformed into some other format as specified by the HTTP Accept header.

See ContentNegotiationService for further discussion.

4.5.1. SPI

The SPI defined by this service is:

public interface ContentMappingService {
    @Programmatic
    Object map(Object object,                           (1)
               List<MediaType> acceptableMediaTypes,    (2)
               RepresentationType representationType);  (3)

}
1 typically the input is a domain object (whose structure might change over time), and the output is a DTO (whose structure is guaranteed to be preserved over time)
2 as per the caller’s HTTP Accept header
3 the representation type required (as per the Restful Objects spec).

This is an "internal" SPI, meaning that it uses types that are not part of the Isis applib. We do not guarantee that semantic versioning will be honoured for these APIs.

4.5.2. Implementations

No default implementations are provided by Isis framework itself. However, the (non-ASF) Isis addons' todoapp includes a sample implementation to convert its ToDoItem entity into a ToDoItemDto (JAXB annotated and derived from an XSD). This uses the Orika mapping library.

The source code is:

@DomainService(nature = NatureOfService.DOMAIN)
public class CustomContentMappingService implements ContentMappingService {
    private MapperFactory mapperFactory;                                (1)
    @Programmatic
    @PostConstruct
    public void init() {
        mapperFactory = new DefaultMapperFactory.Builder().build();
        mapperFactory.registerClassMap(
                mapperFactory.classMap(ToDoItem.class, ToDoItemDto.class)   (2)
                        .byDefault()                                        (3)
                        .toClassMap());
        mapperFactory.registerClassMap(
                mapperFactory.classMap(Bookmark.class, OidDto.class)        (4)
                        .field("identifier", "objectIdentifier")            (5)
                        .byDefault() // all other fields are compatible     (6)
                        .toClassMap());
    }
    @Programmatic
    @Override                                                               (7)
    public Object map(
            final Object object,
            final List<MediaType> acceptableMediaTypes,
            final RepresentationType representationType) {
        if(object instanceof ToDoItem) {
            final Bookmark bookmark = bookmarkService.bookmarkFor(object);
            final ToDoItemDto dto =
                mapperFactory.getMapperFacade().map(object, ToDoItemDto.class); (8)
            final OidDto oidDto =
                mapperFactory.getMapperFacade().map(bookmark, OidDto.class);    (9)
            dto.setOid(oidDto);                                                 (10)
            return dto;
        }
        return null;
    }

    @javax.inject.Inject
    private BookmarkService bookmarkService;
}
1 registry of known mappings
2 how to map ToDoItem to ToDoItemDto …​
3 …​ all properties have same name in both source and destination types
4 how to map Bookmark to OidDto …​
5 …​ this property has a different name
6 …​ all other properties have same name
7 the main API to implement. This implementation ignores the acceptableMediaTypes and representationType
8 map the domain object to the DTO
9 and map its oid (by way of the BookmarkService
10 and perform some additional manual wiring (because domain objects don’t know their Oids).

You’ll notice that the implementation doesn’t actually use the acceptableMediaTypes and representationType parameters. That’s because the calling ContentNegotiationServiceXRoDomainType will double check that the returned object is of the correct type (as defined by the x-ro-domain-type parameter of the HTTP Accept header). Since this (example) todoapp only offers a single mapping, there’s therefore no need to for the mapping service to check further.

This service is a companion to the default implementation of the ContentNegotiationService.

4.6. ContentNegotiationService

The ContentNegotiationService is a plug-in point for the RestfulObjects viewer so that it can generate representations according to HTTP Accept header of the request. This idea is discussed in section 34.1 of the Restful Objects spec v1.0.

The principal motivation is to allow more flexible representations to be generated for REST clients that (perhaps through their use of a certain Javascript library, say) expect, or at least works best with, a certain style of representation.

Another use case is to support "third party" REST clients over which you have no control. In this scenario you must not naively expose entities through the RO viewer, because over time those entities will inevitably evolve and change their structure. If the entities were exposed directly then those REST clients will break.

Instead you need to create some sort of stable facade over your domain entities, one which you will preserve even if the domain entities change. There are three ways in which you can do this:

  • first is to solve the problem at the domain layer by defining a regular Isis view model. This is then surfaced over the RO viewer.

    If the underlying entities change, then care must be taken to ensure that structure of the view model nevertheless is unchanged.

  • a second option is to solve the problem at the persistence layer, but defining a (SQL) view in the database and then mapping this to a (read-only) entity. Again this is surfaced by the RO viewer.

    If the underlying tables change (as the result of a change in their corresponding domain entities) then once more the view must be refactored so that it still presents the same structure.

  • our third option is to solve the problem at the presentation layer, using the ContentNegotiationService described in this section.

    The ContentNegotiationService is responsible for inspecting the HTTP Accept header, and use this to select the correct representation to render.

    The Isis framework provides a default implementation of ContentNegotiationService which inspects the "x-ro-domaintype" component of the HTTP Accept header. If present, this implementation will delegate to the companion ContentMappingService service, if configured.

    A typical implementation of ContentMappingService will convert the domain object into some sort of DTO (data transfer object) as specified by the "x-ro-domaintype". If this DTO is annotated with JAXB or Jackson mappings, then the RO viewer (courtesy of the underlying RestEasy framework) can serialize these directly

    What all that means is that, if the underlying entities change, we are required to update the mappings in the ContentMappingService to map to the same DTOs.

This diagram illustrates the three options available:

facade choices

4.6.1. SPI

The SPI defined by this service is:

public interface ContentNegotiationService {
    @Programmatic
    Response.ResponseBuilder buildResponse(                     (1)
            RepresentationService.Context2 renderContext2,
            ObjectAdapter objectAdapter);
    @Programmatic
    Response.ResponseBuilder buildResponse(                     (2)
            RepresentationService.Context2 renderContext2,
            ObjectAndProperty objectAndProperty);
    @Programmatic
    Response.ResponseBuilder buildResponse(                     (3)
            RepresentationService.Context2 renderContext2,
            ObjectAndCollection objectAndCollection);
    @Programmatic
    Response.ResponseBuilder buildResponse(                     (4)
            RepresentationService.Context2 renderContext2,
            ObjectAndAction objectAndAction);
    @Programmatic
    Response.ResponseBuilder buildResponse(                     (5)
            RepresentationService.Context2 renderContext2,
            ObjectAndActionInvocation objectAndActionInvocation);
}
1 representation of a single object, as per section 14.4 of the RO spec, v1.0
2 representation of a single property of an object, as per section 16.4 of the RO spec v1.0
3 representation of a single collection of an object, as per section 17.5 of the RO spec v1.0
4 representation of a single action (prompt) of an object, as per section 18.2 of the RO spec v1.0
5 representation of the results of a single action invocation, as per section 19.5 of the RO spec v1.0

These methods provide:

  • a RepresentationService.Context2 which provides access to request-specific context (eg HTTP headers), session-specific context (eg authentication) and global context (eg configuration settings)

  • an object representing the information to be rendered

    eg ObjectAdapter, ObjectAndProperty, ObjectAndCollection etc

In all cases, returning null will result in the regular RO spec representation being returned.

This is an "internal" SPI, meaning that it uses types that are not part of the Isis applib. We do not guarantee that semantic versioning will be honoured for these APIs.

4.6.2. Implementation

ContentNegotiationServiceAbstract (in o.a.i.v.ro.rendering.service.conneg) provides a no-op implementation of the SPI, along with supporting methods:

public abstract class ContentNegotiationServiceAbstract implements ContentNegotiationService {
    ...
    protected Object objectOf(final ObjectAdapter objectAdapter) { ... }
    protected Object returnedObjectOf(ObjectAndActionInvocation objectAndActionInvocation) { ... }

    protected Class<?> loadClass(String cls) { ... }

    protected void ensureJaxbAnnotated(Class<?> domainType) { ... }
    protected void ensureDomainObjectAssignable(
        String xRoDomainType, Class<?> domainType, Object domainObject) { ... }
}

As discussed in the introduction, the framework also provides a default implementation, o.a.i.v.ro.rendering.service.conneg.ContentNegotiationServiceXRoDomainType. This handles content negotiation for two of the possible representations, object representations and for action result representations:

  • For object representations it will handle requests with HTTP Accept headers of the form:

    • application/json;profile=urn:org.restfulobjects:repr-types/object;x-ro-domain-type=…​

    • application/xml;profile=urn:org.restfulobjects:repr-types/object;x-ro-domain-type=…​

  • for action result representations it will similarly handle requests with HTTP Accept headers of the form:

    • application/json;profile=urn:org.restfulobjects:repr-types/action-result;x-ro-domain-type=…​

    • application/xml;profile=urn:org.restfulobjects:repr-types/action-result;x-ro-domain-type=…​

The value of the x-ro-domain-type parameter corresponds to the DTO to be mapped into by the ContentMappingService.

If the DTO is annotated with JAXB, then also note that the runtime type must be annotated with the JAXB javax.xml.bind.annotation.XmlRootElement so that RestEasy is able to unambiguously serialize it.

4.6.3. Usage

You can find an example of all these services in the (non-ASF) Isis addons' todoapp. This defines a ToDoItemDto class that is JAXB annotated (it is in fact generated from an XSD).

The example app also includes an implementation of ContentMappingService that maps todoapp.dom.module.todoitem.ToDoItem entities to todoapp.dto.module.todoitem.ToDoItemDto classes.

A REST client can therefore request a DTO representation of an entity by invoking

http://localhost:8080/restful/objects/TODO/0

with an Accept header of:

application/xml;profile=urn:org.restfulobjects:repr-types/object;x-ro-domain-type=todoapp.dto.module.todoitem.ToDoItemDto

will result in an XML serialization of that class:

accept xml

while similarly hitting the same URL with an Accept header of:

application/json;profile=urn:org.restfulobjects:repr-types/object;x-ro-domain-type=todoapp.dto.module.todoitem.ToDoItemDto

will result in the JSON serialization of that class:

accept json

4.6.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' default implementation of ContentNegotiationService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

The default implementation of ContentNegotiationService delegates to ContentMappingService (if present) to convert domain entities into a stable form (eg DTO).

The ContentNegotiationService is itself called by the (default implementation of) RepresentationService.

4.7. EmailNotificationService

The EmailNotificationService supports the user registration (self sign-up) features of the Wicket viewer whereby a user can sign-up to access an application by providing a valid email address.

The Wicket viewer will check whether an implementation of this service (and also the UserRegistrationService) is available, and if so will (unless configured not to) expose a sign-up page where the user enters their email address. A verification email is sent using this service; the email includes a link back to the running application. The user then completes the registration process (choosing a user name, password and so on) and the Wicket viewer creates an account for them (using the aforementioned UserRegistrationService).

The role of this service in all of this is to format and send out emails for the initial registration, or for password resets.

The default implementation of this service uses the EmailService, which must be configured in order for user registration to be enabled.

4.7.1. SPI

The SPI defined by this service is:

public interface EmailNotificationService extends Serializable {
    @Programmatic
    boolean send(EmailRegistrationEvent ev);    (1)
    @Programmatic
    boolean send(PasswordResetEvent ev);        (2)
    @Programmatic
    boolean isConfigured();                     (3)
}
1 sends an email to verify an email address as part of the initial user registration
2 sends an email to reset a password for an already-registered user
3 determines whether the implementation was configured and initialized correctly

If isConfigured() returns false then it is not valid to call send(…​) (and doing so will result in an IllegalStateException being thrown.

4.7.2. Implementation

The framework provides a default implementation, o.a.i.core.runtime.services.userreg.EmailNotificationServiceDefault that constructs the emails to send.

Alternative Implementations

The text of these email templates is hard-coded as resources, in other words baked into the core jar files. If you need to use different text then you can of course always write and register your own implementation to be used instead of Isis' default.

If you have configured an alternative email service implementation, it should process the message body as HTML.

If you wish to write an alternative implementation of this service, note that (unlike most Isis domain services) the implementation is also instantiated and injected by Google Guice. This is because EmailNotificationService is used as part of the user registration functionality and is used by Wicket pages that are accessed outside of the usual Isis runtime.

This implies a couple of additional constraints:

  • first, implementation class should also be annotated with @com.google.inject.Singleton

  • second, there may not be any Isis session running. (If necessary, one can be created on the fly using IsisContext.doInSession(…​))

To ensure that your alternative implementation takes the place of the default implementation, register it explicitly in isis.properties.

4.7.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' default implementation of EmailNotificationService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

As noted elsewhere, the default implementation of this service uses EmailService. This service has no specific configuration properties but does require that the EmailService has been configured.

Conversely, this service is used by (Isis' default implementation of) UserRegistrationService.

4.8. EventSerializer

The EmailSerializer service is a supporting service intended for use by (any implementation of) PublishingService. Its responsibility is to combine the EventMetadata and the EventPayload into some serialized form (such as JSON, XML or a string) that can then be published.

See PublishingService for further discussion.

4.8.1. SPI

The SPI defined by this service is:

public interface EventSerializer {
    Object serialize(                   (1)
            EventMetadata metadata,     (2)
            EventPayload payload);      (3)
}
1 returns an object for maximum flexibility, which is then handed off to the PublishingService.
2 standard metadata about the event, such as the user, the transactionId, date/time etc
3 for published actions, will generally be an EventPayloadForActionInvocation (or subclass thereof); for published objects, will generally be an EventPayloadForObjectChanged (or subclass thereof)

It’s important to make sure that the publishing service implementation is able to handle the serialized form. Strings are a good lowest common denominator, but in some cases a type-safe equivalent, such as a w3c DOM Document or JSON node might be passed instead.

4.8.2. Implementation

The (non-ASF) Isis addons' publishing module provides an implementation (org.isisaddons.module.publishing.dom.eventserializer.RestfulObjectsSpecEventSerializer) that represents the event payload using the representation defined by the Restful Objects spec of (transient) objects, grafting on the metadata as additional JSON nodes.

For example, this is the JSON generated on an action invocation:

action invocation published to stderr
Figure 1. JSON representation of a published action invocation

while this is the object change JSON:

changed object published to stderr
Figure 2. JSON representation of a published changed object

You could if you wish change the representation by registering your own implementation of this API in isis.properties:

4.8.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then adding the (non-ASF) Isis addons' publishing module to the classpath will automatically register the RestfulObjectsSpecEventSerializer implementation (and also the module’s PublishingService implementation).

This service is intended (though not mandated) to be used by implementations of PublishingService.

4.9. ExceptionRecognizer

The ExceptionRecognizer service provides the mechanism for both the domain programmer and also for components to be able to recognize and handle certain exceptions that may be thrown by the system. Rather than display an obscure error to the end-user, the application can instead display a user-friendly message.

For example, the JDO/DataNucleus Objectstore provides a set of recognizers to recognize and handle SQL constraint exceptions such as uniqueness violations. These can then be rendered back to the user as expected errors, rather than fatal stacktraces.

It is also possible to provide additional implementations, registered in isis.properties. Unlike other services, where any service registered in isis.properties replaces any default implementations, in the case of this service all implementations registered are "consulted" to see if they recognize an exception (the chain-of-responsibility pattern).

4.9.1. SPI

The SPI defined by this service is:

public interface ExceptionRecognizer2 ... {
    public enum Category {                          (1)
        ...
    }
    public static class Recognition {               (2)
        private Category category;
        private String reason;
        ...
    }
    @Programmatic
    public Recognition recognize2(Throwable ex);    (3)

    @Deprecated
    @Programmatic
    public String recognize(Throwable ex);          (4)

}
1 an enumeration of varies categories of exceptions that are recognised
2 represents the fact that an exception has been recognized as has been converted into a user-friendy message, and has been categorized
3 the main API, to attempt to recognize an exception
4 deprecated API which converted exceptions into strings (reasons), ie without any categorization. This is no longer called.

The categories are:

public interface ExceptionRecognizer2 ... {
    public enum Category {
        CONSTRAINT_VIOLATION,           (1)
        NOT_FOUND,                      (2)
        CONCURRENCY,                    (3)
        CLIENT_ERROR,                   (4)
        SERVER_ERROR,                   (5)
        OTHER                           (6)
    }
    ...
}
1 a violation of some declarative constraint (eg uniqueness or referential integrity) was detected.
2 the object to be acted upon cannot be found (404)
3 a concurrency exception, in other words some other user has changed this object.
4 recognized, but for some other reason…​ 40x error
5 50x error
6 recognized, but uncategorized (typically: a recognizer of the original ExceptionRecognizer API).

In essence, if an exception is recognized then it is also categorized. This lets the viewer act accordingly. For example, if an exception is raised from the loading of an individual object, then this is passed by the registered ExceptionRecognizers. If any of these recognize the exception as representing a not-found exception, then an Isis ObjectNotFoundException is raised. Both the viewers interprets this correctly (the Wicket viewer as a suitable error page, the Restful Objects viewer as a 404 status return code).

If the implementation recognizes the exception then it returns a user-friendly message to be rendered (by the viewer) back to the user; otherwise it returns null. There is no need for the implementation to check for exception causes; the casual chain is unwrapped by Isis core and each exception in the chain will also be passed through to the recognizer (from most specific to least). The recognizer implementation can therefore be as fine-grained or as coarse-grained as it wishes.

4.9.2. Implementation

The framework provides two default implementations:

  • o.a.i.core.metamodel.services.container.DomainObjectContainerDefault provided by Isis core is itself an ExceptionRecognizer, and will handle ConcurrencyExceptions. It will also handle any application exceptions raised by the system (subclasses of o.a.i.applib.RecoverableException).

  • o.a.i.objectstore.jdo.applib.service.exceprecog.ExceptionRecognizerCompositeForJdoObjectStore bundles up a number of more fine-grained implementations:

    • ExceptionRecognizerForSQLIntegrityConstraintViolationUniqueOrIndexException

    • ExceptionRecognizerForJDOObjectNotFoundException

    • ExceptionRecognizerForJDODataStoreException

If you want to recognize and handle additional exceptions (for example to capture error messages specific to the JDBC driver you might be using), then create a fine-grained implementation of ExceptionRecognizer2 for the particular error message (there are some convenience implementations of the interface that you can subclass from if required) and register in isis.properties.

4.9.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then the default implementations provided by the framework (DomainObjectContainerDefault and ExceptionRecognizerCompositeForJdoObjectStore) will be registered.

In addition, you can register any further exception recognizers in isis.properties:

isis.services=...,\
              com.mycompany.myapp.MyExceptionRecognizer,\
              ...

Prior to 1.9.0, the ExceptionRecognizerCompositeForJdoObjectStore also required manual registration:

4.10. LocaleProvider

The LocaleProvider service is one of the services that work together to implement Isis' support for i18n, being used by Isis' default implementation of TranslationService.

The role of the service itself is simply to return the Locale of the current user.

For the "big picture" and further details on Isis' i18n support, see here.

4.10.1. SPI

The SPI defined by this service is:

public interface LocaleProvider {
    @Programmatic
    Locale getLocale();
}

This is notionally request-scoped, returning the Locale of the current user; not that of the server. (Note that the implementation is not required to actually be @RequestScoped, however).

4.10.2. Implementation

Isis' Wicket viewer provides an implementation of this service (LocaleProviderWicket) which leverages Apache Wicket APIs.

Currently there is no equivalent implementation for the RestfulObjects viewer.

4.10.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

and that the Wicket viewer is being used then an implementation of LocaleProvider is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

This service works in conjunction with TranslationService and TranslationsResolver in order to provide i18n support.

4.11. PublishingService

The PublishingService is intended for coarse-grained publish/subscribe for system-to-system interactions, from Isis to some other system. Here the only events published are those that action invocations and of changed objects. A typical use case is to publish onto a pub/sub bus such as ActiveMQ with Camel to keep other systems up to date.

4.11.1. SPI

The SPI defined by the service is:

public interface PublishingService {
    public void publish(
        EventMetadata metadata,                                 (1)
        EventPayload payload);                                  (2)
    @Deprecated
    @Programmatic
    void setEventSerializer(EventSerializer eventSerializer);   (3)
}
1 standard metadata about the event, such as the user, the transactionId, date/time etc
2 for published actions, an EventPayloadForActionInvocation (or subclass thereof); for published objects, an EventPayloadForObjectChanged (or subclass thereof)
3 injects in the EventSerializer service. This is deprecated because not every implementation is required to use an EventSerializer so its inclusion within the SPI of PublishingService was in retrospect a mistake.

Typically implementations will use the injected EventSerializer to convert the metadata and payload into a form to be published:

public interface EventSerializer {
    public Object serialize(EventMetadata metadata, EventPayload payload);
}

The serialized form returned by EventSerializer must be in a form that the PublishingService implementation is able to handle. Strings are a good lowest common denominator, but (if custom implementations of both EventSerializer and PublishingService were in use) then it might also be some other type, for example an org.w3c.dom.Document or an org.json.JSONObject might be returned instead.

4.11.2. Implementation

The (non-ASF) Isis addons' publishing module provides an implementation (org.isisaddons.module.publishing.dom.org.apache.isis.applib.services.publish) that persists each event as a PublishedEvent entity. This holds the serialized form of the event metadata and payload as translated into a string by the injected EventSerializer. The module also provides its own implementation of EventSerializer, namely RestfulObjectsSpecEventSerializer, which represents the event payload using the representation defined by the Restful Objects spec of (transient) objects, grafting on the metadata as additional JSON nodes.

The PublishedEvent entity also has a state field taking the values either "QUEUED" or "PROCESSED". The intention here is that an event bus can poll this table to grab pending events and dispatch them to downstream systems. When PublishedEvents are persisted initially they always take the value "QUEUED".

4.11.3. Usage

To indicate that an action invocation should be published, annotate it with the @Action#publishing() annotation.

To indicate that a changed object should be published is to annotate it with the @DomainObject#publishing() annotation.

It is also possible to "fine-tune" the EventPayload using the #publishingFactory() attribute (for both annotations). By default the EventPayload that is serialized identifies the object(s) being interacted with or changed, and in the case of the action invocation provides details of the action arguments and result (if any) of that action. However, the payload does not (by default) include any information about the new state of these objects. It is therefore the responsibility of the subscriber to call back to Isis to determine any information that has not been published.

Although the representations (if using the Restful Object serializer and Restful Objects viewer) does include hrefs for the objects, this nevertheless requires an additional network call to obtain this information).

In some circumstances, then, it may make more sense to eagerly "push" information about the change to the subscriber by including that state within the payload.

To accomplish this, an implementation of a “PayloadFactory” must be specified in the annotation.

For actions, we implement the PublishingPayloadFactoryForAction (in o.a.i.applib.annotation):

public interface PublishingPayloadFactoryForAction {
    @Programmatic
    public EventPayload payloadFor(
            Identifier actionIdentifier,
            Object target,
            List<Object> arguments,
            Object result);
}
}

The EventPayloadForActionInvocation abstract class (in the Isis applib) should be used as the base class for the object instance returned from payLoadFor(…​).

For objects, the interface to implement is PublishingPayloadFactoryForObject:

public interface PublishingPayloadFactoryForObject {

    @Programmatic
    public EventPayload payloadFor(
        Object changedObject,
        PublishingChangeKind publishingChangeKind);     (1)
}
1 an enum taking the values CREATE, UPDATE, DELETE

Similarly, the EventPayloadForObjectChanged abstract class should be used as the base class for the object returned from payLoadFor(…​).

For example, the following will eagerly include a ToDoItem’s `description property whenever it is changed:

@DomainObject(publishingPayloadFactory=ToDoItemPayloadFactory.class)
public class ToDoItem {
    ...
}

where ToDoItemPayloadFactory is defined as:

public class ToDoItemChangedPayloadFactory implements PublishingPayloadFactoryForObject {
    public static class ToDoItemPayload
        extends EventPayloadForObjectChanged<ToDoItem> {
      public ToDoItemPayload(ToDoItem changed) { super(changed); }
      public String getDescription() { return getChanged().getDescription(); }
    }
    @Override
    public EventPayload payloadFor(Object changedObject, PublishingChangeKind kind) {
        return new ToDoItemPayload((ToDoItem) changedObject);
    }
}

4.11.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then adding the (non-ASF) Isis addons' publishing module to the classpath will automatically register the PublishingService implementation (and also the module’s RestfulObjectsSpecEventSerializer implementation).

The PublishingService is intended for coarse-grained publish/subscribe for system-to-system interactions, from Isis to some other system. Here the only events published are those that action invocations (for actions annotated with @Action#publishing()) and of changed objects (for objects annotated with @DomainObject#publishing().

The EventBusService meanwhile is intended for fine-grained publish/subscribe for object-to-object interactions within an Isis domain object model. The event propagation is strictly in-memory, and there are no restrictions on the object acting as the event (it need not be serializable, for example).

All three of these services collaborate implicitly by way of the HasTransactionId interface.

4.11.6. Design Notes

The following class diagram shows how the above components fit together:

yuml.me 23db58a4

This yuml.me diagram was generated at yuml.me.

4.12. RepresentationService

The RepresentationService is the main plug-in point for the RestfulObjects viewer to generate representations.

The default implementation generates representations according to the Restful Objects spec v1.0. However, it also delegates to the ContentNegotiationService which provides a mechanism for altering representations according to the HTTP Accept header.

The principal motivation is to allow more flexible representations to be generated for REST clients that (perhaps through their use of a certain Javascript library, say) expect, or at least works best with, a certain style of representation.

In all there are three domain services that can influence the representations generated: this service, ContentNegotiationServiceand the ContentMappingService. The diagram below shows how these collaborate:

service collaborations

The RepresentationServiceForRestfulObjects is the default implementation of this service; likewise ContentNegotiationServiceXRoDomainType is the default implementation of the ContentNegotiationService. If you inspect the source code you’ll see that the default implementation of this service’s primary responsibility is to generate the default Restful Objects representations. Therefore, if you what you want to do is to generate a different _representation then in many cases replacing either this service _or the ContentNegotiationService will be equivalent (you’ll notice that their SPIs are very similar).

4.12.1. SPI

The SPI defined by this service is:

public interface RepresentationService {
    @Programmatic
    Response objectRepresentation(                  (1)
            Context rendererContext,
            ObjectAdapter objectAdapter);
    @Programmatic
    Response propertyDetails(                       (2)
            Context rendererContext,
            ObjectAndProperty objectAndProperty,
            MemberReprMode memberReprMode);
    @Programmatic
    Response collectionDetails(                     (3)
            Context rendererContext,
            ObjectAndCollection objectAndCollection,
            MemberReprMode memberReprMode);
    @Programmatic
    Response actionPrompt(                          (4)
            Context rendererContext,
            ObjectAndAction objectAndAction);
    @Programmatic
    Response actionResult(                          (5)
            Context rendererContext,
            ObjectAndActionInvocation objectAndActionInvocation,
            ActionResultReprRenderer.SelfLink selfLink);
    public static interface Context extends RendererContext {
        ObjectAdapterLinkTo getAdapterLinkTo();
    }
}
1 representation of a single object, as per section 14.4 of the RO spec, v1.0
2 representation of a single property of an object, as per section 16.4 of the RO spec v1.0
3 representation of a single collection of an object, as per section 17.5 of the RO spec v1.0
4 representation of a single action (prompt) of an object, as per section 18.2 of the RO spec v1.0
5 representation of the results of a single action invocation, as per section 19.5 of the RO spec v1.0

These methods provide:

  • a RendererContext which provides access to request-specific context (eg HTTP headers), session-specific context (eg authentication) and global context (eg configuration settings)

  • an object representing the information to be rendered

    eg ObjectAdapter, ObjectAndProperty, ObjectAndCollection etc

  • for members, whether the representation is in read/write mode

    ie MemberReprMode

This is an "internal" SPI, meaning that it uses types that are not part of the Isis applib. We do not guarantee that semantic versioning will be honoured for these APIs.

4.12.2. Implementation

As discussed in the introduction, the framework provides a default implementation, o.a.i.v.ro.rendering.service.RepresentationServiceForRestfulObjects. This delegates to ContentNegotiationService to generate an alternative representation; but if none is provided then it falls back on generating the representations as defined in the Restful Objects spec v1.0.

4.12.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' default implementation of RepresentationService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

The default implementation delegates to ContentNegotiationService, whose default implementation may delegate in turn to ContentMappingService (if present).

4.13. TranslationService

The TranslationService is the cornerstone of Isis' i18n support. Its role is to be able to provide translated versions of the various elements within the Isis metamodel (service and object classes, properties, collections, actions, action parameters) and also to translate business rule (disable/valid) messages, and exceptions. These translations provide for both singular and plural forms.

For the "big picture" and further details on Isis' i18n support, see here.

4.13.1. SPI

The SPI defined by this service is:

public interface TranslationService {
    @Programmatic
    String translate(String context, String text);      (1)
    @Programmatic
    String translate(String context,                    (2)
                     String singularText,
                     String pluralText, int num);

    enum Mode { READ, WRITE;}
    @Programmatic
    Mode getMode();                                     (3)
}
1 translate the text, in the locale of the "current user".
2 return a translation of either the singular or the plural text, dependent on the num parameter, in the locale of the "current user"
3 whether this implementation is operating in read or in write mode.

If in read mode, then the translations are expected to be present.

If in write mode, then the implementation is saving translation keys, and will always return the untranslated translation.

4.13.2. Implementation

The Isis framework provides a default implementation (TranslationServicePo) that uses the GNU .pot and .po files for translations. It relies on the LocaleProvider service (to return the Locale of the current user) and also the TranslationsResolver service (to read existing translations).

The framework also provides a supporting TranslationServicePoMenu provides menu items under the "Prototyping" secondary menu for controlling this service and downloading .pot files for translation.

For more details on the implementation, see i18n support.

4.13.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then Isis' core implementation of TranslationService service (along with the supporting menu service) are automatically registered and injected (it is annotated with @DomainService) so no further configuration is required. If the menu items are not required then these can be suppressed either using security or by implementing a vetoing subscriber.

This service works in conjunction with LocaleProvider and TranslationsResolver in order to provide i18n support.

4.14. TranslationsResolver

The TranslationsResolver service is one of the services that work together to implement Isis' support for i18n, being used by Isis' default implementation of TranslationService.

The role of the service itself is locate and return translations.

For the "big picture" and further details on Isis' i18n support, see here.

4.14.1. SPI

The SPI defined by this service is:

public interface TranslationsResolver {
    @Programmatic
    List<String> readLines(final String file);
}

4.14.2. Implementation

Isis' Wicket viewer provides an implementation of this service (TranslationsResolverWicket) which leverages Apache Wicket APIs. This searches for translation files in the standard WEB-INF/ directory.

Currently there is no equivalent implementation for the RestfulObjects viewer.

4.14.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

and that the Wicket viewer is being used then an implementation of TranslationsResolver is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

This service works in conjunction with LocaleProvider and TranslationService in order to provide i18n support.

4.15. UserProfileService

The UserProfileService provides the ability for the domain application to return supplementary metadata about the current user. This information is used (by the Wicket viewer) to customize the appearance of the tertiary "Me" menu bar (top right). For example, rather than display the username, instead the user’s first and last name could be displayed.

Another use case is to allow the user to switch context in some fashion or other. This might be to emulate a sort of "sudo"-like function, or perhaps to focus on some particular set of data.

4.15.1. SPI

The SPI defined by the service is:

public interface UserProfileService {
    @Programmatic
    String userProfileName();       (1)
}
1 is used (in the Wicket viewer) as the menu name of the tertiary "Me" menu bar.

If the method returns null or throws an exception then the framework will default to using the current user name.

In the future this API may be expanded; one obvious possibility is to return a profile photo or avatar URL.

4.15.2. Implementation

The (non-ASF) Isis addons' todoapp has a simple implementation of this service:

todoapp

Currently this feature is not integrated with Isis' authentication mechanisms; the information provided is purely metadata provided for presentation purposes only.

4.16. UserRegistrationService

The UserRegistrationService provides the ability for users to sign-up to access an application by providing a valid email address, and also provides the capability for users to reset their password if forgotten.

For user sign-up, the Wicket viewer will check whether an implementation of this service (and also the EmailNotificationService) is available, and if so will render a sign-up page where the user enters their email address. A verification email is sent (using the aforementioned EmailNotificationService) which includes a link back to the running application; this allows the user then to complete their registration process (choose user name, password and so on). When the user has provided the additional details, the Wicket viewer calls _this service in order to create an account for them, and then logs the user on.

For the password reset feature, the Wicket viewer will render a password reset page, and use the EmailNotificationService to send a "password forgotten" email. This service provides the ability to reset a password based on the user’s email address.

It is of course possible for domain objects to use this service; it will be injected into domain object or other domain services in the usual way. That said, we expect that such use cases will be comparatively rare; the primary use case is for the Wicket viewer’s sign-up page.

For further details on the user registration feature (as supported by the Wicket viewer), see here.

4.16.1. SPI

The SPI defined by the service is:

public interface UserRegistrationService {
    @Programmatic
    boolean usernameExists(String username);                                    (1)
    @Programmatic
    boolean emailExists(String emailAddress);                                   (2)
    @Programmatic
    void registerUser(String username, String password, String emailAddress);   (3)
    @Programmatic
    boolean updatePasswordByEmail(String emailAddress, String password);        (4)
}
1 checks if there is already a user with the specified username
2 checks if there is already a user with the specified email address
3 creates the user, with specified password and email address. The username and email address must both be unique (not being used by an existing user)
4 allows the user to reset their password

4.16.2. Implementation

The core Isis framework itself defines only an API; there is no default implementation. Rather, the implementation will depend on the security mechanism being used.

That said, if you have configured your app to use the Isis addons security module, then note that the security module does provide an abstract implementation (SecurityModuleAppUserRegistrationServiceAbstract) of the UserRegistrationService. You will need to extend that service and provide implementation for the two abstract methods: getInitialRole() and getAdditionalInitialRoles().

This is needed so that the self-registered users are assigned automatically to your application role(s) and be able to use the application. Without any role such user will be able only to see/use the logout link of the application.

4.16.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured:

isis.services-installer=configuration-and-annotation

then (if using the (non-ASF) Isis addons' security module) you will need to register your concrete implementation of SecurityModuleAppUserRegistrationServiceAbstract.

The most common use case is to allow users to sign-up through Isis' Wicket viewer. Because the process requires email to be sent, the following services must be configured:

The EmailService in particular requires additional configuration properties to specify the external SMTP service.

5. Classes

TODO

5.1. Superclasses

TODO

The table below summarizes all the convenience superclasses defined by Apache Isis.

Table 29. Convenience Superclasses
API Maven Module
Impl’n (g: a:)
Implementation Notes

o.a.i.applib.
AbstractContainedObject

o.a.i.core
isis-core-applib

(abstract class)

o.a.i.applib.
AbstractDomainEvent

o.a.i.core
services.eventbus
isis-core-applib

(abstract class)

o.a.i.applib.
AbstractDomainObject

o.a.i.core
isis-core-applib

(abstract class)

o.a.i.applib.
AbstractFactoryAndRepository

o.a.i.core
isis-core-applib

(abstract class)

o.a.i.applib.
AbstractService

o.a.i.core
isis-core-applib

(abstract class)

o.a.i.applib.
AbstractViewModel

o.a.i.core
isis-core-applib

(abstract class)

o.a.i.applib.
ActionDomainEvent

o.a.i.core
services.eventbus
isis-core-applib

(abstract class)

o.a.i.applib.
CollectionDomainEvent

o.a.i.core
services.eventbus
isis-core-applib

(abstract class)

o.a.i.applib.
fixturescript
FixtureScript

o.a.i.core
isis-core-applib

(abstract class)

o.a.i.applib.
fixturescripts
FixtureScripts

o.a.i.core
isis-core-applib

(abstract class)

depends on:
ClassDiscoveryService

o.a.i.applib.
PropertyDomainEvent

o.a.i.core
services.eventbus
isis-core-applib

(abstract class)

5.1.1. AbstractContainedObject

TODO

5.1.2. AbstractDomainEvent

TODO

5.1.3. AbstractDomainObject

TODO

5.1.4. AbstractFactoryAndRepository

TODO

5.1.5. AbstractService

TODO

5.1.6. AbstractViewModel

TODO

5.1.7. ActionDomainEvent

TODO

5.1.8. CollectionDomainEvent

TODO

5.1.9. FixtureScript

TODO

5.1.10. FixtureScripts

TODO

5.1.11. PropertyDomainEvent

TODO

5.2. Value Types

TODO
Table 30. JDK Classes
Type Description

java.lang.Boolean

java.lang.Character

java.lang.Double

java.lang.Float

java.lang.Integer

java.lang.Long

java.lang.Short

java.lang.String

java.math.BigDecimal

java.math.BigInteger

java.sql.Date

java.sql.Time

java.sql.Timestamp

java.util.Date

Table 31. JodaTime classes
Type Description

org.joda.time.DateTime

org.joda.time.LocalDateTime

org.joda.time.LocalDate

Table 32. Isis Applib classes
Type Description o.a.i.applib.value.
Blob

o.a.i.applib.value.
Clob

o.a.i.applib.value.
Color

o.a.i.applib.value.
Money

o.a.i.applib.value.
Password

5.2.1. Blob

TODO

5.2.2. Clob

TODO

5.2.3. Color

TODO

5.2.4. Money

TODO

5.2.5. Password

TODO

5.3. Applib Utility Classes

The org.apache.isis.applib.util package has a number of simple utility classes designed to simplify the coding of some common tasks.

5.3.1. Enums

TODO

5.3.2. ObjectContracts

The ObjectContracts test provides a series of methods to make it easy for your domain objects to:

  • implement Comparable (eg so can be stored in java.util.SortedSets)

  • implement toString()

  • implement equals()

  • implement hashCode()

For example:

public class ToDoItem implements Comparable<ToDoItem> {

    public boolean isComplete() { ... }
    public LocalDate getDueBy() { ... }
    public String getDescription() { ... }
    public String getOwnedBy() { ... }

    public int compareTo(final ToDoItem other) {
        return ObjectContracts.compare(this, other, "complete","dueBy","description");
    }

    public String toString() {
        return ObjectContracts.toString(this, "description","complete","dueBy","ownedBy");
    }
}

Note that ObjectContracts makes heavy use of Java Reflection. While it’s great to get going quickly in prototyping, we recommend you use your IDE to code generate implementations of these methods for production code.

Moreover (and perhaps even more importantly) ObjectContracts implementation can cause DataNucleus to recursively rehydrate a larger number of associated entities (More detail below).

We therefore recommend that you disable persistence-by-reachability by adding:

persistor_datanucleus.properties
isis.persistor.datanucleus.impl.datanucleus.persistenceByReachabilityAtCommit=false
The issue in more detail

Consider the entities:

Party <->* AgreementRole <*-> Agreement

In the course of a transaction, the Agreement entity is loaded into memory (not necessarily modified), and then new AgreementRoles are associated to it.

All these entities implement Comparable using ObjectContracts, so that the implementation of AgreementRole's (simplified) is:

public class AgreementRole {
    ...
    public int compareTo(AgreementRole other) {
        return ObjectContracts.compareTo(this, other, "agreement","startDate","party");
    }
    ...
}

while Agreement's is implemented as:

    public class Agreement {
        ...
        public int compareTo(Agreement other) {
            return ObjectContracts.compareTo(this, other, "reference");
        }
        ...
    }

and Party's is similarly implemented as:

public class Party {
    ...
    public int compareTo(Party other) {
        return ObjectContracts.compareTo(this, other, "reference");
    }
    ...
}

DataNucleus’s persistence-by-reachability algorithm adds the AgreementRoles into a SortedSet, which causes AgreementRole#compareTo() to fire:

  • the evaluation of the "agreement" property delegates back to the Agreement, whose own Agreement#compareTo() uses the scalar reference property. As the Agreement is already in-memory, this does not trigger any further database queries

  • the evaluation of the "startDate" property is just a scalar property of the AgreementRole, so will already in-memory

  • the evaluation of the "party" property delegates back to the Party, whose own Party#compareTo() requires the uses the scalar reference property. However, since the Party is not yet in-memory, using the reference property triggers a database query to "rehydrate" the Party instance.

In other words, figuring out whether AgreementRole is comparable requires the persistence-by-reachability algorithm to run, causing the adjacent associated entity Party to also be retrieved.

5.3.3. Reasons

There are two different classes provided to help build reasons returned by disableXxX() and validateXxx() methods:

  • the org.apache.isis.applib.util.ReasonBuffer helper class

  • the org.apache.isis.applib.util.Reasons helper class

For example:

public class Customer {
    ...
    public String validatePlaceOrder(Product p, int quantity) {
        return Reasons.coalesce(
            whetherCustomerBlacklisted(this),
            whetherProductOutOfStock(p)
        );
    }
}

Which you use (if any) is up to you.

5.3.4. TitleBuffer

The TitleBuffer utility class is intended to make it easy to construct title strings (returned from the title() method).

For example, it has overloaded versions of methods called append() and concat().

5.4. Mixins

The interfaces listed in this chapter act like "mix-ins"; they allow domain services to contribute actions/properties/collections to any domain objects that implement these interfaces.

5.4.1. HasTransactionId

The HasTransactionId interface is a mix-in for any domain objects that pertain

public interface HasTransactionId {

    public UUID getTransactionId();                             (1)
    public void setTransactionId(final UUID transactionId);
}
1 unique identifier (a GUID) of the transaction in which this interaction occurred.

Modules that either have domain entity that implement and/or services that contribute this interface are:

5.4.2. HasUsername

The HasUsername interface is a mix-in for domain objects to be associated with a username. Other services and modules can then contribute actions/collections to render such additional information relating to the activities of the user.

The interface is defined is:

public interface HasUsername {
    public String getUsername();
}

Modules that either have domain entity that implement and/or services that contribute this interface are:

6. Object Layout

In implementing the naked objects pattern, Apache Isis aims to infer as much information from the domain classes as possible. Nevertheless, some metadata relating solely to the UI is inevitably required.

This chapter describes how this is done both for domain objects — statically or dynamically — and for the application menu bar (containing domain service' actions).

6.1. Static Object Layout

Metadata providing UI hints can be specified either statically, using annotations, or dynamically, using a .layout.json file. This section describes the static approach, using annotations.

6.1.1. @MemberOrder

The @MemberOrder annotation is used to specify the relative order of domain class properties, collections and actions.

The annotation defines two attributes, name() and sequence(). Their usage depends on the member type:

  • for properties, the name() is used to group properties together. The sequence() then orders properties within these groups. If no name() is specified then the property is placed in a fallback "General" group, called "General".

    The name of these "member groups" are then referenced by @MemberGroupLayout.

  • for collections, the name() attribute is (currently) unused. The sequence() orders collections relative to one another

  • for actions, the name() attribute associates an action with either a property or with a collection.

    If the name() attribute matches a property name, then the action’s button is rendered close to the property, according to @ActionLayout#position() attribute.

    On the other hand if the `name() attribute matches a collection name, then the action’s button is rendered on the collection’s header.

    If there is no name() value, then the action is considered to pertain to the object as a whole, and its button is rendered close to the object’s icon and title.

    Within any of these, the sequence() then determines the relative ordering of the action with respect to other actions that have been similarly associated with properties/collections or left as "free-standing".

For example:

public class ToDoItem {
    @MemberOrder(sequence="1")
    public String getDescription() { ... }
    @MemberOrder(sequence="2")
    public String getCategory() { ... }
    @MemberOrder(sequence="3")
    public boolean isComplete() { ... }
    @MemberOrder(name="Detail", sequence="1")
    public LocalDate getDueBy() { ... }
    @MemberOrder(name="Detail", sequence="2")
    public BigDecimal getCost() { ... }
    @MemberOrder(name="Detail", sequence="4")
    public String getNotes() { ... }
    @MemberOrder(name="Misc", sequence="99")
    public Long getVersionSequence() { ... }
    ...
}

This defines three property (or member) groups, "General", "Detail" and "Misc"; "General" is the default if no name attribute is specified. Properties in the same member group are rendered together, as a fieldset.

In addition, actions can optionally be associated (rendered close to) either properties or actions. This is done by overloading the @MemberOrder’s `name attribute, holding the value of the property or collection.

For example:

public class ToDoItem {
    @MemberOrder(sequence="3")
    public boolean isComplete() { ... }
    @MemberOrder(name="complete", sequence="1")
    public ToDoItem completed() { ...}
    @MemberOrder(name="complete", sequence="2")
    public ToDoItem notYetCompleted() { ...}

    @MemberOrder(sequence="1")
    public SortedSet<ToDoItem> getDependencies() { ... }
    @MemberOrder(name="dependencies", sequence="1")
    public ToDoItem add(ToDoItem t) { ...}
    @MemberOrder(name="dependencies", sequence="2")
    public ToDoItem remove(ToDoItem t) { ...}
    ...
}

will associate the completed() and notYetCompleted() actions with the complete property, and will associate the add() and remove() actions with the dependencies collection.

The value of sequence() is a string. The simplest convention (as shown in the example above) is to use numbers — 1, 2, 3 — though it is a better idea to leave gaps in the numbers — 10, 20, 30 perhaps — such that a new member may be added without having to edit existing numbers. A useful alternative is to adopt the 'dewey-decimal' notation — 1, 1.1, 1.2, 2, 3, 5.1.1, 5.2.2, 5.2, 5.3 — which allows for an indefinite amount of future insertion. It also allows subclasses to insert their class members as required.

6.1.2. @MemberGroupLayout

The @MemberGroupLayout annotation specifies the relative positioning of property groups as being either in a left column, a middle column or in a right column. The annotation also specifies the relative width of the columns.

The property groups in this case are those that are inferred from the @MemberOrder#name() attribute.

For example:

@MemberGroupLayout(
     columnSpans={3,3,0,6},
     left={"General", "Misc"},
     middle="Detail"
 )
public class ToDoItem {
    ...
}

Four values are given in the columnSpans attribute. The first three are the relative widths of the three columns of property groups. The fourth, meanwhile, indicates the width of a final column that holds all the collections of the object.

The values of these spans are taken as proportions of 12 virtual columns across the page (this taken from the Bootstrap library).

For example:

  • {3,3,0,6} indicates:

    • a left column of properties taking up 25% of the width

    • a middle column of properties taking up 25% of the width

    • a right column of collections taking up 50% of the width

  • {2,6,0,4} indicates:

    • a left column of properties taking up ~16% of the width

    • a middle column of properties taking up 50% of the width

    • a right column of collections taking up ~33% of the width

  • {2,3,3,4} indicates:

    • a left column of properties taking up ~16% of the width

    • a middle column of properties taking up 25% of the width

    • a right column of properties taking up 25% of the width

    • a far right column of collections taking up ~33% of the width

If the sum of all the columns exceeds 12, then the collections are placed underneath the properties, taking up the full span. For example:

  • {4,4,4,12} indicates:

    • a left column of properties taking up ~33% of the width

    • a middle column of properties taking up ~33% of the width

    • a right column of properties taking up ~33% of the width

    • the collections underneath the property columns, taking up the full width

6.1.3. Example Layouts

Below are sketches for the layout of the ToDoItem class of the Isis addons example todoapp (not ASF):

The first divides the properties into two equal sized columns (6-6-0) and puts the collections underneath (12):

6 6 0 12

The next divides the collections into three equal sized columns (4-4-4) and again puts the collections underneath (12):

4 4 4 12

The last puts the properties into a single column (4-0) and places the collections into the other larger column (8-0):

4 0 8 0

6.1.4. Other Annotations

As of 1.8.0, all the layout annotations have been consolidated into the various XxxLayout annotations: @ActionLayout @CollectionLayout, @DomainObjectLayout, @DomainServiceLayout, @ParameterLayout, @PropertyLayout, and @ViewModelLayout

6.2. Dynamic Object Layout

Metadata providing UI hints can be specified either statically, using annotations, or dynamically, using a .layout.json file. This section describes the dynamic approach, using the file.

6.2.1. JSON layout file

The JSON layout file for class Xxx takes the name Xxx.layout.json, and resides in the same package as the class. The format of the file is:

{
  "columns": [                                // list of columns
    {
      "span": 6,                              // span of the left-hand property column
      "memberGroups": {                       // ordered map of member (property) groups
        "General": {                          // member group name
          "members": {
            "description": {                  // property, no associated actions, but with UI hint
              "propertyLayout": {
                "typicalLength": 50           // UI hint for size of field (no longer used in ISIS 1.8.0)
              }
            },
            "category": {},
            "complete": {                     // property, with associated actions
              "propertyLayout": {
                "describedAs": "Whether this todo item has been completed"
              },
              "actions": {
                "completed": {
                  "actionLayout": {
                    "named": "Done",          // naming UI hint
                    "cssClass": "x-highlight" // CSS UI hint
                  }
                },
                "notYetCompleted": {
                  "actionLayout": {
                    "named": "Not done"
                  }
                }
              }
            }
          },
          "Misc": {
            "members": {
              "notes": {
                "propertyLayout": {
                  "multiLine": 5              // UI hint for text area
                }
              },
              "versionSequence": {}
            }
          }
        }
      }
    },
    {
      "span": 6,                              // span of the middle property column
      "memberGroups": { ... }
    },
    {
      "span": 0                               // span of the right property column (if any)
    },
    {
      "span": 6,
      "collections": {                        // ordered map of collections
        "dependencies": {                     // collection, with associated actions
          "collectionLayout": {
            "paged": 10,                      // pagination UI hint
            "render": "EAGERLY"               // lazy-loading UI hint
          },
          "actions": {
            "add":{},
            "delete": {}
          },
        },
        "similarItems": {}                    // collection, no associated actions
      }
    }
  ],
  "actions": {                                // actions not associated with any member
    "delete": {},
    "duplicate": {
      "actionLayout": {
        "named": {
          "value": "Clone"
        }
      }
    }
  }
}

Although advisable, it is not necessary to list all class members in this file. Any members not listed with be ordered according either to annotations (if present) or fallback/default values.

Note also that the layout file may contain entries for contributed associations and actions; this allows each contributee classes to define their own layout for their contributions, possibly overriding any static metadata on the original domain service contributor.

6.2.2. Downloading an initial layout file

The fastest way to get started is to use the (non-ASF) Isis addons' devutils module to download the layout file (derived from any existing static metadata defined by annotations).

6.3. Application Menu Layout

The actions of domain services are made available as an application menu bar. By default each domain service corresponds to a single menu on this menu bar, with its actions as the drop-down menu items. This is rarely exactly what is required, however. The @MemberOrder and @DomainServiceLayout annotations can be used to rearrange the placement of menu items.

The screenshots below are taken from Estatio, an open source estate management application built using Apache Isis.

6.3.1. @DomainServiceLayout

Menus for domain services can be placed either on a primary, secondary or tertiary menu bar.

layout menus

Within a single top-level menu (eg "Fixed Assets") there can be actions from multiple services. The Wicket viewer automatically adds a divider between each:

dividers

In the example above the top-level menu combines the actions from the Properties, Units and FixedAssetRegistrations services. The Properties service is annotated:

@DomainServiceLayout(
        named="Fixed Assets",
        menuBar = DomainServiceLayout.MenuBar.PRIMARY,
        menuOrder = "10.1"
)
public class Properties ... { ... }

while the Units service is annotated:

@DomainServiceLayout(
        named="Fixed Assets",
        menuBar = DomainServiceLayout.MenuBar.PRIMARY,
        menuOrder = "10.2"
)
public class Units ... { ... }

and similarly FixedAssetRegistrations is annotated:

@DomainServiceLayout(
        named="Fixed Assets",
        menuBar = DomainServiceLayout.MenuBar.PRIMARY,
        menuOrder = "10.3"
)
public class FixedAssetRegistrations ... { ... }

Note that in all three cases the value of the named attribute and the menuBar attribute is the same: "Fixed Assets" and PRIMARY. This means that all will appear on a "Fixed Assets" menu in the primary menu bar.

Meanwhile the value of menuOrder attribute is significant for two reasons:

  • for these three services on the same ("Fixed Assets") top-level menu, it determines the relative order of their sections (Properties first, then Units, then FixedAssetRegistrations)

  • it determines the placement of the top-level menu itself ("Fixed Assets") with respect to other top-level menus on the menu bar.

To illustrate this latter point, the next top-level menu on the menu bar, "Parties", is placed after "Fixed Assets" because the menuOrder of the first of its domain services, namely the Parties service, is higher than that for "Fixed Assets":

@DomainServiceLayout(
        named="Parties",
        menuBar = DomainServiceLayout.MenuBar.PRIMARY,
        menuOrder = "20.1"
)
public class Parties ... { ... }

Note that only the menuOrder of the first domain service is significant in placing the menus along the menu bar; thereafter the purpose of the menuOrder is to order the menu services sections on the menu itself.

6.3.2. Ordering of a service’s actions within a menu

For a given service, the actions within a section on a menu is determined by the @MemberOrder annotation. Thus, for the Units domain service, its actions are annotated:

public class Units extends EstatioDomainService<Unit> {

    @MemberOrder(sequence = "1")
    public Unit newUnit( ... ) { ... }

    @MemberOrder(sequence = "2")
    public List<Unit> findUnits( ... ) { ... }

    @ActionLayout( prototype = true )
    @MemberOrder(sequence = "99")
    public List<Unit> allUnits() { ... }
    ...
}

Note that the last is also a prototype action (meaning it is only displayed in SERVER_PROTOTYPE (=Wicket Development) mode). In the UI it is rendered in italics.

(It is possible to override this place of a given action by specifying @MemberOrder(name="…​") where the name is that of a top-level menu. Prior to 1.8.0 this was the only way of doing things, as of 1.8.0 its use is not recommended).

6.3.3. Tertiary menubar

The tertiary menu bar consists of a single unnamed menu, rendered underneath the user’s login, top right. This is intended primarily for actions pertaining to the user themselves, eg their account, profile or settings:

tertiary

Domain services' actions can be associated with the tertiary menu using the same @DomainServiceLayout annotation. For example, the updateEpochDate(…​) and listAllSettings(…​) actions come from the following service:

@DomainServiceLayout(
        menuBar = DomainServiceLayout.MenuBar.TERTIARY,
        menuOrder = "10.1"
)
public class EstatioAdministrationService ... {

    @MemberOrder(sequence = "1")
    public void updateEpochDate( ... ) { ... }

    @MemberOrder(sequence = "2")
    public List<ApplicationSetting> listAllSettings() { ... }
    ...
}

Because the number of items on the tertiary menu is expected to be small and most will pertain to the current user, the viewer does not place dividers between actions from different services on the tertiary menu.

6.3.4. Isis Add-on modules

Some of the (non-ASF) Isis Addons modules also provide services whose actions appear in top-level menus.

The security's module places its domain service menus in three top-level menus:

  • its ApplicationUsers, ApplicationRoles, ApplicationPermission, ApplicationFeatureViewModels and ApplicationTenancies domain services are all grouped together in a single "Security" top-level menu, on the SECONDARY menu bar

  • its SecurityModuleAppFixturesService domain service, which allows the security modules' fixture scripts to be run, is placed on a "Prototyping" top-level menu, also on the SECONDARY menu bar

  • its MeService domain service, which provides the me() action, is placed on the TERTIARY menu bar.

Meanwhile the devutils module places its actions - to download layouts and so forth - on a "Prototyping" top-level menu, on the SECONDARY menu bar.

Currently there is no facility to alter the placement of these services. However, their UI can be suppressed using security or using a vetoing subscriber.

6.4. Static vs Dynamic Layouts

Using dynamic object layouts using JSON has the huge benefit that the layout can be updated without requiring a recompile of the code and redeploy of the app. Many developers also find it easier to rationalize about layout when all the hints are collated together in a single place (rather than scattered across the class members as annotations).

Another benefit of dynamic layout is that UI hints can be provided for contributed associations and actions that are synthesised at runtime.

The main downsides of using dynamic layouts are a lack of typesafety (a typo will result in the metadata not being picked up for the element) and syntactic fragility (an invalid JSON document will result in no metadata for the entire class).

Also, dynamic layouts have no notion of inheritance, whereas the dewey-decimal format @MemberOrder annotation allows the metadata of the subclass its superclasses to fit together relatively seamlessly.

6.4.1. Best of both worlds?

Using the (non-ASF) Isis addons' jrebel plugin comes close to getting the best of both words: metadata is specified in a type-safe way using annotations, but can be reloaded automatically.

The downsides are licensing cost, and also the fact that metadata for contributed actions in the contributee class cannot be specified.

Another open source alternative that you might also like to explore is DCEVM; there’s a good write-up on the IntelliJ blog.


Copyright © 2010~2015 The Apache Software Foundation, licensed under the Apache License, v2.0.
Apache, the Apache feather logo, Apache Isis, and the Apache Isis project logo are all trademarks of The Apache Software Foundation.

-->