WrapperFactory

Provides the ability to 'wrap' a domain object such that it can be interacted with while enforcing the hide/disable/validate rules implied by the Apache Causeway programming model.

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=…​).

The wrapper can alternatively also be used to execute the action asynchronously, through an java.util.concurrent.ExecutorService . Any business rules will be invoked synchronously beforehand, however.

The 'wrap' is a runtime-code-generated proxy that wraps the underlying domain object. The wrapper can then be interacted with as follows:

  • a get method for properties or collections

  • a set method for properties

  • 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 as hidden then a HiddenException will be thrown. Similarly if an action has a validate method and the supplied arguments are invalid then a InvalidException will be thrown.

In addition, the following methods may also be called:

  • the title method

  • any defaultXxx or choicesXxx method

If the object has (see #isWrapper(Object) already been wrapped), then should just return the object back unchanged.

API

WrapperFactory.java
interface WrapperFactory {
  T wrap(T domainObject, SyncControl syncControl)     (1)
  T wrap(T domainObject)     (2)
  T wrapMixin(Class<T> mixinClass, Object mixee, SyncControl syncControl)     (3)
  T wrapMixinT(Class<T> mixinClass, MIXEE mixee, SyncControl syncControl)     (4)
  T wrapMixin(Class<T> mixinClass, Object mixee)     (5)
  T wrapMixinT(Class<T> mixinClass, MIXEE mixee)     (6)
  T unwrap(T possibleWrappedDomainObject)     (7)
  boolean isWrapper(T possibleWrappedDomainObject)     (8)
  T asyncWrap(T domainObject, AsyncControl<R> asyncControl)     (9)
  T asyncWrapMixin(Class<T> mixinClass, Object mixee, AsyncControl<R> asyncControl)     (10)
  T asyncWrapMixinT(Class<T> mixinClass, MIXEE mixee, AsyncControl<R> asyncControl)     (11)
  List<InteractionListener> getListeners()     (12)
  boolean addInteractionListener(InteractionListener listener)     (13)
  boolean removeInteractionListener(InteractionListener listener)     (14)
  void notifyListeners(InteractionEvent ev)
  R execute(AsyncCallable<R> asyncCallable)     (15)
}
1 wrap(T, SyncControl)

Provides the "wrapper" of a domain object against which to invoke the action.

2 wrap(T)

A convenience overload for #wrap(Object, SyncControl) , returning a wrapper to invoke the action synchronously, enforcing business rules. Any exceptions will be propagated, not swallowed.

3 wrapMixin(Class, Object, SyncControl)

Provides the wrapper for a FactoryService#mixin(Class, Object) mixin , against which to invoke the action.

4 wrapMixinT(Class, MIXEE, SyncControl)

Provides the wrapper for a Mixin typesafe FactoryService#mixin(Class, Object) mixin , against which to invoke the action.

5 wrapMixin(Class, Object)

A convenience overload for #wrapMixin(Class, Object, SyncControl) , returning a wrapper to invoke the action synchronously, enforcing business rules. Any exceptions will be propagated, not swallowed.

6 wrapMixinT(Class, MIXEE)

A convenience overload for #wrapMixinT(Class, Object, SyncControl) , returning a wrapper to invoke the action synchronously, enforcing business rules. Any exceptions will be propagated, not swallowed.

7 unwrap(T)

Obtains the underlying domain object, if wrapped.

8 isWrapper(T)

Whether the supplied object is a wrapper around a domain object.

9 asyncWrap(T, AsyncControl)

Returns a proxy object for the provided domainObject , through which can execute the action asynchronously (in another thread).

10 asyncWrapMixin(Class, Object, AsyncControl)

Returns a proxy object for the provided mixinClass , through which can execute the action asynchronously (in another thread).

11 asyncWrapMixinT(Class, MIXEE, AsyncControl)

Returns a proxy object for the provided mixinClass , through which can execute the action asynchronously (in another thread).

12 getListeners()

All InteractionListener s that have been registered using #addInteractionListener(InteractionListener) .

13 addInteractionListener(InteractionListener)

Registers an InteractionListener , to be notified of interactions on all wrappers.

14 removeInteractionListener(InteractionListener)

Remove an InteractionListener , to no longer be notified of interactions on wrappers.

15 execute(AsyncCallable)

Provides a mechanism for custom implementations of java.util.concurrent.ExecutorService , as installed using AsyncControl#with(ExecutorService) , to actually execute the AsyncCallable that they are passed initially during WrapperFactory#asyncWrap(Object, AsyncControl) and its brethren.

Members

wrap(T, SyncControl)

Provides the "wrapper" of a domain object against which to invoke the action.

The provided SyncControl determines whether business rules are checked first, and conversely whether the action is executed. There are therefore three typical cases:

  • check rules, execute action

  • skip rules, execute action

  • check rules, skip action

The last logical option (skip rules, skip action) is valid but doesn’t make sense, as it’s basically a no-op.

Otherwise, will do all the validations (raise exceptions as required etc.), but doesn’t modify the model.

wrap(T)

A convenience overload for #wrap(Object, SyncControl) , returning a wrapper to invoke the action synchronously, enforcing business rules. Any exceptions will be propagated, not swallowed.

wrapMixin(Class, Object, SyncControl)

Provides the wrapper for a FactoryService#mixin(Class, Object) mixin , against which to invoke the action.

The provided SyncControl determines whether business rules are checked first, and conversely whether the action is executed. See #wrap(Object, SyncControl) for more details on this.

wrapMixinT(Class, MIXEE, SyncControl)

Provides the wrapper for a Mixin typesafe FactoryService#mixin(Class, Object) mixin , against which to invoke the action.

The provided SyncControl determines whether business rules are checked first, and conversely whether the action is executed. See #wrap(Object, SyncControl) for more details on this.

wrapMixin(Class, Object)

A convenience overload for #wrapMixin(Class, Object, SyncControl) , returning a wrapper to invoke the action synchronously, enforcing business rules. Any exceptions will be propagated, not swallowed.

wrapMixinT(Class, MIXEE)

A convenience overload for #wrapMixinT(Class, Object, SyncControl) , returning a wrapper to invoke the action synchronously, enforcing business rules. Any exceptions will be propagated, not swallowed.

unwrap(T)

Obtains the underlying domain object, if wrapped.

If the object #isWrapper(Object) is not wrapped , then should just return the object back unchanged.

isWrapper(T)

Whether the supplied object is a wrapper around a domain object.

asyncWrap(T, AsyncControl)

Returns a proxy object for the provided domainObject , through which can execute the action asynchronously (in another thread).

asyncWrapMixin(Class, Object, AsyncControl)

Returns a proxy object for the provided mixinClass , through which can execute the action asynchronously (in another thread).

asyncWrapMixinT(Class, MIXEE, AsyncControl)

Returns a proxy object for the provided mixinClass , through which can execute the action asynchronously (in another thread).

getListeners()

All InteractionListener s that have been registered using #addInteractionListener(InteractionListener) .

addInteractionListener(InteractionListener)

Registers an InteractionListener , to be notified of interactions on all wrappers.

This is retrospective: 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).

