Skip to main content

JSP Template Building Block

Continuing on my quest for creating a Java Mediator for Tridion templating, I have now reached the moment when I would focus on a JSP TBB. Check out my previous post about Java Fragment TBB and how to validate its syntax/compilation capability.

The JSP TBB is meant to provide the same functionality as a Dreamweaver TBB, only in Java/JSP technology. So I had in mind the following requirements:
  • JSP support (write the TBB in JSP syntax);
  • compile JSP to Java source file;
  • compile Java source to byte-code;
  • execute Java compiled class in the context of Engine and Package objects;
  • create "Output" package item containing the result of JSP execution;
The following is a sample of JSP Layout TBB for a Page Template.
Note:
  • The objects Engine and Package are available in the 'request';
  • There is no real 'request' object as the entire JSP code-above does not run in or require a J2EE container -- the request is a FakeHttpServletRequest, but more on that below...
  • Page object is called _page, as there is already a "page" variable defined in the HttpJspBase of this JSP source class;

General Approach

  • The JSP Layout TBB type is handled by the mediator class (.net) JspJstlMediator, as defined in the Tridion.ContentManager.config file;
  • The .NET mediator object calls the Java proxy JspJstlMediatorImpl.Transform(), which performs the actions:
    • create JSP file on file-system;
    • compile JSP to Java source;
    • compile Java source to Java Servlet .class;
    • execute the servlet while injecting the Engine and Package JNI proxy objects into the request as attributes;
  • Output package item is created and it contains the output generated by the execution of the servlet class;

JspJstlMediator.cs

public class JspJstlMediator : IMediator {

    public void Transform(Engine engine, Template template, Package package) {
        Utils.SetupJni4NetBridge();
        IMediator mediator = new JspJstlMediatorImpl();
        mediator.Transform(engine, template, package);
    }
}

The Utils.SetupJni4NetBridge() is a utility method that simply creates the Jni4Net Bridge, as described in my previous post Yet Another Java Mediator.

JspJstlMediatorImpl.java

This class simply dispatches to the template handler for JSP generation, compilation and execution.

public class JspJstlMediatorImpl implements IMediator {

    public void Transform(Engine engine, Template template, Package _package) {
        JspJstlTemplateHandler handler = new JspJstlTemplateHandler(template);
        handler.createJsp();
        handler.compileJsp();
        handler.compileJava();
        handler.execute(engine, _package);
    }
}

JspJstlTemplateHandler.java

This class implements the entire logic of handling a given Template. The constructors are shown below:

public JspJstlTemplateHandler(String name, String content, long lastModifiedTicks) {
    this.name = name;
    this.content = content;
    this.lastModifiedTicks = lastModifiedTicks;
    //... other initializations
}

public JspJstlTemplateHandler(Template template) {
    this(template.getTitle(),
        template.getContent(),
        template.getRevisionDate().ToUniversalTime().getTicks());
}

Create JSP File

The sole purpose of this method is to create the JSP file on the file system. So it takes the content of the Template and writes it into a file in a certain location on the file system. Several parts of this methods are missing, e.g. exception handling or cache handling based on Template LastRevisionDate and file LastModified date.

public void createJsp() {
    try {
        BufferedWriter out = new BufferedWriter(new FileWriter(jspFileName));
        out.write(content);
        out.close();
    } catch (IOException ioe) {
        // handle exception
    }
}

Compile JSP to Java Source File

The biggest challenge for me was to find a way to generate the Java Servlet source code representing the JSP itself and then execute it outside a J2EE container! I could have opted for the presence of a Tomcat instance on the CM server and I would just drop the JSP in one of its web-application roots. I thought however that that would impose some overhead that I don't want to deal with (e.g. installation of Tomcat, configuring a web-application).

Instead I opted for an out-of-container JSP compilation and JSP/Java Servlet execution. I will explain the execution part further-down in this post. Let's focus now on the JSP to Java source 'compilation'.

I used  Jasper2 from Tomcat 6 for converting JSP files to Java sources. There is also a shell class for Jasper compiler in class org.apache.jasper.JspC that can be used to pre-compile JSP files. This class is in fact a Java Application, so it can run from command line, thus outside of a J2EE container -- exactly what I wanted.

