Using Resolvers in Induction - Part 2
This tutorial dicusses resolvers in Induction from the perspective of a programmer who needs to write her own resolver. This tutorial covers the substantially enhanced resolver structure introduced in Induction 1.5.0b.
The mechanics of URL resolution
Let's start by looking at how Induction uses the controller and view resolver to resolve an incoming HTTP request. The HTTP request is first sent to the controller resolver, if the controller resolver returns a resolution the respective controller is activated. If the controller resolver determines that the HTTP request does not resolve to a controller the resolve() method is expected to return null). If the HTTP request does not resolve to a controller the HTTP request is sent to the view resolver. If the view resolver returns a resolution the respective view is activated, otherwise an error is returned to the client indicating that the URL did not resolve to either a controller or view.
Induction has an interface for each type of resolver. At startup Induction requires a concrete implementation for each type of resolver. By default Induction loads a powerful, built-in resolver for each type of resolver, the built-in resolvers are discussed in Using Resolvers in Induction - Part 1.
Let's take a closer look now at the structure of the controller and view resolvers by examining their interface definitions. The controller and view resolvers are quite similar in structure. Following is the interface (abbreviated for clarity) of controller resolver.
package com.acciente.induction.resolver;
public interface ControllerResolver
{
}
The first thing we notice about the ControllerResolver interface is that it does not enforce any methods!! As of Induction 1.5.0b this interface is now serves only as marker since controller resolvers now support variable method parameters (denoted by (...) below). An implementation of the ControllerResolver interface implementation is expected to implement the methods described below:
- public Resolution resolveRequest(...) this method is called to resolve a controller from an HTTP request. The method is expected to return a Resolution object describing the controller to be invoked, or null if the HTTP request did not resolve to a controller.
- public Resolution resolveThrowable(...) this method is called to resolve an error handler controller when a controller throws and exception.. The method is expected to return a Resolution object describing the controller to be invoked, or null if the exception did not resolve to a controller.
Induction looks for the above method names at runtime, the methods cannot be defined in the interface since they can have variable parameters. For any parameter declared Induction attempts to inject a value for the parameter based on the type (exactly like the dependency injection Induction does elsewhere). The complete list of parameter type supported are documented here:
The Resolution class used as the return value above is an inner class in the ControllerResolver interface. Show below is the complete ControllerResolver interface with the static inner class Resolution:
package com.acciente.induction.resolver;
public interface ControllerResolver
{
/**
* A container object for the resolution information.
*/
public static class Resolution
{
private String _sClassName;
private String _sMethodName;
private boolean _bIsIgnoreMethodNameCase;
private Map _oOptions;
/**
* Creates a resolution object.
*
* @param sClassName the fully qualified name of the controller class
* @param sMethodName the name of the method to invoke in the controller class
*/
public Resolution( String sClassName, String sMethodName )
{
this( sClassName, sMethodName, false, null );
}
/**
* Creates a resolution object.
*
* @param sClassName the fully qualified name of the controller class
* @param sMethodName the name of the method to invoke in the controller class
* @param bIsIgnoreMethodNameCase tells Induction to ignore case when attempting to find a match for the
* method name in this resolution.
*/
public Resolution( String sClassName, String sMethodName, boolean bIsIgnoreMethodNameCase )
{
this( sClassName, sMethodName, bIsIgnoreMethodNameCase, null );
}
/**
* Creates a resolution object.
*
* @param sClassName the fully qualified name of the controller class
* @param sMethodName the name of the method to invoke in the controller class
* @param bIsIgnoreMethodNameCase tells Induction to ignore case when attempting to find a match for the
* method name in this resolution.
* @param oOptions is an optional map (may be null) containing data that the resolver wishes to store as part
* of the resolution. The controller's handler can access this data by choosing have the resolution object
* injected. This options maps is useful if the resolver is used to map a wide range of requests to a small
* number of controllers whose behaviour is parameterized by the options map.
*/
public Resolution( String sClassName, String sMethodName, boolean bIsIgnoreMethodNameCase, Map oOptions )
{
if ( sClassName == null )
{
throw new IllegalArgumentException( "Controller resolution must define a class name" );
}
_sClassName = sClassName;
_sMethodName = sMethodName;
_bIsIgnoreMethodNameCase = bIsIgnoreMethodNameCase;
_oOptions = oOptions;
}
public String getClassName()
{
return _sClassName;
}
public String getMethodName()
{
return _sMethodName;
}
public boolean isIgnoreMethodNameCase()
{
return _bIsIgnoreMethodNameCase;
}
public Map getOptions()
{
return _oOptions;
}
}
}
The controller resolution object contains the fully qualified class name of the controller to be activated, the method name of within the controller class and if the case of the method name is significant when finding the method. The resolution also permits a a Map containing "options". The purpose of this map is to allow the controller resolver to pass arbitrary information to a controller. A controller method gets access to this information by declaring a formal parameter of type Controller.Resolution in its parameter list. One use of the options map is to allow a controller resolver to parameterize the behaviour of a controller by using information in the HTTP request.
The interface for the view resolver is very similar to the ControllerResolver. The main difference is in the Resolution object returned by a view resolver. The view resolver's resolution does not contain a method name. This is because of the different activation mechanisms used by Induction for controllers and views. A controller has a single instance (similar to a servlet) to which all requests are dispatched (controller methods are therefore expected to be thread-safe). In contrast, for a view a new view instance is created for each view activation (the reason for this is that a view instance is the container for any all data used to render the view). The complete code for the ViewResolver is shown below.
package com.acciente.induction.resolver;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
public interface ViewResolver
{
/**
* A container object containg the resolution information.
*/
public static class Resolution
{
private String _sClassName;
private Map _oOptions;
/**
* Creates a resolution object.
*
* @param sClassName the fully qualified name of the view class
*/
public Resolution( String sClassName )
{
this( sClassName, null );
}
/**
* Creates a resolution object.
*
* @param sClassName the fully qualified name of the view class
* method name in this resolution.
* @param oOptions is an optional map (may be null) containing data that the resolver wishes to store as part
* of the resolution. The view's handler can access this data by choosing have the resolution object
* injected. This options maps is useful if the resolver is used to map a wide range of requests to a small
* number of views whose behaviour is parameterized by the options map.
*/
public Resolution( String sClassName, Map oOptions )
{
if ( sClassName == null )
{
throw new IllegalArgumentException( "View resolution must define a class name" );
}
_sClassName = sClassName;
_oOptions = oOptions;
}
public String getClassName()
{
return _sClassName;
}
public Map getOptions()
{
return _oOptions;
}
}
}
A Simple Example
Shown below is simple example to illustrate how to write a simple custom view resolver. This example also illustrates how the a resolver can leverage the built-in short URL resolvers and build on there capabilities.
The resolver below directs to the view demoapp.resolvers_app.SiteHomePage if no path is specified in the HTTP request or if the path is simply /.
package demoapp.resolvers_app;
import com.acciente.induction.init.config.Config;
import com.acciente.induction.resolver.ShortURLViewResolver;
import com.acciente.induction.resolver.ViewResolver;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class MyCustomViewResolver implements ViewResolver
{
private ShortURLViewResolver _oShortURLViewResolver;
public MyCustomViewResolver( Config.ViewMapping oViewMapping, ClassLoader oClassLoader ) throws IOException
{
_oShortURLViewResolver = new ShortURLViewResolver( oViewMapping, oClassLoader );
}
public Resolution resolveRequest( HttpServletRequest oRequest )
{
// if the request only specifies a hostname or just contains the root path, then
// resolve to our custom home page
if ( oRequest.getPathInfo() == null || oRequest.getPathInfo().equals( "/" ) )
{
return new Resolution( "demoapp.resolvers_app.SiteHomePage" );
}
// otherwise just delegate to the standard short url resolver
return _oShortURLViewResolver.resolveRequest( oRequest );
}
}
To direct Induction to use the above resolver the following lines are specified in the induction-<your-servlet-name-here>.xml
<config>
... other entries ....
<view-resolver>
<class>demoapp.resolvers_app.MyCustomViewResolver</class>
</view-resolver>
<config>