<
Fork me on GitHub

1. Domain Services

This guide documents Apache Isis' domain services, both those that act as an API (implemented by the framework for your domain objects to call), and those domain services that act as an SPI (implemented by your domain application and which are called by the framework).

Apache Isis documentation is broken out into a number of user, reference and "supporting procedures" guides.

The user guides available are:

The reference guides are:

The remaining guides are:

  • Developers' Guide (how to set up a development environment for Apache Isis and contribute back to the project)

  • Committers' Guide (release procedures and related practices)

2. Introduction

2.1. Types of Domain Service

The domain services also group into various broad categories. Many support functionality of the various layers of the system (presentation layer, application layer, core domain, persistence layer); others exist to allow the domain objects to integrate with other bounded contexts, or provide various metadata (eg for development-time tooling). The diagram below shows these categories:

categories

A small number of 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.

This rest of this guide is broken out into several chapters, one for each of the various types/categories of domain service.

2.2. Public API

The vast majority of Apache Isis' domain services are defined in Apache Isis' applib (o.a.i.core:isis-core-applib module) as stable, public classes. Importantly, this also minimizes the coupling between your code and Apache Isis, allowing you to easily mock out these services in your unit tests.

2.3. Using the services

Apache Isis includes an extensive number of domain services for your domain objects to use; simply define the service as an annotated field and Apache Isis will inject the service into your object.

For example:

public class Customer {

    public void sendEmail( String subject, String body) {
        List<String> cc = Collections.emptyList;
        List<String> bcc = Collections.emptyList;
        emailService.send(getEmailAddress(), cc, bcc, subject, body);
    }
    public boolean hideSendEmail() {
        return !emailService.isConfigured();
    }

    @Inject                                                            (1)
    EmailService emailService;
}
1 Service automatically injected by the framework.

For objects that are already persisted, the service is automatically injected just after the object is rehydrated by JDO/DataNucleus.

For transient objects (instantiated programmatically), the FactoryService's instantiate() method (or the deprecated DomainObjectContainer's newTransientInstance() method) will automatically inject the services.

Alternatively the object can be instantiated simply using new, then services injected using ServiceRegistry's injectServicesInto(…​) method (or the deprecated DomainObjectContainer's injectServicesInto(…​) method).

2.4. Overriding the services

The framework provides default implementations for many of the domain services. This is convenient, but sometimes you will want to replace the default implementation with your own service implementation.

The trick is to use the @DomainServiceLayout#menuOrder() attribute, specifying a low number (typically "1").

For a small number of domain services, all implementations are used (following the chain-of-responsibility pattern), not just the first one. The services in question are: ContentMappingService, GridSystemService, and RoutingService.

For example, suppose you wanted to provide your own implementation of LocaleProvider. Here’s how:

@DomainService(
        nature = NatureOfService.DOMAIN
)
@DomainServiceLayout(
        menuOrder = "1"                             (1)
)
public class MyLocaleProvider implements LocaleProvider {
    @Override
    public Locale getLocale() {
        return ...
    }
}
1 takes precedence over the default implementation.

It’s also quite common to want to decorate the existing implementation (ie have your own implementation delegate to the default); this is also possible and quite easy (if using 1.10.0 or later). The idea is to have the framework inject all implementations of the service, and then to delegate to the first one that isn’t "this" one:

@DomainService(nature=NatureOfService.DOMAIN)
@DomainServiceLayout(
        menuOrder = "1"                                                                 (1)
)
public class MyLocaleProvider implements LocaleProvider {
    @Override
    public Locale getLocale() {
        return getDelegateLocaleProvider().getLocale();                                 (2)
    }
    private LocaleProvider getDelegateLocaleProvider() {
        return Iterables.tryFind(localeProviders, input -> input != this).orNull();     (3)
    }
    @Inject
    List<LocaleProvider> localeProviders;                                               (4)
}
1 takes precedence over the default implementation when injected elsewhere.
2 this implementation merely delegates to the default implementation
3 find the first implementation that isn’t this implementation (else infinite loop!)
4 injects all implementations, including this implemenation

The above code could be improved by caching the delegateLocaleProvider once located (rather than searching each time).

3. Presentation Layer SPI

Domain service SPIs for the presentation layer influence how the Apache Isis viewers behave.

The table below summarizes the presentation layer SPIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 1. Presentation Layer SPI
SPI Description Implementation Notes

o.a.i.applb.
services.conmap
ContentMappingService

(Attempt to) map the returned data into the representation required by the client’s HTTP Accept header.

Replaces (and simplifies) the earlier ContentMappingService that defined an SPI using classes internal to the framework.
+ No default implementation.

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.error
ErrorReportingService

Record details of an error occurring in the system (eg in an external incident recording system such as JIRA), and return a more friendly (jargon-free) message to display to the end user, with optional reference (eg XXX-1234).

(none)

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.grid
GridSystemService

Validates and normalizes the grid layout for a domain class (with respect to a particular grid system such as Bootstrap3), also providing a default grid (for those domain classes where there is no grid layout).

GridSystemServiceBS3
o.a.i.core
isis-core-metamodel

o.a.i.applib.
services.grid
GridLoaderService

Responsible for loading a grid layout for a domain class, eg from a layout.xml file.

GridLoaderServiceDefault
o.a.i.core
isis-core-metamodel

o.a.i.applib.
services.grid
GridService

A facade on top of both GridLoaderService and GridSystemService, thus being able to return normalized grids for any domain class.

GridServiceDefault
o.a.i.core
isis-core-metamodel

o.a.i.applib.
services.hint
HintStore

Stores UI hints on a per-object basis. For example, the viewer remembers which tabs are selected, and for collections which view is selected (eg table or hidden), which page of a table to render, or whether "show all" (rows) is toggled.

HintStoreUsingWicketSession
o.a.i.viewer
isis-viewer-wicket-impl

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.routing
RoutingService

Return an alternative object than that returned by an action.

RoutingServiceDefault
o.a.i.core
isis-core-applib

The default implementation will return the home page (per HomePageProviderService) if a void or null is returned.
Used by the Wicket viewer only.

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.urlencoding
UrlEncodingService

Converts strings into a form safe for use within a URL. Used to convert view models mementos into usable URL form.

UrlEncodingService
UsingBaseEncoding
o.a.i.applib
isis-core-applib

o.a.i.applib.
services.userprof
UserProfileService

Obtain an alternative (usually enriched/customized) name for the current user, to render in the UI.

Key:

  • o.a.i is an abbreviation for org.apache.isis

  • o.ia.m is an abbreviation for org.isisaddons.module

There are also a number of presentation layer SPIs that use internal classes. These can be found in the next chapter.

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

Unlike most other domain services, the framework (that is, ContentNegotiationService) will check all available implementations of ContentMappingService to convert the domain object to the requested media type, rather than merely the first implementation found; in other words it uses the chain-of-responsibility pattern. Services are checked in the ordering defined by @DomainServiceLayout#menuOrder()). The mapped object used will be the first non-null result returned by an implementation.

3.1.1. SPI

The SPI defined by this service is:

public interface ContentMappingService {
    Object map(Object object,                           (1)
               List<MediaType> acceptableMediaTypes);   (2)
}
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

In versions prior to v1.12.0, this interface resided in a different package, internal to the Restful Objects viewer, and defined a slightly different signature that used an internal enum:

public interface ContentMappingService {
    Object map(Object object,
               List<MediaType> acceptableMediaTypes,
               RepresentationType representationType);   (1)

}
1 enum representing the requested representation; only ever take a value of DOMAIN_OBJECT or ACTION_RESULT.

3.1.2. Implementations

No default implementations are provided by Apache Isis framework itself.

However, the (non-ASF) Isis addons' todoapp includes a sample implementation to convert its ToDoItem entity into a (JAXB annotated) ToDoItemDto. The source code is:

@DomainService(nature = NatureOfService.DOMAIN)
public class ContentMappingServiceForToDoItem implements ContentMappingService {
    @Override
    public Object map(
            final Object object,
            final List<MediaType> acceptableMediaTypes) {
        if(object instanceof ToDoItem) {
            for (MediaType acceptableMediaType : acceptableMediaTypes) {
                final Map<String, String> parameters = acceptableMediaType.getParameters();
                final String className = parameters.get("x-ro-domain-type");
                if(className.eqausl(ToDoItemV1_1.class.getName())) {
                    return newToDoItemV1_1((ToDoItem) object);
                }
            }
        }
        return null;
    }
    private ToDoItemV1_1 newToDoItemV1_1(final ToDoItem toDoItem) {
        final ToDoItemV1_1 dto = new ToDoItemV1_1();
        dto.setToDoItem(toDoItem);
        dto.setDescription(toDoItem.getDescription());
        ...
        return dto;
    }
    ...
}

This service is a companion to the default implementation of the ContentNegotiationService.

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

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

3.2.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 Apache 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 Apache 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 Apache 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.2.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' default implementation of EmailNotificationService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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.

3.3. ErrorReportingService

The ErrorReportingService service is an optional SPI that providies the ability to record any errors/exceptions that might occur in the application into an external incident recording system (such as JIRA). The service also allows a user-friendly (jargon-free) error message to be returned and rendered to the end-user, along with an optional incident reference (eg a JIRA issue XXX-1234).

3.3.1. SPI

The SPI defined by this service is:

public interface ErrorReportingService {
    Ticket reportError(ErrorDetails errorDetails);
}

where ErrorDetails provided to the service is:

public class ErrorDetails {
    public String getMainMessage() { ... }          (1)
    public boolean isRecognized() { ... }           (2)
    public boolean isAuthorizationCause() { ... }   (3)
    public List<String> getStackTraceDetailList() { (4)
}
1 the main message to be displayed to the end-user. The service is responsible for translating this into the language of the end-user (it can use LocaleProvider if required).
2 whether this message has already been recognized by an ExceptionRecognizer service. Generally this converts potentially non-recoverable (fatal) exceptions into recoverable exceptions (warnings) as well providing an alternative mechanism for generating user-friendly error messages.
3 whether the cause of the exception was due to a lack of privileges. In such cases the UI restricts the information shown to the end-user, to avoid leaking potentially sensitive information
4 the stack trace of the exception, including the trace of any exceptions in the causal chain. These technical details are hidden from the user and only shown for non-recoverable exceptions.

and Ticket (returned by the service) has the constructor:

public class Ticket implements Serializable {
    public Ticket(
        final String reference,             (1)
        final String userMessage,           (2)
        final String details) { ... }       (3)
}
1 is a unique identifier that the end-user can use to track any follow-up from this error. For example, an implementation might automatically log an issue in a bug tracking system such as JIRA, in which case the reference would probably be the JIRA issue number <tt>XXX-1234</tt>.
2 a short, jargon-free message to display to the end-user.
3 is optional additional details to show. For example, these might include text on how to recover from the error, or workarounds, or just further details on contacting the help desk if the issue is severe and requires immediate attention.

3.3.2. Implementation

The (non-ASF) Isis addons' kitchensink app provides an example implementation:

@DomainService( nature = NatureOfService.DOMAIN )
public class KitchensinkErrorReportingService implements ErrorReportingService {
    private int ticketNumber = 1;
    @Override
    public Ticket reportError(final ErrorDetails errorDetails) {
        return new Ticket(
                nextTicketReference(),
                "The Kitchen sink app is sorry to report that: " + errorDetails.getMainMessage(),
                  "These are additional details for the end-user to read.\n"
                + "This content should be able to span many lines.\n"
                + "More detail.\n"
                + "Some suggested work-arounds.\n"
                + "Details of how to contact help desk.\n"
                + "And so on");
    }
    String nextTicketReference() {
        return "" + ticketNumber++;
    }
}

which is rendered as:

kitchensink example

3.3.3. Registering the Services

There is no default implementation of this service. To register your own implementation (and assuming that an AppManifest is being used to bootstrap the app), then just ensure that the implementation is on the classpath and the module containing the implementation is returned in AppManifest#getModules().

3.4. 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).

3.4.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 Apache 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 Apache 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.

3.4.2. Implementation

The framework provides two default implementations:

  • o.a.i.core.metamodel.services.container.DomainObjectContainerDefault provided by Apache 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.

3.4.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) 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.

If the JDO exception recognizers are not required (rather unlikely), then they can be disabled en-masse using the configuration property isis.services.ExceptionRecognizerCompositeForJdoObjectStore.disable.

3.5. GridSystemService

The GridSystemService encapsulates a single layout grid system which can be used to customize the layout of domain objects. In particular this means being able to return a "normalized" form (validating and associating domain object members into the various regions of the grid) and in providing a default grid if there is no other metadata available.

The framework provides a single such grid implementation, namely for Bootstrap3.

Unlike most other domain services, the framework will check all available implementations of GridSystemService to obtain available grid systems, rather than merely the first implementation found; in other words it uses the chain-of-responsibility pattern. Services are called in the order defined by @DomainServiceLayout#menuOrder()).

Note though that each concrete implementation must also provide corresponding Wicket viewer components capable of interpreting the grid layout.

3.5.1. SPI

The SPI defined by this service is:

public interface GridSystemService<G extends Grid> {
    Class<? extends Grid> gridImplementation();                 (1)
    String tns();                                               (2)
    String schemaLocation();                                    (3)
    Grid defaultGrid(Class<?> domainClass);                     (4)
    void normalize(G grid, Class<?> domainClass);               (5)
    void complete(G grid, Class<?> domainClass);                (6)
    void minimal(G grid, Class<?> domainClass);                 (7)
}
1 The concrete subclass of Grid supported by this implementation. As noted in the introduction, there can be multiple implementations of this service, but there can only be one implementation per concrete subclass. As is normal practice, the service with the lowest @DomainServiceLayout#menuOrder() takes precedence.
2 the target namespace for this grid system. This is used when generating the XML. The Bootstrap3 grid system provided by the framework returns the value http://isis.apache.org/applib/layout/grid/bootstrap3.
3 the schema location for the XSD. The Bootstrap3 grid system provided by the framework returns the value http://isis.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd.
4 a default grid, eg two columns in ratio 4:8. Used when no existing grid layout exists for a domain class.
5 Validates and normalizes a grid, modifying the grid so that all of the domain object’s members (properties, collections, actions) are bound to regions of the grid. This is done using existing metadata, most notably that of the @MemberOrder annotation. Such a grid, if persisted as the layout XML file for the domain class, allows the @MemberOrder annotation to be removed from the source code of the domain class (but other annotations must be retained).
6 Takes a normalized grid and enriches it with additional metadata (taken from Apache Isis' internal metadata) that can be represented in the layout XML. Such a grid, if persisted as the layout XML file for the domain class, allows all layout annotations (@ActionLayout, @PropertyLayout and @CollectionLayout) to be removed from the source code of the domain class.
7 Takes a normalized grid and strips out removes all members, leaving only the grid structure. Such a grid, if persisted as the layout XML file for the domain class, requires that the @MemberOrder annotation is retained in the source code of said class in order to bind members to the regions of the grid.

3.5.2. Implementation

The framework provides GridSystemServiceBS3, an implementation that encodes the bootstrap3 grid system. (The framework also provides Wicket viewer components that are capable of interpreting and rendering this metadata).

3.5.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app), then the Bootstrap3 default implementation of GridSystemService is automatically registered and injected, and no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

This service is used by GridService.

3.6. GridLoaderService

The GridLoaderService provides the ability to load the XML layout (grid) for a domain class.

3.6.1. SPI

The SPI defined by this service is:

public interface GridLoaderService {
    boolean supportsReloading();                (1)
    void remove(Class<?> domainClass);          (2)
    boolean existsFor(Class<?> domainClass);    (3)
    Grid load(final Class<?> domainClass);      (4)
}
1 whether dynamic reloading of layouts is enabled. The default implementation enables reloading for prototyping, disables in production
2 support metamodel invalidation/rebuilding of spec, eg as called by this Object mixin action.
3 whether any persisted layout metadata (eg a .layout.xml file) exists for this domain class.
4 returns a new instance of a Grid for the specified domain class, eg as loaded from a layout.xml file. If none exists, will return null (and the calling GridService will use GridSystemService to obtain a default grid for the domain class).

3.6.2. Implementation

The framework provides a default implementation of this service, namely GridLoaderServiceDefault. This implementation loads the grid from its serialized representation as a .layout.xml file, loaded from the classpath.

For example, the layout for a domain class com.mycompany.myapp.Customer would be loaded from com/mycompany/myapp/Customer.layout.xml.

3.6.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app), then the default implementation of GridLoaderService is automatically registered and injected, and no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

This service is used by GridService.

3.7. GridService

The GridService provides the ability to load the XML layout (grid) for a domain class. To do this it delegates:

  • to GridLoaderService to load a pre-existing layout for the domain class, if possible

  • to GridSystemService to normalize the grid with respect to Apache Isis' internal metamodel, in other words to ensure that all of the domain objects' properties, collections and actions are associated with regions of the grid.

Once a grid has been loaded for a domain class, this is cached internally by Apache Isis' internal meta model (in the GridFacet facet). If running in prototype mode, any subsequent changes to the XML will be detected and the grid rebuilt. This allows for dynamic reloading of layouts, providing a far faster feedback (eg if tweaking the UI while working with end-users). Dynamic reloading is disabled in production mode.

3.7.1. SPI

The SPI defined by this service is:

public interface GridService {
    boolean supportsReloading();                (1)
    void remove(Class<?> domainClass);          (2)
    boolean existsFor(Class<?> domainClass);    (3)
    Grid load(final Class<?> domainClass);      (4)
    Grid defaultGridFor(Class<?> domainClass);  (5)
    Grid normalize(final Grid grid);            (6)
    Grid complete(Grid grid);                   (7)
    Grid minimal(Grid grid);                    (8)
}
1 whether dynamic reloading of layouts is enabled. The default implementation enables reloading for prototyping, disables in production
2 support metamodel invalidation/rebuilding of spec, eg as called by this Object mixin action.
3 whether any persisted layout metadata (eg a .layout.xml file) exists for this domain class. Just delegates to corresponding method in GridLoaderService.
4 returns a new instance of a Grid for the specified domain class, eg as loaded from a layout.xml file. If none exists, will return null (and the calling GridService will use GridSystemService to obtain a default grid for the domain class).
5 returns a default grid, eg two columns in ratio 4:8. Used when no existing grid layout exists for a domain class.
6 validates and normalizes a grid, modifying the grid so that all of the domain object’s members (properties, collections, actions) are bound to regions of the grid. This is done using existing metadata, most notably that of the @MemberOrder annotation. Such a grid, if persisted as the layout XML file for the domain class, allows the @MemberOrder annotation to be removed from the source code of the domain class (but other annotations must be retained).
7 Takes a normalized grid and enriches it with additional metadata (taken from Apache Isis' internal metadata) that can be represented in the layout XML. Such a grid, if persisted as the layout XML file for the domain class, allows all layout annotations (@ActionLayout, @PropertyLayout and @CollectionLayout) to be removed from the source code of the domain class.
8 Takes a normalized grid and strips out removes all members, leaving only the grid structure. Such a grid, if persisted as the layout XML file for the domain class, requires that the @MemberOrder annotation is retained in the source code of said class in order to bind members to the regions of the grid.

The first four methods just delegate to the corresponding methods in GridSystemService, while the last four delegate to the corresponding method in GridSystemService. The service inspects the Grid's concrete class to determine which actual GridSystemService instance to delegate to.

3.7.2. Implementation

The framework provides a default implementation of this service, namely GridServiceDefault.

3.7.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app), then the default implementation of GridLoaderService is automatically registered and injected, and no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide). That said, there should be little reason to use a different implementation; if behaviour does need to be changed, it would also be possible to replace the implementation of either the GridLoaderService or the GridSystemService.

