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 one mechanism to ensure that any business constraints defined by the 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 @Parameter(maxLength=…​) or @Property(mustSatisfy=…​).

This capability is frequently used within integration tests, but can also be used in production code.

API

The API breaks into four parts, along with a number of supporting control classes.

Synchronous API

The synchronous API provided by the service is:

public interface WrapperFactory {

    <T> T wrap(T domainObject,                                      (1)
               SyncControl syncControl);

    <T> T wrap(T domainObject);                                     (2)

    <T> T wrapMixin(Class<T> mixinClass, Object mixedIn,            (3)
                    SyncControl syncControl);

    <T> T wrapMixin(Class<T> mixinClass, Object mixedIn);           (4)

    // ...

}
1 wraps the underlying domain object so it can invoked (through its wrapper) synchronously in the same thread.

The SyncControl modifies the way in which the wrapper interacts with the underlying domain object, discussed below.

2 overload that wraps the underlying domain object with a wrapper that will validate all business rules and then execute if they pass.
3 instantiates and wraps a mixin so that it can be invoked (through its wrapper) synchronously in the same thread. Again SyncControl modifies the way in which the wrapper interacts with the underlying domain object, discussed below..
4 overload that instantiates and wraps a mixin with a wrapper that will validate all business rules and then execute the action if they pass.

The service works by returning a "wrapper" around a supplied domain object (using byte buddy), and it is this wrapper that ensures that the hide/disable/validate rules implies by the Apache Isis programming model are enforced.

Domain Objects

For domain objects (not mixins), 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 @ActionLayout#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.

If the interface is performed (action invoked or property set), then - irrespective of whether the business rules were checked or skipped - a command will be created and pre- and post-execute domain events) will be fired.

Mixins

For mixins, the behaviour of the wrapper is similar but simpler. Mixin wrappers only apply to actions, and so the wrapper will enforce the hidden/disable/validate rules before executing. In addition, any default…​(), choices…​() or autoComplete…​() methods can be called.

SyncControl

The SyncControl class controls the way that a (synchronous) wrapper works. Its superclass ControlAbstract is:

ControlAbstract.java
public class ControlAbstract<T extends ControlAbstract<T>> {

    private boolean checkRules = true;                          (1)
    public T withCheckRules() {
        // ...
    }
    public T withSkipRules() {
        // ...
    }

    @Getter @NonNull
    private ExceptionHandler exceptionHandler;                  (2)
    public T with(ExceptionHandler exceptionHandler) {
        // ...
    }

}
1 whether to check business rules (hide/disable/validate) before executing the underlying property or action
2 how to handle exceptions if they occur, using ExceptionHandler:
SyncControl.java
@FunctionalInterface
public interface ExceptionHandler {

    Object handle(Exception ex) throws Exception;

}

Exceptions can be rethrown or ignored. If ignored, the handler can instead return a value; this must be compatible with the expected return value of the underlying action.

... and the SyncControl class itself is:

public class SyncControl extends ControlAbstract<SyncControl> {

    public static SyncControl control() {
        return new SyncControl();
    }

    private SyncControl() {
        with(exception -> {                 (1)
            throw exception;
        });
    }

    private boolean execute = true;         (2)
    public SyncControl withExecute() {
        // ...
    }
    public SyncControl withNoExecute() {
        // ...
    }


}
1 Default exception handler is to rethrow the exception.
2 Don’t actually execute the action (for example, just check the rules as in a "dry-run").

Asynchronous API

The WrapperFactory also allows domain objects to be interacted with in an asynchronous fashion, in other words executed in a separate thread:

public interface WrapperFactory {

    <T,R> T asyncWrap(T domainObject,                      (1)
                      AsyncControl<R> asyncControl);

    <T,R> T asyncWrapMixin(                                (2)
                   Class<T> mixinClass, Object mixedIn,
                   AsyncControl<R> asyncControl);

    // ...

}
1 Wraps the underlying domain object so it can be invoked (through its wrapper) asynchronously in a separate thread. The AsyncControl modifies the way in which the wrapper interacts with the underlying domain object, discussed below.
2 instantiates and wraps a mixin so that it can be invoked (through its wrapper) asynchronously in a separate thread. Again AsyncControl modifies the way this is done.

Executing in a separate thread means that the target and arguments are used in a new session. If any of these are entities, they are retrieved from the database afresh; it isn’t possible to pass domain entity references from the foreground calling thread to the background threads.

AsyncControl

The AsyncControl class controls the way that an asynchronous wrapper — as obtained by asyncWrapXxx(…​) — works. The class inherits from ControlAbstract (see above), to allow the rules checking to be skipped and to configure the ExceptionHandler. It extends with the following behaviour:

AsyncControl.java
@Log4j2
public class AsyncControl<R> extends ControlAbstract<AsyncControl<R>> {

    public static AsyncControl<Void> control() {                        (1)
        return new AsyncControl<>();
    }
    public static <X> AsyncControl<X> control(final Class<X> clazz) {   (2)
        return new AsyncControl<>();
    }

    private AsyncControl() {
        with(exception -> {                                             (3)
            log.error(logMessage(), exception);
            return null;
        });
    }

    @Getter @NonNull
    private ExecutorService executorService =                           (4)
                            ForkJoinPool.commonPool();
    public AsyncControl<R> with(ExecutorService executorService) {
        // ...
    }

    @Getter
    private String user;                                                (5)
    public AsyncControl<R> withUser(final String user) {
        // ...
    }

    @Getter
    private List<String> roles;                                         (6)
    public AsyncControl<R> withRoles(final List<String> roles) {
        // ...
    }
    public AsyncControl<R> withRoles(String... roles) {
        // ...
    }

    @Getter
    private Future<R> future;                                           (7)

    // ...
}
1 instantiate for a void action or property edit (where there is no need or intention to provide a return value through the Future, discussed below).
2 instantiate for an action returning a value of <R> (where this value will be returned through the Future, discussed below).
3 Default exception handler is just to log the exception, though this can be overridden.
4 Default executor service is the common pool.
5 Specify the user for the session used to execute the command asynchronously, in the background. If not specified, then the user of the current foreground session is used.
6 Specify the roles of the user for the session used to execute the command asynchronously, in the background. If not specified, then the roles of the current user foreground user are used.
7 Provides access to a Future representing the result of the background command. This may or may not hold a value.

The AsyncControl can therefore be used to check business rules or to skip them. If business rules are checked, note that they are performed in the context of the foreground session.

Unwrap API

The unwrap API provided by the service is:

public interface WrapperFactory {

    <T> T unwrap(T possibleWrappedDomainObject);                    (1)

    <T> boolean isWrapper(T possibleWrappedDomainObject);           (2)

    // ...

}
1 Obtains the underlying domain object or mixin, if wrapped.

If the object is not wrapped, returns back unchanged.

2 whether the supplied object is a wrapper around a domain object or mixin.

Listener API

The listener API allows the interactions between the wrapper and the underlying domain object to be observed:

public interface WrapperFactory {

    // ...
    List<InteractionListener> getListeners();                       (1)

    boolean addInteractionListener(InteractionListener listener);   (2)

    boolean removeInteractionListener(                              (3)
                    InteractionListener listener);

    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

This API isn’t (currently) used by the framework itself.

One possible use case would be to autogenerate documentation, for example a sequence diagram from tests. Such a feature would probably also use InteractionContext, which builds up an execution call graph of interactions between (wrapped) objects.

Typical 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)
            messageService.raiseError(ex.getMessage());  (2)
            return;
        }
    }
    ...
    @Inject
    WrapperFactory wrapperFactory;
    @Inject
    MessageService messageService;
}
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.