Tuesday, August 5, 2014

JSF webapps fronted with reverse proxy



JSF web applications can be fronted by a reverse proxy just like any other applications, except when the reverse proxy has a different context path compared to the application server that this JSF webapp is deployed to.

Look at the following HTML content of a JSF Forms application. The 'action' URL of the form begins with a forward-slash '/'. This URL is actually a absolute url without the hostname. So, this is not a relative URL. This is a problem if your reverse proxy has a different context. Since the JSF webapp is not aware of the context path of the reverse proxy, the JSF form action will fail.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <h2>Welcome to Marks Calculator</h2>
  <form id="j_id_4" name="j_id_4" method="post" 
        action="/jsf-custom-viewhandler-marks/index.jsf" 
        enctype="application/x-www-form-urlencoded">
    <table cellspacing="10">
      <tbody>
        <tr>
          <td>Subject 1 marks:</td>
          <td>
            <input id="j_id_4:j_id_8" name="j_id_4:j_id_8" type="text" value="2" />
          </td>
        </tr>
        <tr>
          <td>Subject 2 marks:</td>
          <td>
            <input id="j_id_4:j_id_b" name="j_id_4:j_id_b" type="text" value="4" />
          </td>
        </tr>
      </tbody>
    </table>
    <input id="j_id_4:j_id_c" name="j_id_4:j_id_c" type="submit" value="Calculate" />
    <input type="hidden" name="j_id_4_SUBMIT" value="1" />
    <input type="hidden" name="javax.faces.ViewState" id="javax.faces.ViewState" value="wpq2AMaIFASZtUJ+IghBD2X2mhXm5MAFTDfdI20attDvNPOF " />
  </form>
</html>

Now, let's look at how the Apache HTTPD configuration might look like. Do not add forward-slash at the end of ProxyPass/ProxyPassReverse since the Location, '/as', do not have a forward-slash at the end either.


<Location /as>
    Order Deny,Allow
    Deny from none
    Allow from all
    ProxyPass http://localhost:9763 retry=0 timeout=5
    ProxyPassReverse http://localhost:9763
</Location>

Apache is configured to forward requests that come to http://localhost/as/ to http://localhost:9763/.

Now, you can access the webapp by using the request URL - http://localhost/as/jsf-custom-viewhandler-marks/index.jsf. Now, because of the JSF form 'action' URL, the form submit will try to call http://localhost/jsf-custom-viewhandler-marks/index.jsf which is non-existent.

Now, I hope you understand what's the problem here. In my view, this is a limitation of JSF. So, let's look at how to fix this.

 

Implementing A ViewHandler

To fix this, you need to implement javax.faces.application.ViewHandler, and register it to your webapp. In this custom view handler, we will set the action url as a relative url rather than a absolute url without the host name.

Needed dependencies -
commons-lang 2.6
commons-logging 1.1

package org.wso2.as;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.faces.application.ViewHandler;
import javax.faces.application.ViewHandlerWrapper;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;

public class CustomViewHandler extends ViewHandlerWrapper {
  private static final Log log = LogFactory.getLog(CustomViewHandler.class);

  private ViewHandler wrappped;

  public CustomViewHandler(ViewHandler wrappped) {
    super();
    this.wrappped = wrappped;

  }

  @Override
  public ViewHandler getWrapped() {
    return wrappped;
  }

  @Override
  public String getActionURL(FacesContext context, String viewId) {
    String url =  super.getActionURL(context, viewId);
    log.debug("The getActionURL: " + url);
    return addContextPath(context, url);
  }

  @Override
  public String getRedirectURL(FacesContext context, String viewId, Map<String,
          List<String>> parameters, boolean includeViewParams) {
    String url =  super.getRedirectURL(context, viewId, parameters, includeViewParams);
    log.debug("The getRedirectURL: " + url);
    return url;
  }

  @Override
  public String getResourceURL(FacesContext context, String path) {
    String url = super.getResourceURL(context, path);
    log.debug("The getResourceURL: = " + url);
    return addContextPath(context, url);
  }

  private String addContextPath(FacesContext context, String url) {
      final HttpServletRequest request = ((HttpServletRequest) 
               context.getExternalContext().getRequest());
      String result = url;
      if (url.startsWith("/")) {
          int subpath = StringUtils.countMatches(getPath(request), "/") - 1;
          String pathPrefix = "";
          if (subpath > 0) {
              while (subpath > 0) {
                  pathPrefix += "/..";
                  subpath--;
              }
              pathPrefix = StringUtils.removeStart(pathPrefix, "/");
          }
          result = pathPrefix + result;
      }
      return result;
  }

    private String getPath(final HttpServletRequest request) {
        try {
            return StringUtils.replace(new URI(request.getRequestURI()).getPath(), "//", "/");
        } catch (final URISyntaxException e) {
            return StringUtils.EMPTY;
        }
    }

}

In the addContextPath private method, we modify the original url, and replace it with a relative url.

Then, build this class into a jar and pack it into WEB-INF/lib of your web application. You can find the source and a Apache Maven build file at the end of this article.

Now, you need to register this ViewHandler with your web application. For that, open your faces-config.xml and add your view handler as follows. The faces-config.xml is usually placed under WEB-INF/ folder in the webapp.

<application>
  <view-handler>my.package.CustomViewHandler</view-handler>
</application>

Now, the faces-config.xml might look as follows.

<?xml version='1.0' encoding='UTF-8'?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
              version="2.0">

    <application>
      <view-handler>CustomViewHandler</view-handler>
    </application>

    <navigation-rule>
        <from-view-id>/index.xhtml</from-view-id>
        <navigation-case>
            <to-view-id>/results.xhtml</to-view-id>
            <from-outcome>success</from-outcome>
        </navigation-case>
    </navigation-rule>

</faces-config>

 

Setting the sessionCookiePath

Some JSF webapps depends on sessions and JSESSIONID cookie. Usually, JSF sets this cookie to the webapp path. But if the reverse proxy has a different context, then the cookie fails to get stored in the browser properly. In my case, I use EJB with the JSF webapp, and I needed to fix this issue. Otherwise, I faced the below exception.

To fix this, you need to add context.xml to your webapp, and set the sessionCookiePath attribute. We can configure the JSESSIONID cookie path via this configuration file. In our case, we can set it as follows. See the tomcat documentation for more details.

<Context sessionCookiePath="/as/jsf-custom-viewhandler-marks">
</Context>

There, the path has taken into account the reverse proxy configuration path "/as".

Now you are all set. You can use the given Custom View Handler sample code in your own JSF webapps. I have also posted a sample JSF webapp that uses EJB. The sample app can be deployed Apache TomEE.

You can find a sample JSF Custom View Handler code here -

https://github.com/wso2as-developer/javaee-samples/tree/master/JSF/jsf-custom-view-handler

You can find the sample webapp here -

https://github.com/wso2as-developer/javaee-samples/tree/master/JSF/jsf-webapp-w-custom-view-handler





No comments:

Post a Comment