This service calls GridLoaderService and GridSystemService.

This service is called by LayoutService, exposed in the UI through LayoutServiceMenu (to download the layout XML as a zip file for all domain objects) and the downloadLayoutXml() mixin (to download the layout XML for a single domain object).

3.8. HintStore

The HintStore service defines an SPI for the Wicket viewer to store UI hints on a per-object basis. For example, the viewer remembers which tabs are selected, and for collections which view is selected (eg table or hidden), which page of a table to render, or whether "show all" (rows) is toggled.

The default implementation of this service uses the HTTP session. The service is an SPI because the amount of data stored could potentially be quite large (for large numbers of users who use the app all day). An SPI makes it easy to plug in an alternative implementation that is more sophisticated than the default (eg implementing MRU/LRU queue, or using a NoSQL database, or simply to disabling the functionality altogether).

3.8.1. SPI

The SPI of HintStore is:

public interface HintStore {
    String get(final Bookmark bookmark, String hintKey);                (1)
    void set(final Bookmark bookmark, String hintKey, String value);    (2)
    void remove(final Bookmark bookmark, String hintKey);               (3)
    void removeAll(Bookmark bookmark);                                  (4)
    Set<String> findHintKeys(Bookmark bookmark);                        (5)
}
1 obtain a hint (eg which tab to open) for a particular object. Object identity is represented by Bookmark, as per the BookmarkService, so that alternative implementations can easily serialize this state to a string.
2 set the state of a hint. (The value of) all hints are represented as strings.
3 remove a single hint for an object;
4 remove all hints
5 obtain all known hints for an object

3.8.2. Implementation

The core framework provides a default implementation of this service (org.apache.isis.viewer.wicket.viewer.services.HintStoreUsingWicketSession).

3.8.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of HintStore service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

The Wicket viewer exposes the "clear hints" mixin action that is for use by end-users of the application to clear any UI hints that have accumulated for a domain object.

3.9. LocaleProvider

The LocaleProvider service is one of the services that work together to implement Apache 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 Apache Isis' i18n support, see here.

3.9.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).

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

3.9.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app), 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.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

This service works in conjunction with TranslationService and TranslationsResolver in order to provide i18n support.

3.10. RoutingService

The RoutingService provides the ability to return (and therefore render) an alternative object from an action invocation.

There are two primary use cases:

  • if an action returns an aggregate leaf (that is, a child object which has an owning parent), then the parent object can be returned instead.

    For example, an action returning OrderItem might instead render the owning Order object. It is the responsibility of the implementation to figure out what the "owning" object might be.

  • if an action returns null or is void, then return some other "useful" object.

    For example, return the home page (eg as defined by the @HomePage annotation).

Currently the routing service is used only by the Wicket viewer; it is ignored by the Restful Objects viewer.

Unlike most other domain services, the framework will check all available implementations of RoutingService to return a route, rather than the first implementation found; in other words it uses the chain-of-responsibility pattern. Services are called in the order defined by @DomainServiceLayout#menuOrder()). The route used will be the result of the first implementation checked that declares that it can provide a route.

3.10.1. SPI

The SPI defined by this service is:

public interface RoutingService {
    @Programmatic
    boolean canRoute(Object original);  (1)
    @Programmatic
    Object route(Object original);      (2)
}
1 whether this implementation recognizes and can "route" the object. The route(…​) method is only called if this method returns true.
2 the object to use; this may be the same as the original object, some other object, or (indeed) null.

3.10.2. Implementation

The framework provides a default implementation which will always return the original object provided, or the home page if a null or void was provided. It uses the HomePageProviderService.

There can be multiple implementations of RoutingService registered. These are checked in turn (chain of responsibility pattern), ordered according to @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide). The route from the first service that returns true from its canRoute(…​) method will be used.

3.10.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' default implementation of RoutingService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

The default implementation of ths service uses the HomePageProviderService.

3.11. TranslationService

The TranslationService is the cornerstone of Apache Isis' i18n support. Its role is to be able to provide translated versions of the various elements within the Apache 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 Apache Isis' i18n support, see here.

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

3.11.2. Implementation

The Apache 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.

3.11.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache 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.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

The TranslationServicePoMenu menu exposes the TranslationServicePo service’s toPot() method so that all translations can be downloaded as a single file.

This service works in conjunction with LocaleProvider and TranslationsResolver in order to provide i18n support.

3.12. TranslationsResolver

The TranslationsResolver service is one of the services that work together to implement Apache 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 Apache Isis' i18n support, see here.

3.12.1. SPI

The SPI defined by this service is:

public interface TranslationsResolver {
    @Programmatic
    List<String> readLines(final String file);
}

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

3.12.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app), _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.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

This service works in conjunction with LocaleProvider and TranslationService in order to provide i18n support.

3.13. UrlEncodingService

The UrlEncodingService defines a consistent way to convert strings to/from a form safe for use within a URL. The service is used by the framework to map view model mementos (derived from the state of the view model itself) into a form that can be used as a view model. When the framework needs to recreate the view model (for example to invoke an action on it), this URL is converted back into a view model memento, from which the view model can then be hydrated.

Defining this functionality as an SPI has two use cases:

  • first, (though some browsers support longer strings), there is a limit of 2083 characters for URLs. For view model mementos that correspond to large strings (as might occur when serializing a JAXB @XmlRootElement-annotated view model), the service provides a hook.

    For example, each memento string could be mapped to a GUID held in some cluster-aware cache.

  • the service provides the ability, to encrypt the string in order to avoid leakage of potentially sensitive state within the URL.

The framework provides a default implementation of this service, UrlEncodingServiceUsingBaseEncoding (also in the applib) that uses base-64 encoding to UTF-8 charset.

3.13.1. SPI

The SPI defined by the service is:

public interface UrlEncodingService {
    @Programmatic
    public String encode(final String str);     (1)
    @Programmatic
    public String decode(String str);           (2)
}
1 convert the string (eg view model memento) into a string safe for use within an URL
2 unconvert the string from its URL form into its original form URL

3.13.2. Implementation

The framework provides a default implementation (UrlEncodingServiceUsingBaseEncoding) that simply converts the string using base-64 encoding and UTF-8 character set. As already noted, be aware that the maximum length of a URL should not exceed 2083 characters. For large view models, there’s the possibility that this limit could be exceeded; in such cases register an alternative implementation of this service.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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

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

3.14.2. Implementation

There is no default implementation of this service provided by the core Apache Isis framework.

An example implementation can be found in the (non-ASF) Isis addons' todoapp:

todoapp

Currently this feature is not integrated with Apache Isis' authentication mechanisms; the information provided is purely metadata provided for presentation purposes only.

4. Presentation Layer internal SPI

Domain service SPIs for the presentation layer influence how the Apache Isis viewers behave. In addition to those defined in the applib, the framework also defines a small number of services that use classes that are internal to the framework.

We do not guarantee that semantic versioning will be honoured for these ASPIs.

The table below summarizes the presentation layer non-public SPIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 2. "Internal" SPI Services
SPI Maven Module
Impl’n (g: a:)
Implementation Notes

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. 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 Apache 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 Apache 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.1.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.1.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.1.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.1.4. Configuration

The default ContentNegotiationServiceXRoDomainType implementation provides a configuration property which controls whether a mapped domain object is pretty-printed (formatted, indented) or not:

isis.services.ContentNegotiationServiceXRoDomainType.prettyPrint=true

If the property is not set, then the default depends on the deployment type; production mode will disable pretty printing, while prototyping mode will enable it.

4.1.5. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' default implementation of ContentNegotiationService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

4.2.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache 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).

5. Application Layer API

Domain service APIs for the application layer allow the domain objects to control aspects of the application layer, such as sending info messages back to the end-user.

The table below summarizes the application layer APIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 3. Application Layer API
API Description Implementation Notes

o.a.i.applib.
services.acceptheader
AcceptHeaderService

Request-scoped access to HTTP Accept headers.

AcceptHeaderServiceDefault
o.a.i.core
isis-viewer-restfulobjects-rendering

Populated only when the domain objects are accessed using the Restful Objects viewer.

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

depends on:
BackgroundCommand-Service

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.message
MessageService

Methods to inform or warn the user, or to raise errors.

FactoryService-
Default
o.a.i.core
isis-core-metamodel

Supercedes methods in DomainObjectContainer.

o.a.i.applib.
services.sessmgmt
SessionManagementService

Methods for batching long-running work (eg data migration) into multiple sessions.

SessionManagementService-
Default
o.a.i.core
isis-core-runtime

o.a.i.applib.
services.title
TitleService

Methods to programmatically obtain the title or icon of a domain object.

TitleService-
Default
o.a.i.core
isis-core-metamodel

Supercedes methods in DomainObjectContainer.

o.a.i.applib.
services.xactn
TransactionService

Methods for managing transactions.

TransactionService-
Default
o.a.i.core
isis-core-metamodel

Supercedes methods in DomainObjectContainer.

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

Key:

  • o.a.i is an abbreviation for org.apache.isis

  • o.ia.m is an abbreviation for org.isisaddons.module

5.1. AcceptHeaderService

The AcceptHeaderService domain service is a @RequestScoped service that simply exposes the HTTP Accept header to the domain. Its intended use is to support multiple versions of a REST API, where the responsibility for content negotiation (determining which version of the REST API is to be used) is managed by logic in the domain objects themselves.

