-
Notifications
You must be signed in to change notification settings - Fork 3
Interceptors
#Interceptors
Interceptors provide a way to hook into various portions of the request/response pipeline. Interceptors are a kind of Dependency Injection, that seperates the concerns of your application logic from global features such as compression, authentication and authorization. When building applications, there is often a need to allow the application not be coupled with the concerns of the request/response.
XQuerrail2 comes with 3 basic implementations of interceptors that can be adapted to implement your own custom interceptor logic. To understand how interceptors work, you will need to understand the request/response pipeline and where the framework provides the pointcuts for the interceptors to operate. There are 4 primary interception points that are expressed in the XQuerrail request/response pipeline. Each interception point implements a contract that will vary depending on which part of the pipeline the interceptor hooks into.
###Interceptor Scopes
The following the define the various scopes in Request/Response pipeline and the input/output returned from each point.
| Pattern | Input | Output | Description |
|---|---|---|---|
| **before-request** | (empty) | map:map | Is called before the request is parsed and any request parameters are parsed. This is a useful injection point to define global defaults into your request. |
| **after-request** | map:map(request) | map:map(request) | Is called after the request is parsed and before executing a controller action |
| **before-response** | request:request | response as item()* | Is called after an action is called on the target controller and before the response is rendered by a rendering engine. This allows access to request map and can be used to modify it before rendering to an engine. |
| **after-response** | request:request(map:map) | response(item()*) | Is called after a response is generated by the rendering engine and returned to the calling application. This is useful wh:en implementing compression or adding additional response output such as logging and profile data. |
##Configuring Interceptors
Interceptors are configured in the /_config/config.xml file using the <interceptor> configuration element. By convention all interceptors are located in the /_framework/interceptors/ directory. Since they are global to all applications. To configure an interceptor you will define the name and set various attributes to determine which points the interceptor will interact with. It is important to note that the order in which you define the interceptor will the order in which it is executed.
The following example shows the configuration for the ml-security and compressor interceptors. Each interceptor also can specify a configuration that configures provides the context in which the interceptor will execute based on the routing pattern.
<interceptors config="/_config/interceptor.xml">
<interceptor
name="ml-security"
resource="/_config/ml-security.xml"
before-request="false"
after-request="true"
before-response="false"
after-response="false"
/>
<interceptor
name="compressor"
resource="/_config/compressor.xml"
before-request="false"
after-request="false"
before-response="false"
after-response="true"
/>
</interceptors>
| Property | Description |
|---|---|
| **@name** | The name of the interceptor file. XQuerrail will use the same name to find the interceptor module in the `/_framework/interceptor/` directory |
| **@resource** | Determines the configuration resource used to configure the interceptor. Typically you will want to use the global `/_config/` directory. |
| **@before-request** | When **"true"**, determines that the interceptor will be called **before** the request. |
| **@after-request** | When **"true"**, determines that the interceptor will be called **after** the request. |
| **@before-response** | When **"true"**, determines that the interceptor will be called **before** the response. |
| **@before-response** | When **"true"**, determines that the interceptor will be called **after** the response. |
###Configuring Interceptor Scope.
Interceptors can be configured to only act upon certain scoped requests such as only for specific application, controllers,actions or formats. This is done by defining in your interceptor configuration which scoped uri. A scope is defined exactly the same as the routing configuration `{application}:{controller}:{action}:{format}. The difference is that you can use wildcard(*) to control a group of the scope.
The following outlines the configuration for the ml-security interceptor in /_config/ml-security.xml. The ml-security interceptor defines a <login url/> that is only specific to the interceptor. The only requirement for the interceptor configuration is you define a <scope context="{scope}"> element with the @context attribute the request to match against the given URI. Inside the <scope> element you can define any elements you would like necessary to configure the scope. The interceptor router will pass the whole configuration to the interceptor and provide a function to get the matching scope. It is up to the interceptor to determine what scope it uses. This is typically done by calling the interceptor:get-matching-scope() function.
<config xmlns="http://xquerrail.com/config">
<login-url url="/login.html"/>
<anonymous-user value="anonymous"/>
<scope context="demo:default:login">
<allow-role>anonymous</allow-role>
</scope>
<scope context="demo:default:logout">
<deny-role>anonymous</deny-role>
</scope>
<scope context="demo:default:*">
<allow-role>*</allow-role>
</scope>
<scope context="*">
<deny-role>anonymous</deny-role>
</scope>
</config>In the configuration the defined scopes point to various uri contexts and either return a <allow-role> or <deny-role> element. The first scope scope context="demo:default:login"> allows the anonymous user access to login, but denies access to any currently loggedin user. The second scope <scope context="demo:logout:*">... allows any authenticated user to logout, but not to the anonymous role. And finally, the last scope <scope context="*"> <deny-role>anonymous</deny-role> </scope> denies access to all other scopes by using the **"*" scope which means everything.
- It is important to note that the order of your scopes are important as the
interceptor:get-matching-scopefunction will return the scopes in the order they are defined.
###Implementing a custom interceptor
Implementing a custom interceptor is done by creating a library module in the /_framework/interceptor/ directory using the following naming convention : interceptor.{name}.xqy. Once the module is created you will need to implement the interceptor pattern functions required by your custom interceptor. The following is the basic outline of implementing a custom logger interceptor:
(:/_framework/interceptors/interceptor.logger.xqy:)
xquery version "1.0-ml";
(:Must define the interceptor module in the interceptor namespace:)
module namespace custom = "http://xquerrail.com/interceptor";
import module namespace interceptor = "http://xquerrail.com/interceptor" at "/_framework/interceptor.xqy";
import module namespace request = "http://xquerrail.com/request" at "/_framework/request.xqy";
import module namespace config = "http://xquerrail.com/config" at "/_framework/config.xqy";
(:Implement the name function:)
declare function custom:name()
{
xs:QName("custom:custom")
};
(:Return the QName of all interceptor functions you implement:)
declare function custom:implements() {(
xs:QName("interceptor:before-request"),
xs:QName("interceptor:after-request"),
xs:QName("interceptor:before-response"),
xs:QName("interceptor:after-response")
)};
(:Define the functions that implement the custom interceptor:)
(:Implements the before-request function that logs out and returns an empty request:map:)
declare function custom:before-request() as map:map {
xdmp:log("interceptor:Before-Request"),
request:request() (:Return an empty request(map:map):)
};
(:Implements the after-request function and returns the request(map:map):)
declare function custom:after-request(
$request,
$response,
$configuration as element()
) as map:map {
let $init := request:initialize($request)
let $context := interceptor:get-context()
let $scope := interceptor:get-matching-scopes($configuration)[1]
let $level := ($scope/config:level,$configuration/config:level)[1]
return (
xdmp:log("After Request",$level),
$request
)
};
(:Executes the after request which accepts a request(map:map) and returns :)
declare function custom:before-response(
$request,
$response,
$configuration as element()
) {
request:initialize($request),
let $context := interceptor:get-context()
let $scope := interceptor:get-matching-scopes($configuration)[1]
let $level := ($scope/config:level,$configuration/config:level)[1]
xdmp:log("Before Response"),$response
};
(:Executes the after request which accepts a request(map:map) and returns :)
declare function custom:after-response(
$request,
$response,
$configuration as element()
) as item()* {
let $context := interceptor:get-context()
let $scope := interceptor:get-matching-scopes($configuration)[1]
let $level := ($scope/config:level,$configuration/config:level)[1]
return (
xdmp:log("After Response",$level),$response
)
};
In your /_framework/config.xml you will define your <interceptor> configuration element and define the configuration xml using the @resource attribute.
<interceptor
name="logger"
resource="/_config/logger.xml"
before-request="true"
after-request="true"
before-response="true"
after-response="true"
/>
In your /_config/logger.xml file you will define the scopes for which your interceptor will operate. For example we may want to selectively only log on any controller's index function, whose format is xml. You can do this by following the sample configuration as follows:
<config xmlns="http://xquerrail.com/config">
<default-level>info</default-level>
<!--Only log on index function whose is xml -->
<scope context="*:*:index:xml">
<level>info</level>
</scope>
</config>
You will also note that our custom logger interceptor defines a configuration <default-level/> and within each scope defines a override scope element called <level/>. This allows the developer to define custom parameters that can be used to pass additional information to be used by your custom. interceptor.