Wiki News Projects Sources Tasks New Task Reports
RelativePathsInJSF
Page Info Get as PDF

Relative Paths in JSF

Top Announcement: 10-August-2008 EmForge-0.25 with Windows Installer released! See News for details.

During working on EmForge, initially all pages was placed on one level - and I do not meet problems with linking to resources and pages itself - since all pages are placed on one level - it was easy to use relative paths...

But, in some moment we decided to use 'Search-Engine Friendly URLs' - and now, same page may be placed on different levels... and we meet problem with linking to resources...

Actually, it appears in followed cases:

  1. Referencing to the another page in web-project with using h:outputLink
  2. Referencing to images with h:graphicImage and so on
  3. referencing to css and other staff from non-JSF tags
  4. Making links in Java-Code

First Solution - Base-URL

First solution we used in EmForge - was in using base html tag:

<base href="#{emForgeContext.applicationPath}" />

EmForgeContext bean received applicationPath from configuration. I did not liked this way because:

  1. We should specify path to application in configuration - I do not like extra configurations;
  2. We should specify full path to application (like http://www.emforge.org)
  3. Some sites may be accessed by different ways (like http://localhost:8080/EmForge and http://akakunin-nb:8080/EmForge in my case). And even if we accessed site by one host-name - all links will be referenced by host, specified in application path
  4. I realized that links to bookmarks on the same page ('#') doesn't work with base...

After discussing with Walter Mourao about this problem he pointed me another solution: to use FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath() for converting relatives path to absolujte on the web-server

Second Solution: convert relative paths to absolute

Since FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath() returns context path of application on the server - adding it before relative path will give us absolute path on the server...

So, this way


<script src="#{emForgeContext.path}/resources/scripts/prototype.js" type="text/javascript"></script>
there #{emForgeContext.path} returns this context path works perfectly... And for pure html task I had to do so... But - to add this #{emForgeContext.path} into all h:outputLinks and and h:graphicImages - it is too much. Hopefully, using of JSF give us better way to solve this problem To convert all links from relative paths to absolute it is enougth to implement own renderer for javax.faces.Link render-time...

We are using MyFaces-1.1.5 in our project - so, I simple get, sources of renderer, responsible for links and implemented own:

First - two utility methods to easily get context path and to determine - is link relative or not:



    public static String getPath() {
        return FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath();
    }

    /** Determines - is specified path - relative path or not?
     * 
     * @param i_href
     * @return
     */
    public static boolean isRelativePath(String i_href) {
        if (StringUtils.isEmpty(i_href)) {
                return false;
        }
        
        if (i_href.contains("://")) {
                // it is absolute path with protocol specified
                return false;
        }
        
        if (i_href.charAt(0) == '/') {
                // absolute path inside the site
                return false;
        }
        
        return true;
    }

Second: implementing own LinkRenderer - here I simple inherited from org.apache.myfaces.renderkit.html.HtmlLinkRenderer and overrided method renderOutputLinkStart, implemented in HtmlLinkRendererBase:



/** Extension for standard Link Renreder to correctly handle relative paths
 * 
 * @author akakunin
 *
 */
public class HtmlLinkRenderer extends
                org.apache.myfaces.renderkit.html.HtmlLinkRenderer {

        /**Rendred Output Link
         * This code is mostly copied from MyFaces renderer, but - we extensinf it for checking href
         * If href is relative path - we adds context path to get absolute path on the server.
         * It helps easily build web-gui with pages, placed on different levels to refer to some static context
         */
        @Override
        protected void renderOutputLinkStart(FacesContext facesContext, UIOutput output) throws IOException {
        ResponseWriter writer = facesContext.getResponseWriter();

        //calculate href
        String href = org.apache.myfaces.shared_impl.renderkit.RendererUtils.getStringValue(facesContext, output);
        
        if (Helper.isRelativePath(href)) {
                href = Helper.getPath() + "/" + href;
        }
        
        if (getChildCount(output) > 0) {
            StringBuffer hrefBuf = new StringBuffer(href);
            addChildParametersToHref(output, hrefBuf,
                                     (href.indexOf('?') == -1), //first url parameter?
                                     writer.getCharacterEncoding());
            href = hrefBuf.toString();
        }
        href = facesContext.getExternalContext().encodeResourceURL(href);    //TODO: or encodeActionURL ?

        String clientId = output.getClientId(facesContext);

        //write anchor
        writer.startElement(HTML.ANCHOR_ELEM, output);
        writer.writeAttribute(HTML.ID_ATTR, clientId, null);
        writer.writeAttribute(HTML.NAME_ATTR, clientId, null);
        writer.writeURIAttribute(HTML.HREF_ATTR, href, null);
        HtmlRendererUtils.renderHTMLAttributes(writer, output, org.apache.myfaces.shared_impl.renderkit.html.HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES);
        writer.flush();
        }
Actually it is simple copy of base implementation with simple checking for relative links and converting them to absolute (if it is required)

Next - we need to say JSF to use our rendered instead of standard - for doing it add into your faces-config.xml


<render-kit>
    <render-kit-id>HTML_BASIC</render-kit-id>    
    <renderer>
        <component-family>javax.faces.Output</component-family>
        <renderer-type>javax.faces.Link</renderer-type>
        <renderer-class>ru.emdev.EmForge.web.renderer.HtmlLinkRenderer</renderer-class>
    </renderer>
</render-kit>     

So, now, all JSF-components like h:outputLink and based on it will use our renderer - and links will be processed automatically - so, you do not need to case about it in your html-code.

Automatic converting links in graphicImage

First idea for h:graphicImage were same - to implement own renderer, but, looking into code I found, that renderer user ViewContext to get real resource URL (part of code in org.apache.myfaces.shared_impl.renderkit.html.HtmlImageRendererBase):


String src = facesContext.getApplication().getViewHandler().getResourceURL(facesContext, url);

So, seems better solution - to implement own getResourceURL that will convert links for all resources

Since EmForge is using Facelets and Ajax4Jsf we did followed steps:

Implemented own ViewHandler - based on FaceletViewHandler and override only one method:



/** Our implementation of View Handler to correctly process resource links
 * 
 */
public class ViewHandlerImpl extends FaceletViewHandler {

  public ViewHandlerImpl(ViewHandler arg0) {
    super(arg0);
  }

  /** We overriding this method to check - is specified path is relative path - 
   * and add context path to make it absolute
   */ 
  @Override
  public String getResourceURL(FacesContext i_context, String i_url) {
    String url = i_url;
    if (Helper.isRelativePath(url)) {
      url = Helper.getPath() + "/" + url;
                        
      return url;
    }
                
    return super.getResourceURL(i_context, url);
  }
}

Here is one issue: initially, I called return super.getResourceURL(i_context, url); for all url's, but somebody inside it added contextPath once again... I did not realize who did it - so, decided to do not pass url to super-class if it was already processed by our handler

Now, we should say to ajax4jsf to use our view-handler instead of facelets based (in web.xml):


<context-param>
    <param-name>org.ajax4jsf.VIEW_HANDLERS</param-name>
    <param-value>ru.emdev.EmForge.web.ViewHandlerImpl</param-value>
</context-param>

If ajax4jsf is not used - we need to specify it in faces-config.xml:


<view-handler>
    ru.emdev.EmForge.web.ViewHandlerImpl
</view-handler>    

OK, that is all!

Conclusion

As result, we still need care and add #{emForgeContext.path} in all pure html links. And we still should case about adding this context-path in URLs, created inside Java-Code (and not outputed by JSF-tags).

But, at least we should not care about it in all jsf-based tags like h:outputLink and h:graphicImage - that anyway makes our life simpler.

I do not sure this solution 100% working (I did not meet problems with it for now - but who knows) - and I'm not sure it is best solution - but it is that I found after some time working on this problem - any links to some problems and better solutions are only welcomed!

Last Modified by akakunin 1 week ago
Comments (0)
Login to add comment