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:
The Utils.SetupJni4NetBridge() is a utility method that simply creates the Jni4Net Bridge, as described in my previous post Yet Another Java Mediator.
public void createJsp() {
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:
Notice the three parameters I'm sending to JspC are:
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:
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.
I implemented the following 'fake' classes:
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
}
}
- -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