As an alternative to performing content negotiation within the domain classes, the ContentNegotiationService and ContentMappingService SPI domain services allow the framework to perform the content negotiation responsibility.

5.1.1. API & Implementation

The API defined by the service is:

@DomainService(nature = NatureOfService.DOMAIN)
@RequestScoped                                      (1)
public interface AcceptHeaderService {
    @Programmatic
    List<MediaType> getAcceptableMediaTypes();      (2)
}
1 is @RequestScoped, so this domain service instance is scoped to a particular request and is then destroyed
2 returns the list of media types found in the HTTP Accept header.

The default implementation is provided by o.a.i.v.ro.rendering.service.acceptheader.AcceptHeaderServiceForRest.

Note that the service will only return a list when the request is initiated through the Restful Objects viewer. Otherwise the service will return null.

5.1.2. Usage

The intended use of this service is where there are multiple concurrent versions of a REST API, for backward compatibility of existing clients. The AcceptHeaderService allows the responsibility for content negotiation (determining which version of the REST API is to be used) to be performed by logic in the domain objects themselves.

The diagram below illustrated this:

acceptheaderservice

The REST request is submitted to a domain service with a nature of VIEW_REST_ONLY (MyRestApi in the diagram). This uses the AcceptHeaderService to obtain the values of the HTTP Accept header. Based on this it delegates to the appropriate underlying domain service (with a nature of DOMAIN so that they are not exposed in the REST API at all).

The service does not define any conventions as to the format of the media types. The option is to use the media type’s type/subtype, eg application/vnd.myrestapi-v1+json; an alternative is to use a media type parameter as a hint, eg application/json;x-my-rest-api-version=1 (where x-my-rest-api-version is the media type parameter).

The Restful Objects specification does this something similar with its own x-ro-domain-type media type parameter; this is used by the ContentMappingService to determine how to map domain objects to view models/DTOs.

5.1.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' default implementation of AcceptHeaderService class is automatically registered (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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

5.2.1. API & Implementation

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.

5.2.2. Usage

For actions that are void or that return null, Apache Isis will return to the list once executed. But for bulk actions that are non-void, Apache 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 ActionInvocationContext 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 actionInvocationContext.getInvokedOn() == InvokedOn.OBJECT
                ? this  (1)
                : null; (2)
    }
    @Inject
    ActionInvocationContext actionInvocationContext;
}
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

5.2.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' default implementation of ActionInvocationContext class is automatically registered (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

5.2.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);
    }
    ...
}

5.3. BackgroundService

The BackgroundService, and the companion BackgroundCommandService SPI service (used by Apache 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.

5.3.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).

5.3.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 Apache 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(…​).

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

5.3.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of BackgroundService is automatically registered (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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

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

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

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

5.4.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).

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

5.4.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).

5.4.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();

5.4.5. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of CommandContext service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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.

5.5. MessageService

The MessageService allows domain objects to raise information, warning or error messages. These messages can either be simple strings, or can be translated.

The methods in this service replace similar methods (now deprecated) in DomainObjectContainer.

5.5.1. API and Usage

The API of MessageService is:

public interface MessageService {

    void informUser(String message);                                                            (1)
    String informUser(TranslatableString message, Class<?> contextClass, String contextMethod); (2)

    void warnUser(String message);                                                              (3)
    String warnUser(TranslatableString message, Class<?> contextClass, String contextMethod);   (4)

    void raiseError(String message);                                                            (5)
    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) {
        messageService.warnUser(
            product.getDescription() + " out of stock; order fulfillment may be delayed");
    }
    ...
}

5.5.2. Implementation

The core framework provides a default implementation of this service (o.a.i.core.runtime.services.message.MessageServiceDefault).

5.5.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of MessageService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

5.6. SessionManagementService

The SessionManagementService provides the ability to programmatically manage sessions. The primary use case is for fixture scripts or other routines that are invoked from the UI and which create or modify large amounts of data. A classic example is migrating data from one system to another.

5.6.1. API

The API of SessionManagementService is:

public interface SessionManagementService {
    void nextSession();
}

Normally, the framework will automatically start a session and then a transaction before each user interaction (action invocation or property modification), and wil then commit that transaction and close the session after the interaction has completed. If the interaction throws an exception then the transaction is aborted.

The nextSession() method allows a domain object to commit the transaction, close the session, then open a new session and start a new transaction.

Any domain objects that were created in the "previous" session are no longer usable, and must not be rendered in the UI.

5.6.2. Implementation

The core framework provides a default implementation of this service (o.a.i.core.runtime.services.xactn.SessionManagementServiceDefault).

5.6.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of SessionManagementService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

5.7. TitleService

The TitleService provides methods to programmatically obtain the title and icon of a domain object.

The methods in this service replace similar methods (now deprecated) in DomainObjectContainer.

5.7.1. API

The API of TitleService is:

public interface PresentationService {
    String titleOf(Object domainObject);                (1)
    String iconNameOf(Object domainObject);             (2)
}
1 return the title of the object, as rendered in the UI by the Apache Isis viewers.
2 return the icon name of the object, as rendered in the UI by the Apache Isis viewers.

5.7.2. Usage

By way of example, here’s some code based on a system for managing government benefits:

public class QualifiedAdult {

    private Customer qualifying;

    public String title() {
        return "QA for " + titleService.titleOf(qualifying);
    }

    ...
    @Inject
    TitleService titleService;
}

In this example, whatever the title of a Customer, it is reused within the title of that customer’s QualifiedAdult object.

5.7.3. Implementation

The core framework provides a default implementation of this service (o.a.i.core.metamodel.services.title.TitleServiceDefault).

5.7.4. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of TitleService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

5.8. TransactionService

The TransactionService provides a small number of methods to allow domain objects to influence user transactions.

The methods in this service replace similar methods (now deprecated) in DomainObjectContainer.

5.8.1. API

The API of TransactionService is:

public interface TransactionService {
    void flushTransaction();
    void nextTransaction();
}

Normally, the framework will automatically start a transaction before each user interaction (action invocation or property modification), and commit that transaction after the interaction has completed. If the interaction throws an exception then the transaction is aborted.

If the user interaction creates and persists an object (using DomainObjectContainer's persist() method), then the framework actually defers the call and only performs the persistence command at the end of the transaction, or immediately prior to the next query.

/**
 *
 *
 * <p>
 * Occasionally useful to ensure that newly persisted domain objects
 * are flushed to the database prior to a subsequent repository query.
 */

/** * Intended only for use by fixture scripts and integration tests; commits this transaction and starts a new one. */

5.8.2. Implementation

The core framework provides a default implementation of this service (o.a.i.core.metamodel.services.xactn.TransactionServiceDefault).

5.8.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of TransactionService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

5.9. 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).

5.9.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 Apache 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.

5.9.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 Apache Isis does not provide an out-of-the-box implementation of such an ExceptionRecognizer; but it should be simple enough to write oneā€¦

5.9.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 Apache Isis' support for domain events. We may therefore deprecate this API in the future.

5.9.4. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of WrapperFactory service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

6. Application Layer SPI

Domain service SPIs influence how the framework handles application layer concerns, for example which home page to render to the end-user.

The table below summarizes the application layer SPIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 4. Application Layer SPI
API Description Implementation Notes

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.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.homepage
HomePageProviderService

Returns the home page object, if any is defined.

HomePageProvider
ServiceDefault
o.a.i.core
isis-core-runtime

Used by the default implementation of RoutingService.

Key:

  • o.a.i is an abbreviation for org.apache.isis

  • o.ia.m is an abbreviation for org.isisaddons.module

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

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

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

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

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

6.1.5. Registering the Services

The (non-ASF) Isis addons' command module provides an implementation of this service (BackgroundCommandService), and also provides a number of related domain services (BackgroundCommandServiceJdo, BackgroundCommandJdoRepository and BackgroundCommandServiceJdoContributions). This module also provides service implementations of the CommandService.

Assuming that an AppManifest is being used to bootstrap the app) then this can be activated by updating the pom.xml and updating the AppManifest#getModules() method.

If contributions are not required in the UI, these can be suppressed either using security or by implementing a vetoing subscriber.

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

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

This 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).

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

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

6.2.3. Usage

The typical way to indicate that an action should be reified into a Command is by annotating the action using @Action#command().

6.2.4. Registering the Services

The (non-ASF) Isis addons' command module provides an implementation of this service (CommandService), and also provides a number of related domain services (CommandJdoRepository and CommandServiceJdoContributions). This module also provides service implementations of the BackgroundCommandService.

Assuming that an AppManifest is being used to bootstrap the app) then this can be activated by updating the pom.xml and updating the AppManifest#getModules() method.

If contributions are not required in the UI, these can be suppressed either using security or by implementing a vetoing subscriber.

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.

6.3. HomePageProviderService

This service simply provides access to the home page object (if any) that is returned from the domain service action annotated with @HomePage.

It is originally introduced to support the default implementation of RoutingService, but was factored out to support alternative implementations of that service (and may be useful for other use cases).

6.3.1. API & Implementation

The API defined by HomePageProviderService is:

@DomainService(nature = NatureOfService.DOMAIN)
public interface HomePageProviderService {
    @Programmatic
    Object homePage();
}

The default implementation is provided by o.a.i.core.runtime.services.homepage.HomePageProviderServiceDefault.

6.3.2. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of HomePageProviderService is automatically registered (it is annotated with @DomainService) so no further configuration is required.

7. Core/Domain API

The core/domain APIs provide general-purpose services to the domain objects, for example obtaining the current time or user, or instantiating domain objects.

The table below summarizes the core/domain APIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 5. Core/Domain Layer API
API Description Implementation Notes

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.config
ConfigurationService

Access configuration properties (eg from isis.properties file)