Using JspC is in fact very simple due to its versatile parameters that can be passed to it. In my case, the sample for generating JSP to Java source is the following:

public void compileJsp() {
    JspC jspc = new JspC();
    try {
        String[] jspcArgs = new String[] { "-uriroot"jspDir, "-d", jspSrcDir, jspFileName };
        jspc.setArgs(jspcArgs);
        jspc.execute();
    } catch (Exception e) {
        // handle exception
    }
}

Notice the three parameters I'm sending to JspC are:
  • -uriroot - the directory where the root of my web-application (i.e. I don't have one, so I just set parent folder where the JSP resides; I also don't have a web.xml or a WEB-INF folder);
  • -d - the directory where to generate the Java sources under;
  • the actual JSP file location to 'compile';

Compile Java Source to Byte-Code

JspC can also handle the actual compilation of the generated Java source into a byte-code .class file by simply supplying a -compile argument. I chose however not to use this option due to its overhead in requiring several additional JAR files (e.g. ant.jar, ant-launcher.jar, etc).

Instead I opted for using the out-of-the-box Java Compiler API that I already had used for the Java Fragment TBBs. The code is very similar to the previous, only that the file to compile comes from a real File on the file-system and not from a String:

public void compileJava() {
    File javaFile = new File(javaFileName);
    try {
        SourceStringCompiler compiler = new SourceStringCompiler(jspSrcDir);
        compiler.compile(javaFile);
    } catch (RuntimeException re) {
        // handle exception
    }
}

Execute JSP Outside the J2EE Container

Executing the .class file follows the same methodology described in my earlier post -- i.e. reflection. However the twist here is that I'm in fact executing the service(HttpServletRequest, HttpServletResponse) method of a Java Servlet that represents my JSP.

As I said previously, I don't have an Application Server to execute my JSP in. Therefore, I had to fake all the objects that I would normally receive from a J2EE container -- e.g. request, response, servlet context and config, etc.

Once I had all the necessary objects, I was able to invoke the service() method using reflection and it would execute the Servlet, as if it were executing inside a J2EE container. Of course there are some things that simply don't exist and I excluded them from the 'fake' classes. For example, the 'application' or 'session' scope, session itself, many parameters of the request and response objects, etc.

public void execute(Engine engine, Package _package) {
    try {
        File jspSrcDirFile = new File(jspSrcDir);
        ClassLoader parentLoader = JspJstlTemplateHandler.class.getClassLoader();

        URLClassLoader classLoader = new URLClassLoader(
                new URL[] { jspSrcDirFile.toURI().toURL() },
                parentLoader);
        Class<?> jspJavaClass = classLoader.loadClass("org.apache.jsp." + javaName + "_jsp");
        HttpJspBase jspPage = (HttpJspBase) jspJavaClass.newInstance();

        StringWriter outputWriter = new StringWriter();
        FakeJspFactory jspFactory = new FakeJspFactory(outputWriter);
        JspFactory.setDefaultFactory(jspFactory);

        HttpServletRequest request = new FakeHttpServletRequest();
        HttpServletResponse response = new FakeHttpServletResponse();

        request.setAttribute("engine", engine);
        request.setAttribute("package", _package);

        jspPage.init(new FakeServletConfig());
        jspPage.service(request, response);

        _package.PushItem("Output", _package.CreateHtmlItem(outputWriter.toString()));
    } catch (Exception e) {
        // handle exception
    }
}

Note:
  • FakeJspFactory that takes as argument a StringWriter. This is where the JSP execution output will be written to, hence I'm using this writer to create the Output package item holding the result of this TBB's execution;
  • Fake request/response objects needed to pass to the service() method;
  • FakeServletConfig that initializes the JSP Servlet -- needed for things like Expression Evaluator (in EL), tag handling, etc. (but more about tag support in a following post);

The Fake Stuff

In order to execute a Java Servlet without a J2EE container, I have to provide fake classes that the servlet  directly uses, that the container would normally provide. Uncle Bob gives a great explanation of his 'mock' implementation classes.

I implemented the following 'fake' classes:

FakeJspFactory.java

This classes serves two purposes:
  • defines a constructor that takes a StringWriter that will contain the entire execution output;
  • creates a FakePageContext that is represents the entry point into the entire 'fake' world that mimics the J2EE container;
public class FakeJspFactory extends JspFactory {

    private StringWriter outputWriter;

    public FakeJspFactory(StringWriter outputWriter) {
        this.outputWriter = outputWriter;
    }

    public PageContext getPageContext(Servlet servlet, ServletRequest servletRequest, ServletResponse servletResponse,
            String string, boolean b, int i, boolean b1) {
        FakeJspWriter jspWriter = new FakeJspWriter(outputWriter, i, b1);
        return new FakePageContext(jspWriter, (HttpServletRequest) servletRequest,
                (HttpServletResponse) servletResponse);
    }
}

FakePageContext.java

This is the main class that provides all other 'fake' objects to the calling JSP Servlet.

public class FakePageContext extends PageContext {

    private transient HashMap<String, Object> attributes;
    private final JspWriter out;
    private HttpServletRequest request;
    private HttpServletResponse response;
    private ServletContext servletContext;
    private JspApplicationContextImpl applicationContext;
    private ELContextImpl elContext;

    public FakePageContext(JspWriter out, HttpServletRequest request, HttpServletResponse response) {
        this.out = out;
        this.request = request;
        this.response = response;
        attributes = new HashMap<String, Object>(16);
    }

    public JspWriter getOut() {
        return out;
    }

    public ServletRequest getRequest() {
        return request;
    }

    public ServletResponse getResponse() {
        return response;
    }

    public ServletContext getServletContext() {
        if (servletContext == null) {
            servletContext = new FakeServletContext();
        }
        return servletContext;
    }

    public Object findAttribute(String name) {
        Object result = getAttribute(name);
        if (result == null) {
            result = getAttribute(name, REQUEST_SCOPE);
        }
        return result;
    }

    public Object getAttribute(String name) {
        return attributes.get(name);
    }

    public Object getAttribute(String name, int scope) {
        switch (scope) {
            case PAGE_SCOPE:
                return attributes.get(name);

            case REQUEST_SCOPE:
                return request.getAttribute(name);

            default:
                throw new IllegalArgumentException("Invalid scope");
        }
    }

    public int getAttributesScope(String name) {
        if (getAttribute(name) != null) {
            return PAGE_SCOPE;
        }
        if (getAttribute(name, REQUEST_SCOPE) != null) {
            return REQUEST_SCOPE;
        }
        return 0;
    }

    public void removeAttribute(String name) {
        removeAttribute(name, PAGE_SCOPE);
        removeAttribute(name, REQUEST_SCOPE);
    }

    public void removeAttribute(String name, int scope) {
        switch (scope) {
            case PAGE_SCOPE:
                attributes.remove(name);
                break;

            case REQUEST_SCOPE:
                request.removeAttribute(name);
                break;

            default:
                throw new IllegalArgumentException("Invalid scope");
        }
    }

    public void setAttribute(String name, Object attribute) {
        if (attribute != null) {
            attributes.put(name, attribute);
        } else {
            attributes.remove(name);
        }
    }

    public void setAttribute(String name, Object o, int scope) {
        if (o != null) {
            switch (scope) {
                case PAGE_SCOPE:
                    attributes.put(name, o);
                    break;

                case REQUEST_SCOPE:
                    request.setAttribute(name, o);
                    break;

                default:
                    throw new IllegalArgumentException("Invalid scope");
            }
        } else {
            removeAttribute(name, scope);
        }
    }
}

FakeServletConfig.java

public class FakeServletConfig implements ServletConfig {

    private static ServletConfig instance;
    private ServletContext servletContext;

    private FakeServletConfig(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

     public static ServletConfig getInstance() {
        if (instance == null) {
            instance = new FakeServletConfig(FakeServletContext.getInstance());
        }
        return instance;
    }

    public ServletContext getServletContext() {
        return servletContext;
    }
 }

FakeServletContext.java

public class FakeServletContext implements ServletContext {

    private static ServletContext instance;
    private transient HashMap<String, Object> attributes = new HashMap<String, Object>(16);

    public static ServletContext getInstance() {
        if (instance == null) {
            instance = new FakeServletContext();
        }
        return instance;
    }

    public Object getAttribute(String name) {
        return attributes.get(name);
    }

    public void setAttribute(String name, Object value) {
        attributes.put(name, value);
    }

    public void removeAttribute(String name) {
        attributes.remove(name);
    }

    public ServletContext getContext(String s) {
        return instance;
    }
}

FakeHttpServletRequest.java

public class FakeHttpServletRequest implements HttpServletRequest {

    private Map<String, String> parameters = new TreeMap<String, String>();
    private Map<String, Object> attributes = new TreeMap<String, Object>();

    public void setAttribute(String name, Object attribute) {
        attributes.put(name, attribute);
    }

    public Object getAttribute(String name) {
        return attributes.get(name);
    }

    public void removeAttribute(String name) {
        attributes.remove(name);
    }

    public void setParameter(String name, String value) {
        parameters.put(name, value);
    }

    public String getParameter(String name) {
        return parameters.get(name);
    }

    public Map getParameterMap() {
        return parameters;
    }
}

FakeHttpServletResponse.java

All methods in this class have default empty implementation.

Next Steps

  • Implement a Template Content Handler for JSP Layout TBBs. This would perform a compilation of the JSP code and display any potential compilation errors in the Message Center of Tridion CME;
  • Implement JSTL support, such that JSTL tags and Expression Language constructs work in JSP TBBs - e.g. <c:out value="${pageTitle}"/>;
  • Tridion Object Model Bean-ification, so that one can navigate the TOM using EL such as ${component.fields.paragraph[0].bodytext};


Comments

Popular posts from this blog

Running sp_updatestats on AWS RDS database

Part of the maintenance tasks that I perform on a MSSQL Content Manager database is to run stored procedure sp_updatestats . exec sp_updatestats However, that is not supported on an AWS RDS instance. The error message below indicates that only the sa  account can perform this: Msg 15247 , Level 16 , State 1 , Procedure sp_updatestats, Line 15 [Batch Start Line 0 ] User does not have permission to perform this action. Instead there are several posts that suggest using UPDATE STATISTICS instead: https://dba.stackexchange.com/questions/145982/sp-updatestats-vs-update-statistics I stumbled upon the following post from 2008 (!!!), https://social.msdn.microsoft.com/Forums/sqlserver/en-US/186e3db0-fe37-4c31-b017-8e7c24d19697/spupdatestats-fails-to-run-with-permission-error-under-dbopriveleged-user , which describes a way to wrap the call to sp_updatestats and execute it under a different user: create procedure dbo.sp_updstats with execute as 'dbo' as

Content Delivery Monitoring in AWS with CloudWatch

This post describes a way of monitoring a Tridion 9 combined Deployer by sending the health checks into a custom metric in CloudWatch in AWS. The same approach can also be used for other Content Delivery services. Once the metric is available in CloudWatch, we can create alarms in case the service errors out or becomes unresponsive. The overall architecture is as follows: Content Delivery service sends heartbeat (or exposes HTTP endpoint) for monitoring Monitoring Agent checks heartbeat (or HTTP health check) regularly and stores health state AWS lambda function: runs regularly reads the health state from Monitoring Agent pushes custom metrics into CloudWatch I am running the Deployer ( installation docs ) and Monitoring Agent ( installation docs ) on a t2.medium EC2 instance running CentOS on which I also installed the Systems Manager Agent (SSM Agent) ( installation docs ). In my case I have a combined Deployer that I want to monitor. This consists of an Endpoint and a

Debugging a Tridion 2011 Event System

OK, so you wrote your Tridion Event System. Now it's time to debug it. I know this is a hypothetical situtation -- your code never needs any kind of debugging ;) but indulge me... Recently, Alvin Reyes ( @nivlong ) blogged about being difficult to know how exactly to debug a Tridion Event System. More exactly, the question was " What process do I attach to for debugging even system code? ". Unfortunately, there is no simple or generic answer for it. Different events are fired by different Tridion CM modules. These modules run as different programs (or services) or run inside other programs (e.g. IIS). This means that you will need to monitor (or debug) different processes, based on which events your code handles. So the usual suspects are: dllhost.exe (or dllhost3g.exe ) - running as the MTSUser is the SDL Tridion Content Manager COM+ application and it fires events on generic TOM objects (e.g. events based on Tridion.ContentManager.Extensibility.Events.CrudEven