blob: 1e98ddba89f6f20eb066fff2d92c46295d7cff01 [file] [log] [blame]
/*
* Copyright 2003-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package groovy.servlet;
import groovy.text.SimpleTemplateEngine;
import groovy.text.Template;
import groovy.text.TemplateEngine;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Writer;
import java.util.Date;
import java.util.Map;
import java.util.WeakHashMap;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* A generic servlet for serving (mostly HTML) templates.
*
* <p>
* It delegates work to a <code>groovy.text.TemplateEngine</code> implementation
* processing HTTP requests.
*
* <h4>Usage</h4>
*
* <code>helloworld.html</code> is a headless HTML-like template
* <pre><code>
* &lt;html&gt;
* &lt;body&gt;
* &lt;% 3.times { %&gt;
* Hello World!
* &lt;% } %&gt;
* &lt;br&gt;
* &lt;/body&gt;
* &lt;/html&gt;
* </code></pre>
*
* Minimal <code>web.xml</code> example serving HTML-like templates
* <pre><code>
* &lt;web-app&gt;
* &lt;servlet&gt;
* &lt;servlet-name&gt;template&lt;/servlet-name&gt;
* &lt;servlet-class&gt;groovy.servlet.TemplateServlet&lt;/servlet-class&gt;
* &lt;/servlet&gt;
* &lt;servlet-mapping&gt;
* &lt;servlet-name&gt;template&lt;/servlet-name&gt;
* &lt;url-pattern&gt;*.html&lt;/url-pattern&gt;
* &lt;/servlet-mapping&gt;
* &lt;/web-app&gt;
* </code></pre>
*
* <h4>Template engine configuration</h4>
*
* <p>
* By default, the TemplateServer uses the {@link groovy.text.SimpleTemplateEngine}
* which interprets JSP-like templates. The init parameter <code>template.engine</code>
* defines the fully qualified class name of the template to use:
* <pre>
* template.engine = [empty] - equals groovy.text.SimpleTemplateEngine
* template.engine = groovy.text.SimpleTemplateEngine
* template.engine = groovy.text.GStringTemplateEngine
* template.engine = groovy.text.XmlTemplateEngine
* </pre>
*
* <h4>Logging and extra-output options</h4>
*
* <p>
* This implementation provides a verbosity flag switching log statements.
* The servlet init parameter name is:
* <pre>
* generate.by = true(default) | false
* </pre>
*
* @see TemplateServlet#setVariables(ServletBinding)
*
* @author Christian Stein
* @author Guillaume Laforge
* @version 2.0
*/
public class TemplateServlet extends AbstractHttpServlet {
/**
* Simple cache entry that validates against last modified and length
* attributes of the specified file.
*
* @author Christian Stein
*/
private static class TemplateCacheEntry {
Date date;
long hit;
long lastModified;
long length;
Template template;
public TemplateCacheEntry(File file, Template template) {
this(file, template, false); // don't get time millis for sake of speed
}
public TemplateCacheEntry(File file, Template template, boolean timestamp) {
if (file == null) {
throw new NullPointerException("file");
}
if (template == null) {
throw new NullPointerException("template");
}
if (timestamp) {
this.date = new Date(System.currentTimeMillis());
} else {
this.date = null;
}
this.hit = 0;
this.lastModified = file.lastModified();
this.length = file.length();
this.template = template;
}
/**
* Checks the passed file attributes against those cached ones.
*
* @param file
* Other file handle to compare to the cached values.
* @return <code>true</code> if all measured values match, else <code>false</code>
*/
public boolean validate(File file) {
if (file == null) {
throw new NullPointerException("file");
}
if (file.lastModified() != this.lastModified) {
return false;
}
if (file.length() != this.length) {
return false;
}
hit++;
return true;
}
public String toString() {
if (date == null) {
return "Hit #" + hit;
}
return "Hit #" + hit + " since " + date;
}
}
/**
* Simple file name to template cache map.
*/
private final Map cache;
/**
* Underlying template engine used to evaluate template source files.
*/
private TemplateEngine engine;
/**
* Flag that controls the appending of the "Generated by ..." comment.
*/
private boolean generateBy;
/**
* Create new TemplateSerlvet.
*/
public TemplateServlet() {
this.cache = new WeakHashMap();
this.engine = null; // assigned later by init()
this.generateBy = true; // may be changed by init()
}
/**
* Gets the template created by the underlying engine parsing the request.
*
* <p>
* This method looks up a simple (weak) hash map for an existing template
* object that matches the source file. If the source file didn't change in
* length and its last modified stamp hasn't changed compared to a precompiled
* template object, this template is used. Otherwise, there is no or an
* invalid template object cache entry, a new one is created by the underlying
* template engine. This new instance is put to the cache for consecutive
* calls.
* </p>
*
* @return The template that will produce the response text.
* @param file
* The HttpServletRequest.
* @throws ServletException
* If the request specified an invalid template source file
*/
protected Template getTemplate(File file) throws ServletException {
String key = file.getAbsolutePath();
Template template = null;
/*
* Test cache for a valid template bound to the key.
*/
if (verbose) {
log("Looking for cached template by key \"" + key + "\"");
}
TemplateCacheEntry entry = (TemplateCacheEntry) cache.get(key);
if (entry != null) {
if (entry.validate(file)) {
if (verbose) {
log("Cache hit! " + entry);
}
template = entry.template;
} else {
if (verbose) {
log("Cached template needs recompiliation!");
}
}
} else {
if (verbose) {
log("Cache miss.");
}
}
//
// Template not cached or the source file changed - compile new template!
//
if (template == null) {
if (verbose) {
log("Creating new template from file " + file + "...");
}
FileReader reader = null;
try {
reader = new FileReader(file);
template = engine.createTemplate(reader);
} catch (Exception e) {
throw new ServletException("Creation of template failed: " + e, e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignore) {
// e.printStackTrace();
}
}
}
cache.put(key, new TemplateCacheEntry(file, template, verbose));
if (verbose) {
log("Created and added template to cache. [key=" + key + "]");
}
}
//
// Last sanity check.
//
if (template == null) {
throw new ServletException("Template is null? Should not happen here!");
}
return template;
}
/**
* Initializes the servlet from hints the container passes.
* <p>
* Delegates to sub-init methods and parses the following parameters:
* <ul>
* <li> <tt>"generatedBy"</tt> : boolean, appends "Generated by ..." to the
* HTML response text generated by this servlet.
* </li>
* </ul>
* @param config
* Passed by the servlet container.
* @throws ServletException
* if this method encountered difficulties
*
* @see TemplateServlet#initTemplateEngine(ServletConfig)
*/
public void init(ServletConfig config) throws ServletException {
super.init(config);
this.engine = initTemplateEngine(config);
if (engine == null) {
throw new ServletException("Template engine not instantiated.");
}
String value = config.getInitParameter("generated.by");
if (value != null) {
this.generateBy = Boolean.valueOf(value).booleanValue();
}
log("Servlet " + getClass().getName() + " initialized on " + engine.getClass());
}
/**
* Creates the template engine.
*
* Called by {@link TemplateServlet#init(ServletConfig)} and returns just
* <code>new groovy.text.SimpleTemplateEngine()</code> if the init parameter
* <code>template.engine</code> is not set by the container configuration.
*
* @param config
* Current serlvet configuration passed by the container.
*
* @return The underlying template engine or <code>null</code> on error.
*/
protected TemplateEngine initTemplateEngine(ServletConfig config) {
String name = config.getInitParameter("template.engine");
if (name == null) {
return new SimpleTemplateEngine();
}
try {
return (TemplateEngine) Class.forName(name).newInstance();
} catch (InstantiationException e) {
log("Could not instantiate template engine: " + name, e);
} catch (IllegalAccessException e) {
log("Could not access template engine class: " + name, e);
} catch (ClassNotFoundException e) {
log("Could not find template engine class: " + name, e);
}
return null;
}
/**
* Services the request with a response.
* <p>
* First the request is parsed for the source file uri. If the specified file
* could not be found or can not be read an error message is sent as response.
*
* </p>
* @param request
* The http request.
* @param response
* The http response.
* @throws IOException
* if an input or output error occurs while the servlet is
* handling the HTTP request
* @throws ServletException
* if the HTTP request cannot be handled
*/
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
if (verbose) {
log("Creating/getting cached template...");
}
//
// Get the template source file handle.
//
File file = super.getScriptUriAsFile(request);
String name = file.getName();
if (!file.exists()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return; // throw new IOException(file.getAbsolutePath());
}
if (!file.canRead()) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Can not read \"" + name + "\"!");
return; // throw new IOException(file.getAbsolutePath());
}
//
// Get the requested template.
//
long getMillis = System.currentTimeMillis();
Template template = getTemplate(file);
getMillis = System.currentTimeMillis() - getMillis;
//
// Create new binding for the current request.
//
ServletBinding binding = new ServletBinding(request, response, servletContext);
setVariables(binding);
//
// Prepare the response buffer content type _before_ getting the writer.
// and set status code to ok
//
response.setContentType(CONTENT_TYPE_TEXT_HTML);
response.setStatus(HttpServletResponse.SC_OK);
//
// Get the output stream writer from the binding.
//
Writer out = (Writer) binding.getVariable("out");
if (out == null) {
out = response.getWriter();
}
//
// Evaluate the template.
//
if (verbose) {
log("Making template \"" + name + "\"...");
}
// String made = template.make(binding.getVariables()).toString();
// log(" = " + made);
long makeMillis = System.currentTimeMillis();
template.make(binding.getVariables()).writeTo(out);
makeMillis = System.currentTimeMillis() - makeMillis;
if (generateBy) {
StringBuffer sb = new StringBuffer(100);
sb.append("\n<!-- Generated by Groovy TemplateServlet [create/get=");
sb.append(Long.toString(getMillis));
sb.append(" ms, make=");
sb.append(Long.toString(makeMillis));
sb.append(" ms] -->\n");
out.write(sb.toString());
}
//
// flush the response buffer.
//
response.flushBuffer();
if (verbose) {
log("Template \"" + name + "\" request responded. [create/get=" + getMillis + " ms, make=" + makeMillis + " ms]");
}
}
/**
* Override this method to set your variables to the Groovy binding.
* <p>
* All variables bound the binding are passed to the template source text,
* e.g. the HTML file, when the template is merged.
* </p>
* <p>
* The binding provided by TemplateServlet does already include some default
* variables. As of this writing, they are (copied from
* {@link groovy.servlet.ServletBinding}):
* <ul>
* <li><tt>"request"</tt> : HttpServletRequest </li>
* <li><tt>"response"</tt> : HttpServletResponse </li>
* <li><tt>"context"</tt> : ServletContext </li>
* <li><tt>"application"</tt> : ServletContext </li>
* <li><tt>"session"</tt> : request.getSession(<b>false</b>) </li>
* </ul>
* </p>
* <p>
* And via implicite hard-coded keywords:
* <ul>
* <li><tt>"out"</tt> : response.getWriter() </li>
* <li><tt>"sout"</tt> : response.getOutputStream() </li>
* <li><tt>"html"</tt> : new MarkupBuilder(response.getWriter()) </li>
* </ul>
* </p>
*
* <p>Example binding all servlet context variables:
* <pre><code>
* class Mytlet extends TemplateServlet {
*
* protected void setVariables(ServletBinding binding) {
* // Bind a simple variable
* binding.setVariable("answer", new Long(42));
*
* // Bind all servlet context attributes...
* ServletContext context = (ServletContext) binding.getVariable("context");
* Enumeration enumeration = context.getAttributeNames();
* while (enumeration.hasMoreElements()) {
* String name = (String) enumeration.nextElement();
* binding.setVariable(name, context.getAttribute(name));
* }
* }
*
* }
* <code></pre>
* </p>
*
* @param binding
* to be modified
*/
protected void setVariables(ServletBinding binding) {
// empty
}
}