Friday, April 13, 2007

Publishing JSF application from behind a proxy

Problem

Sometimes application servers are located behind a web proxy, so that the contents that they publish is available from a general URL space. For example, the web server www.acme.es acts as a proxy for the web application server srv3.acme.es, so that every web page which is published like http://srv3.acme.es/* are accessible from http://www.acme.es/srv3/*.

The problem is that links generated inside HTML pages by JSF have always server relative paths, which start by a slash, and these paths arrive to the client verbatim. For example, if in srv3.acme.es exists an application called apl, with a page p that references another page q, an external client would make a request to www.acme.es/srv3/apl/p, that will be transformed in a request to srv3.acme.es/apl/p, and then a page that contains a link like /apl/q will be generated, and this page will be returned to the client. When the client follows this link, his browser will make a request to www.acme.es/apl/q (/srv3 has disappeared), which is a different page, that maybe does not even exist.

Solution

The URL of links generated by JSF components are build by the view handler from the view identifier. By default, the view identifier is the path of the JSP page within our web application, and the view handler builds the URL by concatenating the context path, the JSF servlet path and the view identifier (which is the path to the JSP page).

The behaviour of the view handler can be redefined, and this way, we can build the URL of links ourserlves. Next there is a step by step description about how to make this redefinition for the example showed above.

Step by step solution

A view handler is a class that extends javax.faces.application.ViewHandler. This is an abstract class with many methods, but we are only interested in redefining the method getActionURL; for the other ones, we will make a call to the default view handler. If we create a constructor in our view handler that accepts another view handler as a parameter, JSF will provide us with the default view handler in the moment that ours is created. So our view handler could be this way:

import java.io.*;
import java.util.*;
import javax.faces.*;
import javax.faces.context.*;
import javax.faces.application.*;
import javax.faces.component.*;

public class AcmeViewHandler extends ViewHandler
{
ViewHandler defaultHandler;

public AcmeViewHandler(ViewHandler defaultHandler)
{
this.defaultHandler = defaultHandler;
}

public Locale calculateLocale(FacesContext context)
{
return defaultHandler.calculateLocale(context);
}

public String calculateRenderKitId(FacesContext context)
{
return defaultHandler.calculateRenderKitId(context);
}

public UIViewRoot createView(FacesContext context, String viewId)
{
return defaultHandler.createView(context, viewId);
}

public String getActionURL(FacesContext context, String viewId)
{
String actionURL = defaultHandler.getActionURL(context, viewId);
String prefijoProxy = context.getExternalContext().getInitParameter("es.acme.faces.PROXY_PREFIX");

if (prefijoProxy == null)
return actionURL;
else
return prefijoProxy + actionURL;
}

public String getResourceURL(FacesContext context, String path)
{
return defaultHandler.getResourceURL(context, path);
}

public void renderView(FacesContext context, UIViewRoot viewToRender)
throws IOException, FacesException
{
defaultHandler.renderView(context, viewToRender);
}

public UIViewRoot restoreView(FacesContext context, String viewId)
{
return defaultHandler.restoreView(context, viewId);
}

public void writeState(FacesContext context)
throws IOException
{
defaultHandler.writeState(context);
}

}

In JSF 1.2, we can extend ViewHandlerWrapper, so we can avoid implementing every method. The path returned by the getActionURL method is composed by a prefix (in our example /srv3), which is defined by es.acme.faces.PROXY_PREFIX parameter, to which the original URL (which is relative to the application server) is appended. The value of this parameter is defined in the web.xml file:

<web-app>
...
<context-param>
<param-name>es.acme.faces.PROXY_PREFIX</param-name>
<param-value>/op</param-value>
</context-param>
...
<web-app>

Now the only step left is to state in the faces-config.xml file that our class is the one we want to use as the view handler:

<faces-config>
...
<application>
<view-handler>es.acme.faces.AcmeViewHandler</view-handler>
</application>
...
</faces-config>