Mapping FacesServlet to URLs without extensions
October 2nd, 2007 by Christos Fragoulides

Over the past few months, I’ve been working on JavaServer Faces, making an effort to learn the framework and ultimately use it to create a web application. Currently I’m still in the learning process and the more I’m involved with JSF, the more I’m becoming a fan of it. The last challenge I’ve faced is not directly related with JSF, but has to do with Servlets in general.

When you declare a Servlet in your web application’s configuration file (that’s web.xml), you also have to declare a mapping for the Servlet to make the web container forward the requests to the Servlet. There’s two types of mappings you can choose from: extension mapping and prefix mapping. Extension mapping will be of the form “*.extension” and path prefix mapping will be “/path/*”. Here’s an example:

<servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>*.faces</url-pattern>
</servlet-mapping>

I prefer extension mapping for JSF, because this way I’m not restricted to have a certain path for my JSF-enabled pages. The challenge I was talking about previously, has to do with my requirement to go beyond the default mapping mechanism. I want to be able to invoke JSF pages from any path, without using an extension. For example, say there is a JSF page displaying user account information, that has to be invoked using the URL “/account/view.faces”. Wouldn’t it be nicer to have “/account/view” instead?

After a small research, I’ve found out that a Filter can do the job. All that have to be done is a check of the requested URL pattern and if it match the mapping criteria, forward the request to the FacesServlet. Here is how I’ve done it:

public class MappingFilter extends HttpServlet implements Filter {
    ...
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpReq = (HttpServletRequest) request;
        // Get the requested URI
        String uri = httpReq.getRequestURI();
        
        try{
            // Check if the URI matches mapping creteria.
            boolean matches = uri.matches(".*/[\\w]+");
            if(matches){
                ServletContext context = filterConfig.getServletContext();
                // Strip context path from the requested URI
                String path = context.getContextPath();
                if (uri.startsWith(path)){
                    uri = uri.substring(path.length());
                }
                // Check if there is actually a file to handle the forward.
                URL url = context.getResource(uri+".jsp");
                if(url != null){
                    // Generate the forward URI
                    String forwardURI = uri + facesExtension;
                    // Get the request dispatcher
                    RequestDispatcher rd =
                        context.getRequestDispatcher(forwardURI);
                    if(rd != null){
                        // Forward the request to FacesServlet
                        rd.forward(request, response);
                        return;
                    }
                }
            }
            // We are not interested for this request, pass it to the FilterChain.
            chain.doFilter(request, response);
            
        } catch (ServletException sx) {
            filterConfig.getServletContext().log(sx.getMessage());
        } catch (IOException iox) {
            filterConfig.getServletContext().log(iox.getMessage());
        }
        
    }
    ...
}

This filter works just fine and can be used to map extension-less requests to any Servlet. In other words, you can use it to map requests for plain JSP pages also. But when it comes to JSF, there’s a problem: JSF will write action URLs to the forms according to the servlet-mapping parameter that you have set in your web.xml configuration file. So, even if you request using the extension-less URL, after submitting the form your web browser’s bar will have the original location (the one with the extension) -and that’s not nice. After making a huge research this time, struggling for hours to find something in javadocs and in search engines, I’ve managed to find the class responsible for the creation of the action URL. It’s name is ViewHandler and the method is getActionURL(). I’ve also found that you can easily make a custom ViewHandler that sits on top of the one the framework is using and change only part of it’s functionality.

All you have to do is to create a class that extends javax.faces.application.ViewHandler and specify a constructor that takes a parameter of the same type as an argument. Then you declare it in faces-config.xml like this:

<application>        
    <view-handler>gr.xfrag.faces.mapping.MappingViewHandler</view-handler>        
</application>

The JSF framework will find your declaration, and will call your implementation’s constructor, passing it the default ViewHandler used by the implementation you’re using(if you use JSF-RI it will pass com.sun.faces.application.ViewHandlerImpl). You can use this class to delegate every method you don’t want to customize from your custom ViewHandler to the default one. Using this technique, I’ve been able to strip the extension from the action URLs like this:

public class MappingViewHandler extends ViewHandler{
    
    /**
     * The original handler we are customizing.
     */
    private ViewHandler prevHandler = null;
    
    /** Creates a new instance of MappingViewHandler. By including
     * a parameter of the same type, we encourage the JSF framework
     * to pass a reference of the previously used ViewHandler. This way
     * we can use all the previous functionallity and override only the
     * method we are interested in (in this case, the getActionURL() method).
     */
    public MappingViewHandler(ViewHandler prevHandler) {
        this.prevHandler = prevHandler;
    }
    
    
    /**
     * Delegate control to the original ViewHandler
     */
    public Locale calculateLocale(FacesContext context) {
        return prevHandler.calculateLocale(context);
    }

    ...

    /**
     * This is the only method needed to be extended. First, we get the
     * normal URL form the original ViewHandler. Then we simply return
     * the same URL with the extension stripped of.
     */
    public String getActionURL(FacesContext context, String viewId) {
        String origURL = prevHandler.getActionURL(context, viewId);
        int dotIdx = origURL.lastIndexOf(".");
        if (dotIdx > 0) {
            return origURL.substring(0,dotIdx);
        }
        else return origURL;
    }

    ...
}

This way you can have flawless extension-less mapping for JavaServer Faces. If you want to disable requests for URLs having an extension completely (they will still work even if you use my classes), you can add a few more lines of code to the MappingFilter.

To get the complete source code, use the following links:

Posted in JavaServer Faces

Reji Raveendran

xfrag,
Actually i was searching to find out a way to print the Faces Servlet process logs.I’m just a beginner to the colourful and promising world of JSF. It is a well written article and people like you can really guide beginners through this kinda articles.

If you know the way to print the faces servlet logs please share with me.

Thanks a lot.
Reji

Posted at January 19th, 2008 - 6:25 am

xfrag

Reji,

First of all I would like to thank you for your kind words.
As for you question, the answer is “it depends”. For example if you use the JSF 1.2 implementation from Sun and run you application on glassfish application server, you can follow the directions in this page.
If you use another JSF implementation and a different server to run your application, you’ll have to check out the documentation for both to find out the way..
I hope this will help you a bit.

Posted at January 21st, 2008 - 3:50 am

brahim

Good idea to use this filter.
Thank you very much

Posted at December 22nd, 2009 - 5:07 am

Post a Comment Below »
Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.

Java and all Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.