/* * 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. * *
* It delegates work to a groovy.text.TemplateEngine implementation
* processing HTTP requests.
*
*
helloworld.html is a headless HTML-like template
*
* <html>
* <body>
* <% 3.times { %>
* Hello World!
* <% } %>
* <br>
* </body>
* </html>
*
*
* Minimal web.xml example serving HTML-like templates
*
* <web-app>
* <servlet>
* <servlet-name>template</servlet-name>
* <servlet-class>groovy.servlet.TemplateServlet</servlet-class>
* </servlet>
* <servlet-mapping>
* <servlet-name>template</servlet-name>
* <url-pattern>*.html</url-pattern>
* </servlet-mapping>
* </web-app>
*
*
*
* By default, the TemplateServer uses the {@link groovy.text.SimpleTemplateEngine}
* which interprets JSP-like templates. The init parameter template.engine
* defines the fully qualified class name of the template to use:
*
* template.engine = [empty] - equals groovy.text.SimpleTemplateEngine * template.engine = groovy.text.SimpleTemplateEngine * template.engine = groovy.text.GStringTemplateEngine * template.engine = groovy.text.XmlTemplateEngine ** *
* This implementation provides a verbosity flag switching log statements. * The servlet init parameter name is: *
* generate.by = true(default) | false ** * @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
true if all measured values match, else false
*/
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.
*
* * 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. *
* * @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. ** Delegates to sub-init methods and parses the following parameters: *
new groovy.text.SimpleTemplateEngine() if the init parameter
* template.engine is not set by the container configuration.
*
* @param config
* Current serlvet configuration passed by the container.
*
* @return The underlying template engine or null 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.
* * 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. * *
* @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+"; charset="+encoding); 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\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]"); } } }