ConfigurationService-
Default
o.a.i.core
isis-core-runtime

The ConfigurationServiceMenu exposes the allConfigurationProperties action in the user interface.
+ Supercedes methods in DomainObjectContainer.

o.a.i.applib
DomainObjectContainer

Miscellaneous functions, eg obtain title of object.

DomainObjectContainer-
Default
o.a.i.core
isis-core-metamodel

o.a.i.applib.
services.eventbus
EventBusService

Programmatically post events to the internal event bus. Also used by Apache Isis itself to broadcast domain events:

EventBusServiceJdo
o.a.i.core
isis-core-objectstore-jdo-datanucleus

o.a.i.applib.
services.factory
FactoryService

Methods to instantiate and initialize domain objects

FactoryService-
Default
o.a.i.core
isis-core-metamodel

Supercedes methods in DomainObjectContainer.

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.xactn
UserService

Methods to access the currently-logged on user.

UserService-
Default
o.a.i.core
isis-core-metamodel

Supercedes methods in DomainObjectContainer.

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

7.1. 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). Apache Isis provides such a facade through the ClockService.

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

7.1.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 Apache 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

7.1.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache 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).

7.2. ConfigurationService

The ConfigurationService allows domain objects to read the configuration properties aggregated from the various configuration files.

The methods in this service replace similar methods (now deprecated) in DomainObjectContainer.

7.2.1. API and Usage

The API of ConfigurationService is:

public interface ConfigurationService {

    String getProperty(String name);                        (1)
    String getProperty(String name, String defaultValue);   (2)
    List<String> getPropertyNames();                        (3)
    Set<ConfigurationProperty> allProperties();             (4)

}
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.
4 Returns all properties, each as an instance of the ConfigurationProperty view model.

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 = configurationService.getProperty("tweetservice.oauth.consumerKey");
        this.oauthConsumerSecret = configurationService.getProperty("tweetservice.oauth.consumerSecret");
        this.oauthAccessToken = configurationService.getProperty("tweetservice.oauth.accessToken");
        this.oauthAccessTokenSecret = configurationService.getProperty("tweetservice.oauth.accessTokenSecret");
    }
    ...
    @Inject
    ConfigurationService configurationService;
}

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.

7.2.2. Implementation

The core framework provides a default implementation of this service (o.a.i.core.runtime.services.config.ConfigurationServiceDefault).

7.2.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of ConfigurationService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

The ConfigurationServiceMenu exposes the allConfigurationProperties action in the user interface.

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

(Almost all of) the methods in this service have been moved out into a number of more fine-grained services: RepositoryService, MessageService, FactoryService, TitleService, ConfigurationService, UserService and ServiceRegistry.

7.3.1. APIs

The sections below discuss the functions provided by the service, broken out into categories.

Object Creation API

The object creation APIs are used to instantiate new domain objects or view models.

public interface DomainObjectContainer {

    <T> T newTransientInstance(final Class<T> ofType);                              (1)
    <T> T newViewModelInstance(final Class<T> ofType, final String memento);        (2)
    <T> T mixin();                                                                  (3)
    ...
}
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 Apache Isis manage the memento automatically.
3 programmatically instantiate a mixin, as annotated with @Mixin or @DomainObject#nature().

For example:

Customer cust = container.newTransientInstance(Customer.class);
cust.setFirstName("Freddie");
cust.setLastName("Mercury");
container.persist(cust);

As an alternative to using newTransientInstance(…​) or mixin(…​), 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 Apache Isis' created() callback, and in addition any default values for properties (either explicitly set by default…​() or defaulted implicitly according to Apache 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).

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 {
    public <T> List<T> allInstances(Class<T> ofType, long... range);                        (1)
    <T> List<T> allMatches(Query<T> query);                                                 (2)
    <T> List<T> allMatches(Class<T> ofType, Predicate<? super T> predicate, long... range); (3)
    <T> List<T> allMatches(Class<T> ofType, String title, long... range);                   (4)
    <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.

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 {

    boolean isPersistent(Object domainObject);          (1)
    boolean isViewModel(Object domainObject);           (2)

    void persist(Object domainObject);                  (3)
    void persistIfNotAlready(Object domainObject);      (4)

    void remove(Object persistentDomainObject);         (5)
    void removeIfNotAlready(Object domainObject);       (6)

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

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 {

    void informUser(String message);                                                            (1)
    String informUser(TranslatableString message, Class<?> contextClass, String contextMethod); (2)

    void warnUser(String message);                                                              (3)
    String warnUser(TranslatableString message, Class<?> contextClass, String contextMethod);   (4)

    void raiseError(String message);                                                            (5)
    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");
    }
    ...
}
Security API

The security API allows the domain object to obtain the identity of the user interacting with said object.

public interface DomainObjectContainer {
    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.

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 Apache 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 {
    String titleOf(Object domainObject);                (1)
    String iconNameOf(Object domainObject);             (2)
    ...
}
1 return the title of the object, as rendered in the UI by the Apache Isis viewers.
2 return the icon name of the object, as rendered in the UI by the Apache 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
Properties API

The properties API allows domain objects to read the configuration properties aggregated from the various configuration files.

public interface DomainObjectContainer {
    String getProperty(String name);                        (1)
    String getProperty(String name, String defaultValue);   (2)
    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.

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.

The methods are:

public interface DomainObjectContainer {
    <T> T injectServicesInto(final T domainObject);     (1)
    <T> T lookupService(Class<T> service);              (2)
    <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);
Validation API

The intent of this API is to provide a mechanism where an object can programmatically check the state any class invariants. Specifically, this means the validating the current state of all properties, as well as any object-level validation defined by validate().

These methods have been deprecated; this feature should be considered experimental and your mileage may vary.

The API provided is:

public interface DomainObjectContainer {
    boolean isValid(Object domainObject);
    String validate(Object domainObject);
    ...
}

7.3.2. Implementation

The core framework provides a default implementation of this service (o.a.i.core.metamodel.services.container.DomainObjectContainerDefault).

7.3.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of DomainObjectContainer service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

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

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

7.4.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)   (1)
@DomainServiceLayout( menuOrder="1")            (2)
public class MySubscribingDomainService {
    @PostConstruct
    public void postConstruct() {
        eventBusService.register(this);         (3)
    }
    @PreDestroy
    public void preDestroy() {
        eventBusService.unregister(this);       (4)
    }
    ...
    @javax.inject.Inject
    EventBusService eventBusService;
}
1 subscribers are typically not visible in the UI, so specify a DOMAIN nature
2 It’s important that subscribers register before any domain services that might emit events on the event bus service. For example, the (non-ASF) Isis addons' security module provides a domain service that automatically seeds certain domain entities; these will generate lifecycle events and so any subscribers must be registered before such seed services. The easiest way to do this is to use the @DomainServiceLayout#menuOrder() attribute.
3 register with the event bus service during @PostConstruct initialization
4 corresponding deregister when shutting down

This works for both singleton (application-scoped) and also @RequestScoped domain services.

The AbstractSubscriber class automatically performs this registration. As a convenience, it is also annotated with the @DomainServiceLayout#menuOrder() attribute.

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

7.4.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 Apache 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> {}
    ...                                                                             (1)
    public abstract static class PropertyDomainEvent<S,T>
                extends org.apache.isis.applib.event.PropertyDomainEvent<S,T> {}
    public abstract static class CollectionDomainEvent<S,E>
                extends org.apache.isis.applib.event.CollectionDomainEvent<S,E> {}
}
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> { }
    ...                                                                             (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 { }
    @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.

Variation (for contributing services)

A slight variation on this is to not fix the generic parameter at the class level, ie:

public static class LibraryMember {
    public abstract static class ActionDomainEvent<S>
            extends LibMemModule.ActionDomainEvent<S> { }
    ...
}

and instead parameterize down at the action level:

public class LibraryMember {
    public static class LeaveEvent
            extends LibraryMember.ActionDomainEvent<LibraryMember> { } (1)
    }
    @Action(domainEvent=LeaveEvent.class)
    public void leave() { ... }
    ...
}

This then allows for other classes - in particular domain services contributing members - to also inherit from the class-level domain events.

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

7.4.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;
}

7.4.7. Implementation SPI

The implementation of EventBusService provided by Apache 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.

7.4.8. Configuration

The implementation of EventBusService provided by Apache 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.

7.4.9. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of EventBusService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

The EventBusService is intended for fine-grained publish/subscribe for object-to-object interactions within an Apache 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 Apache 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()).

7.5. FactoryService

The FactoryService collects together methods for instantiating domain objects.

The methods in this service replace similar methods (now deprecated) in DomainObjectContainer.

7.5.1. API

The API of FactoryService is:

public interface FactoryService {
    <T> T instantiate(final Class<T> ofType);       (1)
    <T> T mixin();                                  (2)
}
1 create a new non-persisted domain entity. Any services will be automatically injected into the service.
2 programmatically instantiate a mixin, as annotated with @Mixin or @DomainObject#nature().

