Search code examples
javajsptomcatincludewebcontext

Dynamically compile and include JSP(X)s outside Web Context Root


I have a web application heavily using jspx-files and jsp:include-includes which is deployed from the same jar to several hundred contexts in tomcat. Currently we can only modifying the layout of each individual instance by changing CSS or overwriting certain files in other file directories:

Let's say the webapp.jar contains a file named /css/forms.css then the webapp ensures that GETting http://mydomain/cust1/res/css/forms.css is mapped to that file. If the webapp is configured to look in the "override directory" /data/cust1/, then if there's a file named /data/cust1/css/forms.css this file is served instead from the one in the application archive. This is ensured by the webapp itself.

During the last years this has been very successful but recently I feel the pain of the restriction of the more or less static jspxs that cannot be "overridden" ;) Basically I'd like to be able to "override" some jspx files for each deployed context without compiling and deplyoing a custom webapp-jar for each context. (Having a custom something:includeJsp-tag wouldn't be a problem).

Basically the webapp should be able to provide an override to the jsp(x) compiler for individual jspx-files, e.g. take a look at the following example sutrcture:

<!-- webapp.jar:/jspx/view.jspx -->
<jsp:root ...>
  <ns:customInclude src="inc/include.jspx" />
</jsp:root>

<!-- webapp.jar:/jspx/inc/include.jspx -->
<jsp:root ...>
  Default-Markup from the webapp.jar
</jsp:root>

<!-- /data/cust1/jspx/inc/include.jspx -->
<jsp:root ...>
  Custom markup for "cust1"
</jsp:root>

Now, when http://.../cust1/jspx/view.jspx is requested I want /data/cust1/jspx/inc/include.jspx to be compiled and executed by Tomcat. Basically I know that actually everything necessary is already possible somewhere in Tomcat (compiling from a jspx to Java-Bytecode, including the file, ...), but I also know that Tomcat/Jasper adhere to the jsp-spec. I figured by looking at the code that this is not that easy...

So basically, does anyone know how to get a setup like this to work? Did perhaps someone already solve this? Or are there alternatives to my current approach?


Solution

  • I ended up implementing my own FileDirContext that is included via a <Resources>-Tag in context.xml. Actually I implemented two - an extension of org.apache.naming.resources.WARDirContext and an extension of org.apache.naming.resources.FileDirContext because I wanted to enable the same mechanisms during development with eclipse (which deploys to a file structure) and on our servers which deploy from non-exploded .war files.

    Both of them can be configured via the context descriptor and can use several "virtual overlays". An overlay can either be a file system path or an external .war file (actually more like a .zip renamed to .war) or a .jar file packed into the deployed .war file.

    The steps to a tuned down solution with one virtual overlay are:

    1. Create a class MagicWARContext (magic is like the solution to every problem ;) extending from aforementioned WARDirContext.
    2. In this class create a static inner class ProtectedMethodVisibleMaker extends FileDirContext. This allows to access certain protected methods of Tomcat's original implementation. Override doGetRealPath and doLookup.
    3. In your own WAR context create getters and setters for a field private String overlay and a field private ProtectedMethodVisibleMaker overlayContext and instantiate a new and empty instance to this field.
    4. In allocate() of your WAR context call overlayContext.setDocBase(overlay). Be sure to call overlayContext.release() from release() just to be sure.
    5. Now you have to override 5 more methods of WARDirContext
      • doGetRealPath(String)
      • doLookup(String)
      • getAttributes(Name, String[])
      • list(String)
      • list(Name) The resulting enumerations of both lists should of course contain entries of a virtual overlay as well as the original context. So I ended up writing my own NamingEnumeration that iterates over a list of delegate naming enumerations and ensures that each named entry is output only once.
    6. Instantiate your Magic Context from the context descriptor:

      <Resources 
          className="package.name.tomcatextensions.MagicWARContext" 
          overlay="/data/some/dir"/>
      
    7. Pack your classes in a jar and copy this e.g. to tomcat/lib

    Caveats

    We're overriding Tomcat's internal classes. This naturally exposes this code to changes. So someone has to ensure that everything works as expected after version upgrades. Instead of some of the protected methods one can override the actual public ones, but some of them are final so you're basically out of luck.

    Plus it is possible to expose non-magic subcontexts to other application layers via the lookup methods meaning they don't have that overlay anymore or don't contain the fallback resources of your deployed war. I ended up adding some warnings to the log when subcontexts are requested. During startup this happens two times, once for /WEB-INF/classes and once for /WEB-INF/lib.
    In my usecase I prohibit adding virtual overlays that contain either on of those paths because I don't want to have foreign class files appearing in my web context, so this is not a problem. However some other client might be calling those methods and doing something unexpected with them. During my tests I did not find any problem, but there might occure strange effects with caching enabled or when enumerating resources and there will be problems when sub contexts are used... Of course one could create virtualized wrappers around a subcontext returned by a lookup, but I didn't do this yet as I hope this is unnecessary.

    And of course this method enables you to override certain resources of the fallback webapp in un-exploding war deployments. If you just need to add external resources to the web context you can use Tomcat's aliases (see How do I add aliases to a Servlet Context in java?).

    Use at your own risk and have fun.