Action Interception
When Wisdom processes a request, it searches for the route to invoke and, if found, invokes it. However, the invocation can be intercepted, letting filters and interceptors do stuff before and/or after the actual invocation.
Filters vs. Interceptors
Wisdom proposes two types of interceptors:
-
Filters select the request to intercept using urls
-
Interceptors are configured by the controllers
Both filters and interceptors are services. Filters are services independent from the running application. They intercept all requests matching a regex (provided by the filter). On the other hand, interceptors are configured by the controllers. The set of interceptors called for an action is specified by the action itself or by the controller using the annotations bound to interceptors. When several interceptors are declared, a chain is computed and is called sequentially:
Both filters and interceptors are enqueued in the interception chain. Filters are executed first, and then interceptors. When several filters match the incoming requests, the order is determined using their priority (highest first).
Filters can intercept unbound requests, i.e. requests that do not have a matching action method, or all actions having thrown an exception. Such interceptions are not feasible using interceptors.
Creating a filters
A filter is a component implementing org.wisdom.api.interception.Filter and exposing it as a service. Here is an
example of a filter measuring the time spent to compute the response to a request:
include::{sourcedir}/interceptors/TimeFilter.java[tags=controller]
The call method is the method intercepting the request. It should, in the very high majority of the cases, call the
proceed method to continue the chain. If it does not, the chain is cut, and no other filters or
interceptors
will be called.
The uri method returns the regex selecting the intercepted urls. In the example, all requests
pointing to
"/documentation" are intercepted.
The priority method allows configuring the position of the filters in the chain if there are several matching
filters. Filters with the highest priorities are first. Notice that the default error page filter
from Wisdom (displaying
the error pages and route not found pages) has a priority of 1000.
Filters can come and go at anytime, promoting a very dynamic model. So, you can intercept requests targeting controllers not developed by you (such as the asset controller), or deploy a temporary filter to understand a specific issue.
Creating an interceptor’s annotation
Developing an interceptor requires an annotation and the interceptor itself. Interceptors are configured using an
annotation bound to the interceptor. This annotation uses the @Interception meta-annotation specifying that an
interceptor is consuming the annotation:
include::{sourcedir}/interceptors/Logged.java[tags=controller]
The previous snippet shows an interceptor annotation that will be handled by an interceptor. The annotation must be visible at runtime, and can target both types and methods (this means they can be used either on classes or on methods).
|
Note
|
when used on classes, the interceptor is applied to all contained actions. |
|
Important
|
The package containing the annotation must be exported to be usable by Wisdom components not included in the current project. |
Implementing the interceptor
Interceptors are Wisdom components registered as OSGi services. Don’t worry, Wisdom makes that easy:
include::{sourcedir}/interceptors/LoggerInterceptor.java[tags=controller]
First, the class is annotated with three annotations:
@Component
@Provides
@Instantiate
This instructs the framework to expose the OSGi service.
Then, the class contains two main methods:
-
callintercepting the action invocation -
annotationreturning the class of the handled annotation
The annotation method is straightforward. It just returns the class of the handled annotation.
The call method is intercepting the action invocation. It invokes the context.proceed() method to call the next
interceptor. It can also return a Result object immediately, shortcutting the chain. The call method receives the
RequestContext object and the actual interceptor configuration. The RequestContext let the interceptor to:
-
get the current HTTP Context with the
context()method -
get the current HTTP Request with the
request()method -
get the current route with the
route()method -
set data consumed by other interceptor and filters using the
data()method
Annotating controllers
As said, interceptor annotation can target either classes or methods:
include::{sourcedir}/interceptors/MyController.java[tags=class]
include::{sourcedir}/interceptors/MyController.java[tags=method]
Sharing data between filters and interceptor of a chain
Filters and interceptors can share data when they are on the same chain (i.e. intercepting the same request). The
RequestContext object hold the shared data:
context.data().put("my-data", "hello world");
This data can be consumed by the filters and interceptors invoked after the entity having set the data.