Friendly URL in JSF with a simple “Rewrite-Url”

Advertisements

Java Server Faces (JSF) is a very useful server-side rendering framework to create HTML UI in Java projects. It’s part of the Java EE specifications. One of the drawbacks of Java Server Faces is the generated URLs for our pages, they follow as pattern the structure of the folders in the project.

From a UIX perspective, having urls like '/pages/user/user-list.xhml‘ is not good, it’s better to have a url like ‘/users‘, ‘/user/new‘. Also, from a security perspective, we are exposing our folder’s structure to other people, and they can use that information to find a vulnerability.

That’s why there are some third party libraries for JSF like pretty-faces, that allows to use friendly and nice urls in our JSF application. However, they usually offer a bunch of functionalities (patterns, mapping path params, and more) that are not necesary in some of the applications. Therefore, I have created my very simple rewrite url for JSF and avoid overload my application with things that we might not need.

Let’s see the code

Let’s start by creating a singleton to “load” the urls we want to transform to “pretty” urls. We will use a HashMap variable to storage the urls.

package com.orthanc.commons.rewriteurl.jsf;

import java.util.HashMap;
import java.util.Map;

/**
 *
 * @author Adam M. Gamboa G
 */
public class RewrittenURLs {
    private static final RewrittenURLs INSTANCE = new RewrittenURLs();
    private Map<String, String> URLS;
    
    private RewrittenURLs(){
      Map<String, String> urls = new HashMap<>();
      urls.put("/pretty/page", "/ugly/path/to/page.html");
      urls.put("/customers", "/pages/customer/list.xhtml");
    }
    
    public static RewrittenURLs getInstance(){
        return INSTANCE;
    }

    public Map<String, String> getURLS(){
        return URLS;
    }
}

This is the trick!!! We are going to implement a JSF ViewHandlerWrapper and a WebFilter to map a nice url and then dispatch the content in the original “uggly” url.

Let’s start with the ViewHandlerWrapper, this class is going to handle our JSF navigation urls (following the folder structure) and convert it to the “pretty” url that the server side is going to receive.

Advertisements
package com.orthanc.commons.rewriteurl.jsf;

import java.util.HashMap;
import java.util.Map;
import javax.faces.application.ViewHandler;
import javax.faces.application.ViewHandlerWrapper;
import javax.faces.context.FacesContext;

/**
 *
 * @author Adam M. Gamboa G
 */
public class RewriteViewHandler extends ViewHandlerWrapper {

    private static final Map<String,String> URLS = new HashMap<>();
    private final ViewHandler wrapped;

    public RewriteViewHandler(ViewHandler wrapped) {
        this.wrapped = wrapped;
        Map<String, String> rewrittenUrls = RewrittenURLs.getInstance().getURLs();
        
        if(rewrittenUrls != null && !rewrittenUrls.isEmpty()){
            rewrittenUrls.entrySet().stream().forEach((entry) -> {
                URLS.put(entry.getValue(), entry.getKey());
            });
        }
    }

    @Override
    public String getActionURL(FacesContext context, String viewId) {
        String actionURL = super.getActionURL(context, viewId);
        
        String actionURLWithoutParam;
        String actionParams;
        int indexOfParam = actionURL.indexOf('?');
        if(indexOfParam != -1){
            actionURLWithoutParam = actionURL.substring(indexOfParam);
            actionParams = actionURL.substring(0, indexOfParam);
        }else{
            actionURLWithoutParam = actionURL;
            actionParams ="";
        }
        String contextPath = context.getExternalContext().getRequestContextPath();
        String action = actionURLWithoutParam.substring(contextPath.length());
        
        if (URLS.containsKey(action)) {
            String newURL = contextPath+URLS.get(action)+actionParams;
            return newURL;
        }
        return actionURL;
    }

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

Then, the web Filter is in charged of transforming the “pretty” url from the browser and transform it to call html page in our folder’s structure . It’s using the forward from the dispatcher to navigate to the html page without changing the url in the browser.

package com.orthanc.commons.rewriteurl.jsf;

import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

/**
 *
 * @author Adam M. Gamboa G
 */
@WebFilter(urlPatterns = {"/*"}, asyncSupported = true)
public class RewriteURLFilter implements Filter {
    
    private Map<String,String> URLS;
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        URLS = RewrittenURLs.getInstance().getURLs();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper((HttpServletRequest)request);
        String prettyURL = wrapper.getRequestURI().substring(wrapper.getContextPath().length());
        
        String prettyURLWithoutParam;
        String queryParams;
        int indexOfParams = prettyURL.indexOf('?');
        if(indexOfParams != -1){
            prettyURLWithoutParam = prettyURL.substring(indexOfParams);
            queryParams = prettyURL.substring(0, indexOfParams);
        }else{
            prettyURLWithoutParam = prettyURL;
            queryParams ="";
        }
        
        if(URLS.containsKey(prettyURLWithoutParam)) {
            String originalURL = URLS.get(prettyURLWithoutParam)+queryParams;
            RequestDispatcher dispatcher = wrapper.getRequestDispatcher(originalUrl);
            dispatcher.forward(request, response);
        } else {
            chain.doFilter(wrapper, response);
        }
    }
}

Finally, we just need to register our view handler in the faces-config.xml file.

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2"
              xmlns="http://xmlns.jcp.org/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
    <application>
        <view-handler>com.orthanc.commons.rewriteurl.jsf.RewriteViewHandler</view-handler>
    </application>
</faces-config>

I have improved the previous approach to load the mapped URLs from a xml file instead of hard-coding them. In that approach, the application will look for the file rewrite-url.xml.

<xml>
   <url-mapping id="home">
      <pattern value="/home"/>
      <view-id value="/pages/home.xhtml"/>
   </url-mapping>
   <url-mapping id="about">
      <pattern value="/about"/>
      <view-id value="/pages/about.xhtml"/>
   </url-mapping>
   <url-mapping id="customer_list">
      <pattern value="/customers"/>
      <view-id value="/pages/customer/list.xhtml"/>
   </url-mapping>
</xml>  

If you are interested, you can see the code in my gitHub repository – Simple Rewrite URL for JSF

References

https://github.com/AdamGamboa/simple-rewriteurl-jsf

Advertisements

2 comments

  1. I liked your code and wanted to use it. When I deploy the app in tomcat in windows it works great but when I test it in tom cat installed in linux I get 404 error

    1. I haven’t tested it on tomcat, but I did do it on Linux, macOs and windows, without any problem…
      Not sure, if the problem will be tomcat or not, but it should work on any environment. If you want to share more details about your error, then I can help you.

Leave a Reply

Your email address will not be published. Required fields are marked *