removeInteractionListener(InteractionListener)

Remove an InteractionListener , to no longer be notified of interactions on wrappers.

This is retrospective: the listener will no longer be notified of any interactions created on any wrappers, not just on those wrappers created subsequently. (From an implementation perspective this is because the wrappers delegate back to the container to fire the events).

execute(AsyncCallable)

Provides a mechanism for custom implementations of java.util.concurrent.ExecutorService , as installed using AsyncControl#with(ExecutorService) , to actually execute the AsyncCallable that they are passed initially during WrapperFactory#asyncWrap(Object, AsyncControl) and its brethren.

Implementation

The Core Runtime Services module provides a default implementation, WrapperFactoryDefault. This implementation uses byte buddy to create the "wrapper" around a supplied domain object. It is this wrapper that ensures that the hide/disable/validate rules implies by the Apache Causeway programming model are enforced.

Typical Usage: Enforcing Constraints

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.

Usage Notes

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

  • any action

with CAUSEWAY-3084, addTo…​() and removeFrom…​() methods for collections were removed; the notion (direct) collection modification was deprecated as any specific business logic should by handled via actions instead;

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.

Other Use Cases

Creating interactions

One use case for the WrapperFactory is to invoke actions that have Action#executionPublishing enabled. This will result in a serializable representation of the action invocation to be published; this could then be used to route a message to another system.

The target action is often a no-op; indeed it can even be hidden if the WrapperFactory is invoked with rules skipped.

Integration tests

The WrapperFactory is frequently used within integration tests. A longer discussion of the use of the WrapperFactory within integration tests can be found here.

Trust boundaries

If there is a (lack of) trust boundary between the caller and callee — eg if they reside in different modules — then the WrapperFactory 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.

Listener API

One possible use case for the listener API 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.