The object is created in memory, but is not persisted. The benefits of using this method (instead of simply using the Java new keyword) are:

  • any services will be injected into the object immediately (otherwise they will not be injected until the frameworkbecomes aware of the object, typically when it is persisted through the RepositoryService

  • the default value for any properties (usually as specified by defaultXxx() supporting methods) will not be set and the created() callback will be called.

The corollary is: if your code never uses defaultXxx() or the created() callback, then you can just new up the object. The ServiceRegistry service can be used to inject services into the domain object.

7.5.2. Usage

For example:

Customer cust = factoryService.instantiate(Customer.class);
cust.setFirstName("Freddie");
cust.setLastName("Mercury");
repositoryService.persist(cust);

7.5.3. Implementation

The core framework provides a default implementation of this service (o.a.i.core.metamodel.services.factory.FactoryServiceDefault).

7.5.4. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of FactoryService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

The RepositoryService is often used in conjunction with the FactoryService, to persist domain objects after they have been instantiated and populated.

An alternative to using the factory service is to simply instantiate the object ("new is the new new") and then use the ServiceRegistry service to inject other domain services into the instantiated object.

7.6. Scratchpad

The Scratchpad service is a request-scoped service to allow objects to exchange information even if they do not directly call each other.

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

7.6.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:

@Action(
    semantics=SemanticsOf.SAFE,
    invokeOn=InvokeOn.COLLECTION_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.

7.6.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of Scratchpad service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

The ActionInteractionContext service allows bulk actions to co-ordinate with each other.

The QueryResultsCache is useful for caching the results of expensive method calls.

7.7. UserService

The UserService allows the domain object to obtain the identity of the user interacting with said object.

The methods in this service replace similar methods (now deprecated) in DomainObjectContainer.

7.7.1. API and Usage

The API of UserService is:

public interface UserService {
    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.

7.7.2. Implementation

The core framework provides a default implementation of this service (o.a.i.core.runtime.services.user.UserServiceDefault).

7.7.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of UserService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

8. Integration API

The integration APIs provide functionality to the domain objects to integrate with other bounded contexts, for example sending an email or serializing an object out to XML.

The table below summarizes the integration APIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 7. Integration API
API Description Implementation Notes

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.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.
services.email
EmailService

Send a HTML email, optionally with attachments.

EmailServiceDefault
o.a.i.core
isis-core-runtime

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.jaxb
JaxbService

Marshal and unmarshal JAXB-annotated view models to/from XML.

JaxbServiceDefault
o.a.i.core
isis-core-schema

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.xmlsnapshot
XmlSnapshotService

Generate an XML representation of an object and optionally a graph of related objects.

XmlSnapshotServiceDefault
o.a.i.core
isis-core-runtime

8.1. 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 Apache Isis' internal Oid (object identifier). Nevertheless, the ability to uniquely address any domain object within an Apache 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.

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

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

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

8.1.4. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of BookmarkService is automatically registered (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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

8.2.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 Apache 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).

8.2.2. Usage within the framework

The EmailNotificationService uses this service in order to generate emails as part of user registration.

8.2.3. Implementations

The Wicket viewer core framework provides a default implementation of this API:

  • org.apache.isis.viewer.wicket.viewer.services.DeepLinkServiceWicket

8.2.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app), _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.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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

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

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

8.3.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 Apache 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 Apache 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 Apache 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.

8.3.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of EmailService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

The email service is used by the EmailNotificationService which is, in turn, used by UserRegistrationService.

8.4. GuiceBeanProvider

The GuiceBeanProvider domain service acts as a bridge between Apache 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 Apache 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 Apache Isis to be injected into your domain object code.

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

8.4.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".

8.4.3. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app), _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.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

8.5. JaxbService

The JaxbService allows instances of JAXB-annotated classes to be marshalled to XML and unmarshalled from XML back into domain objects.

8.5.1. API & Implementation

The API defined by JaxbService is:

public interface JaxbService {
    @Programmatic
    <T> T fromXml(Class<T> domainClass, String xml);                                            (1)
    @Programmatic
    public String toXml(final Object domainObject);                                             (2)
    public enum IsisSchemas {                                                                   (3)
        INCLUDE, IGNORE
    }
    @Programmatic
    public Map<String, String> toXsd(final Object domainObject, final IsisSchemas isSchemas);} (4)
}
1 unmarshalls the XML into an instance of the class.
2 marshalls the domain object into XML
3 whether to include or exclude the Isis schemas in the generated map of XSDs. Discussed further below.
4 generates a map of each of the schemas referenced; the key is the schema namespace, the value is the XML of the schema itself.

With respect to the IsisSchemas enum: a JAXB-annotated domain object will live in its own XSD namespace and may reference multiple other XSD schemas. In particular, many JAXB domain objects will reference the common Isis schemas (for example the OidDto class that represents a reference to a persistent entity). The enum indicates whether these schemas should be included or excluded from the map.

Isis provides a default implementation of the service, o.a.i.schema.services.jaxb.JaxbServiceDefault.

8.5.2. Usage within the framework

This service is provided as a convenience for applications, but is also used internally by the framework to @XmlRootElement-annotated view models. The functionality to download XML and XSD schemas is also exposed in the UI through mixins to Dto interface.

8.5.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of JaxbService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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

8.6.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 Apache 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.

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

8.6.4. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of MementoService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

8.7. 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).

8.7.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).

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

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

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

8.7.5. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of XmlSnapshotService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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.

The JaxbService is a simple wrapper around standard JAXB functionality for generating both XMLs and XSDs from JAXB-annotated classes. Note that there is built-in support for JAXB classes (ie annotated with @XmlRootElement) to be used as view models.

9. Metadata API

The metadata APIs provide access to the framework’s internal metamodel. These are generally of use to support development-time activities, for example creating custom UIs through Swagger.

The table below summarizes the metadata APIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 8. Metadata API
API Description Implementation Notes

o.a.i.applib.
services.appfeat
ApplicationFeatureRepository

Provides access to string representations of the features (package, class, class members) of the domain classes within the metamodel.

ApplicationFeatureDefault
o.a.i.core
isis-core-metamodel

(not visible in UI)

o.a.i.applib.
services.layout
LayoutService

Provides the ability to download dynamic layout XML files, in various styles.

LayoutServiceDefault
o.a.i.core
isis-core-metamodel

Functionality surfaced in the UI through related mixin and menu.

o.a.i.applib.
services.metamodel
MetaModelService

Access to certain information from the Apache Isis metamodel.

MetaModelServiceDefault
o.a.i.core
isis-core-metamodel

Functionality surfaced in the UI through related menu.

o.a.i.applib.
services.registry
ServiceRegistry

Methods to access and use other domain services.

ServiceRegistry-
Default
o.a.i.core
isis-core-metamodel

Supercedes methods in DomainObjectContainer.

o.a.i.applib.
services.swagger
SwaggerService

Generates Swagger spec files to describe the public and/or private RESTful APIs exposed by the RestfulObjects viewer. These can then be used with the Swagger UI page to explore the REST API, or used to generate client-side stubs using the Swagger codegen tool, eg for use in a custom REST client app.

SwaggerServiceDefault
o.a.i.core
isis-core-metamodel

A SwaggerServiceMenu domain service is also provided which enables the swagger spec to be downloaded. Apache Isis' Maven plugin also provides a swagger goal which allows the spec file(s) to be generated at build time (eg so that client-side stubs can then be generated in turn).

9.1. ApplicationFeatureRepository

The ApplicationFeatureRepository provides the access to string representations of the packages, classes and class members (collectively: "application features") of the domain classes within the Apache Isis' internal metamodel.

This functionality was originally implemented as part of (non-ASF) Isis Addons security module, where the string representations of the various features are used to represent permissions.

9.1.1. API & Implementation

The API defined by the service is:

public interface ApplicationFeatureRepository {
    List<String> packageNames();
    List<String> packageNamesContainingClasses(ApplicationMemberType memberType);
    List<String> classNamesContainedIn(String packageFqn, ApplicationMemberType memberType);
    List<String> classNamesRecursivelyContainedIn(String packageFqn);
    List<String> memberNamesOf(String packageFqn, String className, ApplicationMemberType memberType);
}

where ApplicationMemberType in turn is:

public enum ApplicationMemberType {
    PROPERTY,
    COLLECTION,
    ACTION;
}

These methods are designed primarily to return lists of strings for use in drop-downs.

9.1.2. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of ApplicationFeatureRepository service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

9.2. LayoutService

The LayoutService provides the ability to obtain the XML layout for a single domain object or for all domain objects. This functionality is surfaced through the user interface through a related mixin and menu action.

9.2.1. API & Implementation

The API defined by LayoutService is:

public interface LayoutService {
    String toXml(Class<?> domainClass, Style style);        (1)
    byte[] toZip(Style style);                              (2)
}
1 Returns the serialized XML form of the layout (grid) for the specified domain class, in specified style (discussed below).
2 Returns (a byte array) of a zip of the serialized XML of the layouts (grids), for all domain entities and view models.

The Style enum is defined as:

enum Style {
    CURRENT,
    COMPLETE,
    NORMALIZED,
    MINIMAL
}

The CURRENT style corresponds to the layout already loaded for the domain class, typically from an already persisted layout.xml file. The other three styles allow the developer to choose how much metadata is to be specified in the XML, and how much (if any) will be obtained elsewhere, typically from annotations in the metamodel (but also from .layout.json file if present). The table below summarises the choices:

Table 9. Table caption
Style @MemberGroupLayout @MemberOrder @ActionLayout, @PropertyLayout, @CollectionLayout

COMPLETE

serialized as XML

serialized as XML

serialized as XML

NORMALIZED

serialized as XML

serialized as XML

not in the XML

MINIMAL

serialized as XML

not in the XML

not in the XML

As a developer, you therefore have a choice as to how you provide the metadata required for customised layouts:

  • if you want all layout metadata to be read from the .layout.xml file, then download the "complete" version, and copy the file alongside the domain class. You can then remove all @MemberGroupLayout, @MemberOrder, @ActionLayout, @PropertyLayout and @CollectionLayout annotations from the source code of the domain class.

  • if you want to use layout XML file to describe the grid (columns, tabs etc) and specify which object members are associated with those regions of the grid, then download the "normalized" version. You can then remove the @MemberGroupLayout and @MemberOrder annotations from the source code of the domain class, but retain the @ActionLayout, @PropertyLayout and @CollectionLayout annotations.

  • if you want to use layout XML file ONLY to describe the grid, then download the "minimal" version. The grid regions will be empty in this version, and the framework will use the @MemberOrder annotation to bind object members to those regions. The only annotation that can be safely removed from the source code with this style is the @MemberGroupLayout annotation.

