Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Wednesday, June 23, 2004

Clever Jetty Hack: Dynamically merging directories inside a web application context

I'm working on converting an existing web site from homebrew servlets and JSPs to Tapestry. What's interesting is that the application in question is deployed into different hosts using different "skins". Certain stylesheets and images are different, and there are minor differences in layout and behavior.

So, I'm working with their workspace directory layout; there's a common directory that contains the main content (images, stylesheets, Tapestry artifacts). Then there's additional directories, such as defaultskin, that contain skin-specific assets and artifacts.

When building and deploying, the content is merged together to form a composite web application context.

For development purposes, I want to leave the directory structure intact, but that makes it impossible to run the application (the assets and artifacts in the defaultskin directory simply aren't visible. What I need is to be able to have a virtual directory within my web application that points to an entirely different directory; I want to map /skin in my context to the defaultskin folder, so I can use URLs like http://.../skin/images/about.gif.

Did a bit of research and realized that the correct approach was to create my own implementation of Jetty's WebApplicationContext that included the necessary hooks:

package portal.jetty;

import java.io.IOException;

import org.mortbay.jetty.servlet.ServletHttpContext;
import org.mortbay.jetty.servlet.WebApplicationContext;
import org.mortbay.util.Resource;

/**
 * Used only during testing using Jetty, this allows resources "within"
 * a web application context to be retrieved from an entirely different
 * directory.
 *
 * @author Howard Lewis Ship
 */
public class SkinContext extends WebApplicationContext
{
    private String _skinURI;
    private String _skinPath;
    private Resource _skinResource;

    /**
     * Standard constructor; passed a path or URL for a war (or exploded war
     * directory).
     */
    public SkinContext(String war)
    {
        super(war);
    }

    public String getSkinURI()
    {
        return _skinURI;
    }

    public void setSkinURI(String string)
    {
        _skinURI = string;
    }

    public Resource getResource(String contextPath) throws IOException
    {
        if (contextPath.startsWith(_skinURI))
        {
            String postPrefixPath = contextPath.substring(_skinURI.length());

            return _skinResource.addPath(postPrefixPath);
        }

        return super.getResource(contextPath);
    }

    public String getSkinPath()
    {
        return _skinPath;
    }

    public void setSkinPath(String string)
    {
        _skinPath = string;
    }

    public void start() throws Exception
    {
        super.start();

        _skinResource = Resource.newResource(_skinPath);
    }

}

This gets combined with a specialized jetty.xml (startup configuration file):

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure 1.2//EN" "http://jetty.mortbay.org/configure_1_2.dtd">

<!-- At deployment time, the WAR will be built properly, so that content under 
  "skin" is just appropriate to the type of site. During development, we don't want to have
  to copy files around unecessarily, so we alias the web/defaultskin directory as "/skin". -->
   
<Configure class="org.mortbay.jetty.Server">
  <Call name="addListener">
    <Arg>
      <New class="org.mortbay.http.SocketListener">
        <Set name="port">8080</Set>
      </New>
    </Arg>
  </Call>
  
  <Call name="addContext">
    <Arg/>
    <Arg>
      <New class="portal.jetty.SkinContext">
        
          <Arg>web/common</Arg>
        
        <Set name="contextPath">/</Set>
        <Set name="skinURI">/skin/</Set>
        <Set name="skinPath">web/defaultskin/</Set>
      </New>
    </Arg>
  </Call>
  
</Configure>

Viva open source! I didn't even bother pulling down the Jetty source to figure this out, I just navigated around Jetty's browse CVS to find the few code snippets. Of course, I don't think this solution is ideal for deployment, but that's not the point ... it's about making my development time efficient and I'm quite happy with it.

No comments: