/* * 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.FileInputStream; import java.io.InputStreamReader; import java.io.Reader; 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/> * <p/> * It delegates work to a <code>groovy.text.TemplateEngine</code> implementation * processing HTTP requests. * <p/> * <h4>Usage</h4> * <p/> * <code>helloworld.html</code> is a headless HTML-like template * <pre><code> * <html> * <body> * <% 3.times { %> * Hello World! * <% } %> * <br> * </body> * </html> * </code></pre> * <p/> * Minimal <code>web.xml</code> example serving HTML-like templates * <pre><code> * <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> * </code></pre> * <p/> * <h4>Template engine configuration</h4> * <p/> * <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> * <p/> * <h3>Servlet Init Parameters</h3> * <p/> * <h4>Logging and extra-output options</h4> * <p/> * <p/> * This implementation provides a verbosity flag switching log statements. * The servlet init parameter name is: * <pre> * generated.by = true(default) | false * </pre> * <p/> * <h4>Groovy Source Encoding Parameter</h4> * <p/> * <p/> * The following servlet init parameter name can be used to specify the encoding TemplateServlet will use * to read the template groovy source files: * <pre> * groovy.source.encoding * </pre> * * @author Christian Stein * @author Guillaume Laforge * @version 2.0 * @see TemplateServlet#setVariables(ServletBinding) */ 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; private String fileEncodingParamVal; private static final String GROOVY_SOURCE_ENCODING = "groovy.source.encoding"; /** * Create new TemplateServlet. */ public TemplateServlet() { this.cache = new WeakHashMap(); this.engine = null; // assigned later by init() this.generateBy = true; // may be changed by init() this.fileEncodingParamVal = null; // may be changed by init() } /** * Gets the template created by the underlying engine parsing the request. * <p/> * <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> * * @param file The HttpServletRequest. * @return The template that will produce the response text. * @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 recompilation!"); } } } 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 + "..."); } String fileEncoding = (fileEncodingParamVal != null) ? fileEncodingParamVal : System.getProperty(GROOVY_SOURCE_ENCODING); Reader reader = null; try { reader = fileEncoding == null ? new FileReader(file) : new InputStreamReader(new FileInputStream(file), fileEncoding); 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); } value = config.getInitParameter(GROOVY_SOURCE_ENCODING); if (value != null) { this.fileEncodingParamVal = value; } log("Servlet " + getClass().getName() + " initialized on " + engine.getClass()); } /** * Creates the template engine. * <p/> * 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 servlet 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/> * </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 + "; 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<!-- 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]"); } } }