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
orchoicesXxx
method
If the object has (see #isWrapper(Object) already been wrapped), then should just return the object back unchanged.
API
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.
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:
-
the title() and
toString()
methods -
any default…(), choices…() or autoComplete…() methods
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.