Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tomcat JSP/JSTL without HTTP

I have a pretty standard web app running under Tomcat 7.

What I'm now trying to do is leverage JSP/JSTL as a templating language independent of the HTTP/web serving aspects of Tomcat to produce HTML that can be emailed and converted to PDF.

Has anyone else tried to do this before and could help me with some pointers?

Thanks in advance.

like image 384
Kong Avatar asked Dec 16 '11 01:12

Kong


1 Answers

In contrast to what Stephen C said, yes, JSP are Servlets, etc. etc. (And Velocity is quite good and easy to use)

But, what is a Servlet?

It's an interface. An interface with one major method:

service(ServletRequest req, ServletResponse res)

Locate the JSP class, cast it to a Servlet, create implementations of ServletRequest and ServletResponse, and then...

String jspClassName = findJspClassForJSP("your.jsp");
Class jspClass = Class.forName(jspClassName);
Servlet jspServlet = (Servlet)jspClass.newInstance();
MyServletRequest req = new MyServletRequest();
MyServletResponse resp = new MyServletResponse();
jspServlet.init();
jspServlet.service(req, resp);
jspServlet.destroy();
String results = reps.getContent();

Will this work? Well, after some work it will. Obviously you need to implement the minimum facades of the ServletRequest/Response as well as what ever your JSPs will need. But, likely you will probably need little more than the attributes and the streams. If you make your Response return a StringWriter, you're halfway there.

The next part is creating the servlet from the JSP. Handily, the Jasper compiler does that for you -- the game is invoking it. I have never done it directly, but it clearly can be done since both the servlet container does it, as well as the JSPC script/bat file, the ant task, as well as most of the Servlet containers out there use Jasper. So, that can be done. Once you know how to invoke that, you'll know the final generated class name for the JSP. (See the first line of the sample.)

Have I ever done this? No. But I bet within less than a day of messing around you'll know whether this is doable or not. I'm betting it is, especially if you don't run in to any class loader shenanigans. You'll possibly have an issue if you let your users change and regenerate a JSP (so MyEmail.jsp gets compiled in to MyEmail.class, MyEmail_2.class, etc.). But if you invoke Jasper yourself, you'll likely have more control over this. The other hard part is determining the class name of the JSP. Most of the containers follow a basic pattern here, so if you poke around in the generated code from a WAR you'll likely find it.

Keep the JSPs reasonably simple (and an Email template shouldn't need to super complicated with embedded Java or anything making random calls), and it even more a good chance it will work.

Your solution may not be portable out of the box out of Tomcat, but you likely won't care. The folks that I've talked to that use JSP for templates, simply opened a socket to their own server and made a request. They didn't go this far either.

But on the surface, save some whacky class loader black hole hell, I bet you can get this to work pretty quick. Implement as little of the request and response as you need to, fight a few NPEs as the JSP and JSTL call stuff you weren't planning, and, as Santa says,

Hack away, Hack away, Hack away all!

Addenda:

So, for all the naysayers...

public void runJsp() {
    JspC jspc = new JspC();
    jspc.setUriroot("/tmp/app");
    jspc.setOutputDir("/tmp/dest");
    jspc.setJspFiles("newjsp.jsp");
    jspc.setCompile(true);
    try {
        jspc.execute();
        Class cls = Class.forName("org.apache.jsp.newjsp_jsp");
        Servlet s = (Servlet) cls.newInstance();
        MyRequest req = new MyRequest();
        MyResponse resp = new MyResponse();

        s.init(getServletConfig());
        s.service(req, resp);
        s.destroy();
        System.out.println(resp.getSw().toString());
    } catch (JasperException ex) {
        throw new RuntimeException(ex);
    } catch (ClassNotFoundException ex) {
        throw new RuntimeException(ex);
    } catch (InstantiationException ex) {
        throw new RuntimeException(ex);
    } catch (IllegalAccessException ex) {
        throw new RuntimeException(ex);
    } catch (ServletException ex) {
        throw new RuntimeException(ex);
    } catch (IOException ex) {
        throw new RuntimeException(ex);
    }
}

Amazing what source code and 1/2 hour in a debugger will do for you.

I created a simple JSP in /tmp/app/newjsp.jsp.

jspc.setUriroot tells the compiler where the base of your "web app" is located. jspc.setOutputDir tells jspc where to put the generated Java and Class files. jspc.setJspFiles tells jspc what files to compile, based off of the URI Root. jspc.setCompile told it to actually compile the code. Finally, jspc.execute() does the deed.

By default Jasper uses the package org.apache.jsp, and creates a new class based on the JSP file name. For my simple experiment, I simply put "/tmp/dest" on to the class path of my Glassfish container, so that the container would find the generated classes.

I load the class, and get an instance.

Finally, I created MyRequest, MyRequest, and, ultimately, MySession. My IDE conveniently created stubs for the respective interfaces. In this case I implemented: MyRequest.getSession(), MyResponse.setContentType(), MyResponse.setBufferSize(), and MyResponse.getWriter().

public PrintWriter getWriter() throws IOException {
    if (sw == null) {
        sw = new StringWriter();
        pw = new PrintWriter(sw);
    }
    return pw;
}

Obviously sw and pw are instance variables of MyResponse.

MyRequest returned an instance of MySession. My implementation of MySession does -- nothing. But the runtime wanted a Session, it just doesn't use it on its own for my very simple JSP, and I wasn't motivated on stuffing in the the one from the Servlet.

I tested this on Glassfish v2.1. I simply added appserv_rt.jar (from glassfish/lib) to my build class path (so it could find the JspC jars), but I don't bundle it in the WAR (since it's already in the container).

And, shazam, it worked. In "real life", assuming the process that wanted to leverage the JSP was actually sourced from a web request, I would simply create an HttpServletResponseWrapper and override the earlier three methods, the rest would probably Just Work. If a web request isn't in the picture at all, then you'd need to create your own Session implementation (no big deal really, it's just a map).

I'd also use a private URLClassLoader to load the faux JSP classes. If I KNEW I'd never reload a JSP, then would just make the destination my WEB-INF/classes directory and give it it's own package and let the system load them.

But, yup, it worked. No big deal. It's just java.

like image 115
Will Hartung Avatar answered Sep 21 '22 22:09

Will Hartung