The service’s functionality is exposed in the UI through a mixin (per object) and a menu action (for all objects):

  • the Object mixin provides the ability to download the XML layout for any domain object (entity or view model).

  • the LayoutServiceMenu provides the ability to download all XML layouts as a single ZIP file (in any of the three styles).

The XML can then be copied into the codebase of the application, and annotations in the domain classes removed as desired.

The GridService is responsible for loading and normalizing layout XML for a domain class. It in turn uses the GridLoaderService and GridSystemService services.

9.3. MetaModelService

The MetaModelService provides access (albeit currently extremely limited) to aspects of Apache 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.

9.3.1. API

The API defined by the service is:

public interface MetaModelService {
    Class<?> fromObjectType(final String objectType);   (1)
    String toObjectType(final Class<?> domainType);     (2)
    void rebuild(final Class<?> domainType);            (3)
    List<DomainMember> export();                        (4)
}
1 reverse lookup of a domain class' object type
2 lookup of a domain class' object type
3 invalidate and rebuild the internal metadata (an ObjectSpecification) for the specified domain type.
4 returns a list of representations of each of member of each domain class.

9.3.2. Implementation

The framework provides a default implementation of this service (org.apache.isis.core.metamodel.services.metamodel.MetaModelServiceDefault).

9.3.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of MetamodelService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

The MetaModelServiceMenu provides a method to download all domain members as a CSV. Internally this calls MetaModelService#export().

9.4. ServiceRegistry

The ServiceRegistry service collects together methods for accessing other domain services.

The methods in this service replace similar methods (now deprecated) in DomainObjectContainer.

9.4.1. API

The API of ServiceRegistry is:

public interface ServiceRegistry {
    <T> T injectServicesInto(final T domainObject);     (1)
    <T> T lookupService(Class<T> service);              (2)
    <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).
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

Service injection is done automatically if objects are created using the .adoc#_rgsvc_api_FactoryService[FactoryService]

9.4.2. Usage

The primary use case is to instantiate domain objects using a regular constructor ("new is the new new"), and then using the #injectServicesInto(…​) API to set up any dependencies.

For example:

Customer cust = serviceRegistry.injectServicesInto( new Customer());
cust.setFirstName("Freddie");
cust.setLastName("Mercury");
repositoryService.persist(cust);

The alternative is to use the FactoryService API which performs both steps in a single factory method.

9.4.3. Implementation

The core framework provides a default implementation of this service (o.a.i.core.runtime.services.registry.ServiceRegistryDefault).

9.4.4. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of ServiceRegistry service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

9.5. SwaggerService

The SwaggerService generates Swagger spec files to describe the public and/or private RESTful APIs exposed by the RestfulObjects viewer.

These spec files can then be used with the Swagger UI page to explore the REST API, or used to generate client-side stubs using the Swagger codegen tool, eg for use in a custom REST client app.

Not all of the REST API exposed by the Restful Objects viewer is included in the Swagger spec files; the emphasis is those REST resources that are used to develop custom apps: domain objects, domain object collections and action invocations. When combined with Apache Isis' own simplified representations, these are pretty much all that is needed for this use case.

9.5.1. API & Implementation

The API defined by SwaggerService is:

public interface SwaggerService {
    enum Visibility {
        PUBLIC,                     (1)
        PRIVATE,                    (2)
        PRIVATE_WITH_PROTOTYPING;   (3)
    }
    enum Format {                   (4)
        JSON,
        YAML
    }
    String generateSwaggerSpec(final Visibility visibility, final Format format);
}
1 Generate a Swagger spec for use by third-party clients, ie public use. This specification is restricted only to view models and to domain services with a nature of VIEW_REST_ONLY.
2 Generate a Swagger spec for use only by internally-managed clients, ie private internal use. This specification includes domain entities and all menu domain services (as well as any view models).
3 Generate a Swagger spec that is the same as private case (above), but also including any prototype actions.
4 Swagger specs can be written either in JSON or YAML format.

Isis provides a default implementation of the service, o.a.i.core.metamodel.services.swagger.SwaggerServiceDefault.

9.5.2. Usage within the framework

This service is provided as a convenience for applications, it is not (currently) used by the framework itself.

9.5.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of SwaggerService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

A SwaggerServiceMenu domain service provides a prototype action that enables the swagger spec to be downloaded from the Wicket viewer’s UI.

Apache Isis' Maven plugin also provides a swagger goal which allows the spec file(s) to be generated at build time. this then allows client-side stubs can then be generated in turn as part of a build pipeline.

10. Testing

The testing APIs provide functionality to domain objects for use when testing or demoing an application.

The testing SPIs allow the framework to provide supporting functionality for use when testing or demoing an application.

The table below summarizes the testing APIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 10. Testing API
API Description Implementation Notes

o.a.i.applib.
services.
fixturespec
FixtureScriptsDefault

Fallback implementation of FixtureScripts, providing the ability to execute fixture scripts.

FixtureScriptsDefault
o.a.i.core
isis-core-applib

Interacts with FixtureScripts- SpecificationProvider.

o.a.i.applib.
services.fixturespec
FixtureScripts-
SpecificationProvider

Provides settings for FixtureScriptsDefault fallback domain service for executing fixture scripts.

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

The table below summarizes the testing SPIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 11. Testing SPI
SPI Description Implementation Notes

10.1. FixtureScriptsDefault

The FixtureScriptsDefault service provides the ability to execute fixture scripts .

The service extends from the FixtureScripts, and is only instantiated by the framework if there no custom implementation of FixtureScripts has been otherwise provided; in other words it is a fallback.

If this service is instantiated (as a fallback) then it uses the FixtureScriptsSpecificationProvider to obtain a FixtureScriptsSpecification. This configures this service, telling it which package to search for FixtureScript classes, how to execute those classes, and hints that influence the UI.

We recommend using FixtureScriptsSpecificationProvider rather than subclassing FixtureScripts.

10.1.1. API & Implementation

The API for the service is:

public class FixtureScriptsDefault ... {
    @Programmatic
    public List<FixtureResult> runFixtureScript(
        FixtureScript fixtureScript,
        String parameters) { ... }
}
  1. in other words the same as FixtureScripts superclass that it inherits from.

10.1.2. Configuration

As noted in the introduction, this service is only instantiated if there is no other implementation of FixtureScripts available on the classpath.

If an instance of FixtureScriptsSpecificationProvider is available on the classpath, then the service will be visible in the UI (assuming prototype mode). Otherwise the service will be available only to be injected and invoked programmatically.

The service interacts with FixtureScriptsSpecificationProvider.

10.2. 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).

10.2.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).

10.2.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();
                }
            });
    ...
}

10.2.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of SudoService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

10.3. FixtureScriptsSpec’nProvider

The FixtureScriptsSpecificationProvider configures the FixtureScriptsDefault domain service, providing the location to search for fixture scripts and other settings.

The service is only used if the FixtureScriptsDefault service is instantiated as a fallback by the framework. If the application provides its own subclass of FixtureScripts superclass, then this provider service is not used.

Of the two designs, we encourage you to implement this "provider" SPI rather than subclass FixtureScripts. The primary benefit (apart from decoupling responsibilities) is that it ensures that there is always an instance of FixtureScripts available for use.

10.3.1. SPI

The SPI defined by the service is:

public interface FixtureScriptsSpecificationProvider {
    @Programmatic
    FixtureScriptsSpecification getSpecification();
}

where FixtureScriptsSpecification exposes these values:

public class FixtureScriptsSpecification {
    public String getPackagePrefix() { ... }
    public FixtureScripts.NonPersistedObjectsStrategy getNonPersistedObjectsStrategy() { ... }
    public FixtureScripts.MultipleExecutionStrategy getMultipleExecutionStrategy() { ... }
    public Class<? extends FixtureScript> getRunScriptDefaultScriptClass() { ... }
    public DropDownPolicy getRunScriptDropDownPolicy() { ... }
    public Class<? extends FixtureScript> getRecreateScriptClass() { ... }
    ...
}

The class is immutable but it has a builder (obtained using FixturescriptsSpecification.builder(…​)) for a fluent API.

10.3.2. Implementation

The SimpleApp archetype has a simple implementation of this service:

@DomainService(nature = NatureOfService.DOMAIN)
public class DomainAppFixturesProvider implements FixtureScriptsSpecificationProvider {
    @Override
    public FixtureScriptsSpecification getSpecification() {
        return FixtureScriptsSpecification
                .builder(DomainAppFixturesProvider.class)
                .with(FixtureScripts.MultipleExecutionStrategy.EXECUTE)
                .withRunScriptDefault(RecreateSimpleObjects.class)
                .withRunScriptDropDown(FixtureScriptsSpecification.DropDownPolicy.CHOICES)
                .withRecreate(RecreateSimpleObjects.class)
                .build();
    }
}

11. Persistence Layer API

The persistence layer APIs provide domain objects with tools to manage the interactions with the persistence layer, for example adding on-the-fly caching to queries that are called many times within a loop.

The table below summarizes the persistence layer APIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 12. Persistence Layer API
API Description Implementation Notes

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.
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.repository
RepositoryService

Methods to help implement repositories: query for existing objects, persist new or delete existing objects

RepositoryService-
Default
o.a.i.core
isis-core-metamodel

Supercedes methods in DomainObjectContainer.

Key:

  • o.a.i is an abbreviation for org.apache.isis

  • o.ia.m is an abbreviation for org.isisaddons.module

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

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

11.1.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;
}

11.1.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 Apache 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 Apache 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.

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

11.1.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;
}

11.1.6. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of IsisJdoSupport service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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

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

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

11.2.3. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of QueryResultsCache service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

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.

11.3. RepositoryService

The RepositoryService collects together methods for creating, persisting and searching for entities from the underlying persistence store. It 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.

The methods in this service replace similar methods (now deprecated) in DomainObjectContainer.

11.3.1. API

The API of RepositoryService is:

public interface RepositoryService {

    <T> T instantiate(final Class<T> ofType);                                               (1)

    boolean isPersistent(Object domainObject);                                              (2)
    void persist(Object domainObject);                                                      (3)
    void remove(Object persistentDomainObject);                                             (4)

    <T> List<T> allInstances(Class<T> ofType, long... range);                               (5)

    <T> List<T> allMatches(Query<T> query);                                                 (6)
    <T> List<T> allMatches(Class<T> ofType, Predicate<? super T> predicate, long... range); (7)

    <T> T uniqueMatch(Query<T> query);                                                      (8)
    <T> T uniqueMatch(final Class<T> ofType, final Predicate<T> predicate);                 (9)

    <T> T firstMatch(Query<T> query);                                                       (10)
    <T> T firstMatch(final Class<T> ofType, final Predicate<T> predicate);                  (11)

}
1 create a new non-persisted domain entity. This is identical to FactoryService's instantiate(…​) method, but is provided in the RepositoryService's API too because instantiating and persisting objects are often done together.
2 test whether a particular domain object is persistent or not
3 persist (ie save) an object to the persistent object store (or do nothing if it is already persistent).
4 remove (ie delete) an object from the persistent object store (or do nothing if it has already been deleted).
5 return 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.
6 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
7 As the previous, but with client-side filtering using a Predicate. Only really intended for prototyping.
8 Returns the first instance that matches the supplied query. If no instance is found then `null `will be returned, while if there is more that one instances a run-time exception will be thrown. Generally this method is preferred for looking up an object by its (primary or alternate) key.
9 As the previous, but with client-side filtering using a Predicate. Only really intended for prototyping.
10 Returns the first instance that matches the supplied query. If no instance is found then null `will be returned. No exception is thrown if more than one matches, so this is less strict that `uniqueMatch(…​).
11 As the previous, but with client-side filtering using a Predicate. Only really intended for prototyping.

The uniqueMatch(…​) methods are the recommended way of querying for (precisely) one instance. The firstMatch(…​) methods are for less strict querying.

11.3.2. Usage

This section briefly discusses how application code can use these APIs.

Query
Customer cust = repositoryService.instantiate(Customer.class);
cust.setFirstName("Freddie");
cust.setLastName("Mercury");
repositoryService.persist(cust);

You should be aware that by default Apache 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.

Query

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.

11.3.3. Implementation

The core framework provides a default implementation of this service (o.a.i.core.metamodel.services.repository.RepositoryServiceDefault).

(Disabling) Auto-flush

Normally any queries are automatically preceded by flushing pending commands to persist or remove objects.

This key allows this behaviour to be disabled.

 *
 * <p>
 *     Originally introduced as part of ISIS-1134 (fixing memory leaks in the objectstore)
 *     where it was found that the autoflush behaviour was causing a (now unrepeatable)
 *     data integrity error (see <a href="https://issues.apache.org/jira/browse/ISIS-1134?focusedCommentId=14500638&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-14500638">ISIS-1134 comment</a>, in the isis-module-security.
 *     However, that this could be circumvented by removing the call to flush().
 *     We don't want to break existing apps that might rely on this behaviour, on the
 *     other hand we want to fix the memory leak.  Adding this configuration property
 *     seems the most prudent way forward.
 * </p>
 */
public static final String KEY_DISABLE_AUTOFLUSH = "isis.services.container.disableAutoFlush";

11.3.4. Registering the Service

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of RepositoryService service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).

the FactoryService is often used in conjunction with the RepositoryService, to instantiate domain objects before persisting.

12. Persistence Layer SPI

The persistence layer SPIs influence how the framework persists domain objects, for example controlling how to create an audit log of changes to domain objects.

The table below summarizes the persistence layer SPIs defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 13. Persistence Layer SPI
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.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.publish
PublishingService

Publish any action invocations and changed objects, typically for interchange with an external system in a different bounded context.

PublishingServiceMq
o.ia.m.publishmq
isis-module-publishmq,
also
PublishingService
o.ia.m.publishing
isis-module-publishing

related services: PublishingService- ``Contributions``, `PublishingService-` Repository.
depends on:
EventSerializer

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

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

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

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

12.1.3. Usage

The typical way to indicate that an object should be audited is to annotate it with the @DomainObject#auditing() annotation.

12.1.4. Registering the Services

The (non-ASF) Isis addons' audit module provides an implementation of this service (AuditingService), and also provides a number of related domain services (AuditingServiceMenu, AuditingServiceRepository and AuditingServiceContributions).

Assuming that an AppManifest is being used to bootstrap the app) then this can be activated by updating the pom.xml and updating the AppManifest#getModules() method.

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.

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

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

12.2.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:

12.2.3. Registering the Services

There is no default implementation of this service provided by the core Apache Isis framework.

The (non-ASF) Isis addons' publishing module provides an implementation of this service (RestfulObjectsSpecEventSerializer) that serializes action invocations and published objects into a format based on the Restful Objects specification. It also (as you might imagine) provides an implementation of the PublishingService.

Assuming that an AppManifest is being used to bootstrap the app) then this can be activated by updating the pom.xml and updating the AppManifest#getModules() method.

This service is intended (though not mandated) to be used by implementations of PublishingService. The (non-ASF) Isis addons' publishing module does use it (though the (non-ASF) Isis addons' publishmq module does not).

12.3. PublishingService

The PublishingService is intended for coarse-grained publish/subscribe for system-to-system interactions, from Apache 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.

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

12.3.2. Implementation

The framework provides no default implementations of this service. There are however two implementations available in the (non-ASF) Isis Addons.

isis-module-publishing

The (non-ASF) Isis addons' publishing module provides an implementation (org.isisaddons.module.publishing.dom.PublishingService) 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".

isis-module-publishmq

The (non-ASF) Isis addons' publishmq module provides an implementation (org.isisaddons.module.publismq.dom.servicespi.PublishingServiceUsingActiveMq) that publishes each action invocation as an event on an ActiveMQ message queue. These are converted into a canonical XML form (the ActionInvocationMemento schema) using the ActionInvocationMementoDtoUtils class; the idea being that subscribers on the ActiveMQ message queue can then query back for further information, for example using the RestfulObjects viewer.

At the time of writing this implementation does not publish changed object events.

12.3.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 Apache 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);
    }
}

12.3.4. Registering the Services

There is no default implementation of this service provided by the core Apache Isis framework.

Both the (non-ASF) Isis addons' publishing module and the publishmq module provide implementations of this service. Assuming that an AppManifest is being used to bootstrap the app) then this can be activated by updating the pom.xml and updating the AppManifest#getModules() method.

The modules also provide services that contribute to the UI. If contributions are not required in the UI, these can be suppressed either using security or by implementing a vetoing subscriber.

The PublishingService is intended for coarse-grained publish/subscribe for system-to-system interactions, from Apache 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 Apache 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.

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

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

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

12.4.2. Implementation

The core Apache 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.

12.4.3. Registering the Services

There is no default implementation of this service provided by the core Apache Isis framework.

If using the (non-ASF) Isis addons' security module) for authentication and authorization, then note that it provides an adapter class, SecurityModuleAppUserRegistrationServiceAbstract, that provides most of the implementation. You are still required to implement a subclass and register.

For example:

@DomainService(nature=NatureOfService.DOMAIN)
public class AppUserRegistrationService extends SecurityModuleAppUserRegistrationServiceAbstract {
    protected ApplicationRole getInitialRole() {
    return findRole("regular-user");
    }
    protected Set<ApplicationRole> getAdditionalInitialRoles() {
        return Collections.singleton(findRole("self-registered-user"));
    }
    private ApplicationRole findRole(final String roleName) {
        return applicationRoles.findRoleByName(roleName);
    }
    @Inject
    private ApplicationRoles applicationRoles;
}

The most common use case is to allow users to sign-up through Apache 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.

13. Bootstrapping SPI

Bootstrapping SPIs influence how the framework locates the components that make up the running application.

The table below summarizes the bootstrapping SPI defined by Apache Isis. It also lists their corresponding implementation, either a default implementation provided by Apache Isis itself, or provided by one of the in (non-ASF) Isis Addons modules.

Table 14. Bootstrapping SPI
SPI Description Implementation Notes

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

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

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

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

13.1.3. Usage

The usage will vary depending upon the conventions of the design. As of 1.9.0, the usage of the service has been centralized such that the packages to be scanned are located from the AppManifest's #getModules() method.

For example, the SimpleApp archetype's app manifest includes:

public class DomainAppAppManifest implements AppManifest {
    @Override
    public List<Class<?>> getModules() {
        return Arrays.asList(
                DomainAppDomainModule.class,  // domain (entities and repositories)
                DomainAppFixtureModule.class, // fixtures
                DomainAppAppModule.class      // home page service etc
        );
    }
    ...
}

where the three module classes in effect define three different package prefixes to search under (for domain services, fixture scripts and persistent entities).

Other usages of the ClassDiscoveryService are likely to work in a similar way, requiring some sort of scope to be specified.

13.1.4. Registering the Services

Assuming that the configuration-and-annotation services installer is configured (implicit if using the AppManifest to bootstrap the app) then Apache Isis' core implementation of ClassDiscoveryService2 service is automatically registered and injected (it is annotated with @DomainService) so no further configuration is required.

To use an alternative implementation, use @DomainServiceLayout#menuOrder() (as explained in the introduction to this guide).


Copyright © 2010~2016 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.