PageRenderTime 38ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 1ms

/subprojects/groovy-servlet/src/main/java/groovy/servlet/TemplateServlet.java

https://github.com/groovy/groovy-core
Java | 506 lines | 238 code | 46 blank | 222 comment | 57 complexity | 6af9e1ae721ac65d009623113ad624f4 MD5 | raw file
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. package groovy.servlet;
  20. import groovy.text.SimpleTemplateEngine;
  21. import groovy.text.Template;
  22. import groovy.text.TemplateEngine;
  23. import java.io.File;
  24. import java.io.FileInputStream;
  25. import java.io.IOException;
  26. import java.io.InputStream;
  27. import java.io.InputStreamReader;
  28. import java.io.Reader;
  29. import java.io.Writer;
  30. import java.net.URL;
  31. import java.util.Date;
  32. import java.util.Map;
  33. import java.util.WeakHashMap;
  34. import javax.servlet.ServletConfig;
  35. import javax.servlet.ServletException;
  36. import javax.servlet.http.HttpServletRequest;
  37. import javax.servlet.http.HttpServletResponse;
  38. /**
  39. * A generic servlet for serving (mostly HTML) templates.
  40. * <p>
  41. * It delegates work to a <code>groovy.text.TemplateEngine</code> implementation
  42. * processing HTTP requests.
  43. * <p>
  44. * <h4>Usage</h4>
  45. * <p>
  46. * <code>helloworld.html</code> is a headless HTML-like template
  47. * <pre><code>
  48. * &lt;html&gt;
  49. * &lt;body&gt;
  50. * &lt;% 3.times { %&gt;
  51. * Hello World!
  52. * &lt;% } %&gt;
  53. * &lt;br&gt;
  54. * &lt;/body&gt;
  55. * &lt;/html&gt;
  56. * </code></pre>
  57. * <p>
  58. * Minimal <code>web.xml</code> example serving HTML-like templates
  59. * <pre><code>
  60. * &lt;web-app&gt;
  61. * &lt;servlet&gt;
  62. * &lt;servlet-name&gt;template&lt;/servlet-name&gt;
  63. * &lt;servlet-class&gt;groovy.servlet.TemplateServlet&lt;/servlet-class&gt;
  64. * &lt;/servlet&gt;
  65. * &lt;servlet-mapping&gt;
  66. * &lt;servlet-name&gt;template&lt;/servlet-name&gt;
  67. * &lt;url-pattern&gt;*.html&lt;/url-pattern&gt;
  68. * &lt;/servlet-mapping&gt;
  69. * &lt;/web-app&gt;
  70. * </code></pre>
  71. * <p>
  72. * <h4>Template engine configuration</h4>
  73. * <p>
  74. * By default, the TemplateServer uses the {@link groovy.text.SimpleTemplateEngine}
  75. * which interprets JSP-like templates. The init parameter <code>template.engine</code>
  76. * defines the fully qualified class name of the template to use:
  77. * <pre>
  78. * template.engine = [empty] - equals groovy.text.SimpleTemplateEngine
  79. * template.engine = groovy.text.SimpleTemplateEngine
  80. * template.engine = groovy.text.GStringTemplateEngine
  81. * template.engine = groovy.text.XmlTemplateEngine
  82. * </pre>
  83. * <p>
  84. * <h3>Servlet Init Parameters</h3>
  85. * <p>
  86. * <h4>Logging and extra-output options</h4>
  87. * <p>
  88. * This implementation provides a verbosity flag switching log statements.
  89. * The servlet init parameter name is:
  90. * <pre>
  91. * generated.by = true(default) | false
  92. * </pre>
  93. * <p>
  94. * <h4>Groovy Source Encoding Parameter</h4>
  95. * <p>
  96. * The following servlet init parameter name can be used to specify the encoding TemplateServlet will use
  97. * to read the template groovy source files:
  98. * <pre>
  99. * groovy.source.encoding
  100. * </pre>
  101. *
  102. * @author Christian Stein
  103. * @author Guillaume Laforge
  104. * @version 2.0
  105. * @see TemplateServlet#setVariables(ServletBinding)
  106. */
  107. public class TemplateServlet extends AbstractHttpServlet {
  108. /**
  109. * Simple cache entry. If a file is supplied, then the entry is validated against
  110. * last modified and length attributes of the specified file.
  111. *
  112. * @author Christian Stein
  113. */
  114. private static class TemplateCacheEntry {
  115. Date date;
  116. long hit;
  117. long lastModified;
  118. long length;
  119. Template template;
  120. public TemplateCacheEntry(File file, Template template) {
  121. this(file, template, false); // don't get time millis for sake of speed
  122. }
  123. public TemplateCacheEntry(File file, Template template, boolean timestamp) {
  124. if (template == null) {
  125. throw new NullPointerException("template");
  126. }
  127. if (timestamp) {
  128. this.date = new Date(System.currentTimeMillis());
  129. } else {
  130. this.date = null;
  131. }
  132. this.hit = 0;
  133. if (file != null) {
  134. this.lastModified = file.lastModified();
  135. this.length = file.length();
  136. }
  137. this.template = template;
  138. }
  139. /**
  140. * Checks the passed file attributes against those cached ones.
  141. *
  142. * @param file Other file handle to compare to the cached values. May be null in which case the validation is skipped.
  143. * @return <code>true</code> if all measured values match, else <code>false</code>
  144. */
  145. public boolean validate(File file) {
  146. if (file != null) {
  147. if (file.lastModified() != this.lastModified) {
  148. return false;
  149. }
  150. if (file.length() != this.length) {
  151. return false;
  152. }
  153. }
  154. hit++;
  155. return true;
  156. }
  157. public String toString() {
  158. if (date == null) {
  159. return "Hit #" + hit;
  160. }
  161. return "Hit #" + hit + " since " + date;
  162. }
  163. }
  164. /**
  165. * Simple file name to template cache map.
  166. */
  167. private final Map<String, TemplateCacheEntry> cache;
  168. /**
  169. * Underlying template engine used to evaluate template source files.
  170. */
  171. private TemplateEngine engine;
  172. /**
  173. * Flag that controls the appending of the "Generated by ..." comment.
  174. */
  175. private boolean generateBy;
  176. private String fileEncodingParamVal;
  177. private static final String GROOVY_SOURCE_ENCODING = "groovy.source.encoding";
  178. /**
  179. * Create new TemplateServlet.
  180. */
  181. public TemplateServlet() {
  182. this.cache = new WeakHashMap<String, TemplateCacheEntry>();
  183. this.engine = null; // assigned later by init()
  184. this.generateBy = true; // may be changed by init()
  185. this.fileEncodingParamVal = null; // may be changed by init()
  186. }
  187. /**
  188. * Find a cached template for a given key. If a <code>File</code> is passed then
  189. * any cached object is validated against the File to determine if it is out of
  190. * date
  191. * @param key a unique key for the template, such as a file's absolutePath or a URL.
  192. * @param file a file to be used to determine if the cached template is stale. May be null.
  193. * @return The cached template, or null if there was no cached entry, or the entry was stale.
  194. */
  195. private Template findCachedTemplate(String key, File file) {
  196. Template template = null;
  197. /*
  198. * Test cache for a valid template bound to the key.
  199. */
  200. if (verbose) {
  201. log("Looking for cached template by key \"" + key + "\"");
  202. }
  203. TemplateCacheEntry entry = (TemplateCacheEntry) cache.get(key);
  204. if (entry != null) {
  205. if (entry.validate(file)) {
  206. if (verbose) {
  207. log("Cache hit! " + entry);
  208. }
  209. template = entry.template;
  210. } else {
  211. if (verbose) {
  212. log("Cached template " + key + " needs recompiliation! " + entry);
  213. }
  214. }
  215. } else {
  216. if (verbose) {
  217. log("Cache miss for " + key);
  218. }
  219. }
  220. return template;
  221. }
  222. /**
  223. * Compile the template and store it in the cache.
  224. * @param key a unique key for the template, such as a file's absolutePath or a URL.
  225. * @param reader a reader for the template's source.
  226. * @param file a file to be used to determine if the cached template is stale. May be null.
  227. * @return the created template.
  228. * @throws Exception Any exception when creating the template.
  229. */
  230. private Template createAndStoreTemplate(String key, InputStream inputStream, File file) throws Exception {
  231. if (verbose) {
  232. log("Creating new template from " + key + "...");
  233. }
  234. Reader reader = null;
  235. try {
  236. String fileEncoding = (fileEncodingParamVal != null) ? fileEncodingParamVal :
  237. System.getProperty(GROOVY_SOURCE_ENCODING);
  238. reader = fileEncoding == null ? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, fileEncoding);
  239. Template template = engine.createTemplate(reader);
  240. cache.put(key, new TemplateCacheEntry(file, template, verbose));
  241. if (verbose) {
  242. log("Created and added template to cache. [key=" + key + "] " + cache.get(key));
  243. }
  244. //
  245. // Last sanity check.
  246. //
  247. if (template == null) {
  248. throw new ServletException("Template is null? Should not happen here!");
  249. }
  250. return template;
  251. } finally {
  252. if (reader != null) {
  253. reader.close();
  254. } else if (inputStream != null) {
  255. inputStream.close();
  256. }
  257. }
  258. }
  259. /**
  260. * Gets the template created by the underlying engine parsing the request.
  261. *
  262. * <p>
  263. * This method looks up a simple (weak) hash map for an existing template
  264. * object that matches the source file. If the source file didn't change in
  265. * length and its last modified stamp hasn't changed compared to a precompiled
  266. * template object, this template is used. Otherwise, there is no or an
  267. * invalid template object cache entry, a new one is created by the underlying
  268. * template engine. This new instance is put to the cache for consecutive
  269. * calls.
  270. *
  271. * @return The template that will produce the response text.
  272. * @param file The file containing the template source.
  273. * @throws ServletException If the request specified an invalid template source file
  274. */
  275. protected Template getTemplate(File file) throws ServletException {
  276. String key = file.getAbsolutePath();
  277. Template template = findCachedTemplate(key, file);
  278. //
  279. // Template not cached or the source file changed - compile new template!
  280. //
  281. if (template == null) {
  282. try {
  283. template = createAndStoreTemplate(key, new FileInputStream(file), file);
  284. } catch (Exception e) {
  285. throw new ServletException("Creation of template failed: " + e, e);
  286. }
  287. }
  288. return template;
  289. }
  290. /**
  291. * Gets the template created by the underlying engine parsing the request.
  292. *
  293. * <p>
  294. * This method looks up a simple (weak) hash map for an existing template
  295. * object that matches the source URL. If there is no cache entry, a new one is
  296. * created by the underlying template engine. This new instance is put
  297. * to the cache for consecutive calls.
  298. *
  299. * @return The template that will produce the response text.
  300. * @param url The URL containing the template source..
  301. * @throws ServletException If the request specified an invalid template source URL
  302. */
  303. protected Template getTemplate(URL url) throws ServletException {
  304. String key = url.toString();
  305. Template template = findCachedTemplate(key, null);
  306. // Template not cached or the source file changed - compile new template!
  307. if (template == null) {
  308. try {
  309. template = createAndStoreTemplate(key, url.openConnection().getInputStream(), null);
  310. } catch (Exception e) {
  311. throw new ServletException("Creation of template failed: " + e, e);
  312. }
  313. }
  314. return template;
  315. }
  316. /**
  317. * Initializes the servlet from hints the container passes.
  318. * <p>
  319. * Delegates to sub-init methods and parses the following parameters:
  320. * <ul>
  321. * <li> <tt>"generatedBy"</tt> : boolean, appends "Generated by ..." to the
  322. * HTML response text generated by this servlet.
  323. * </li>
  324. * </ul>
  325. *
  326. * @param config Passed by the servlet container.
  327. * @throws ServletException if this method encountered difficulties
  328. * @see TemplateServlet#initTemplateEngine(ServletConfig)
  329. */
  330. public void init(ServletConfig config) throws ServletException {
  331. super.init(config);
  332. this.engine = initTemplateEngine(config);
  333. if (engine == null) {
  334. throw new ServletException("Template engine not instantiated.");
  335. }
  336. String value = config.getInitParameter("generated.by");
  337. if (value != null) {
  338. this.generateBy = Boolean.valueOf(value);
  339. }
  340. value = config.getInitParameter(GROOVY_SOURCE_ENCODING);
  341. if (value != null) {
  342. this.fileEncodingParamVal = value;
  343. }
  344. log("Servlet " + getClass().getName() + " initialized on " + engine.getClass());
  345. }
  346. /**
  347. * Creates the template engine.
  348. * <p>
  349. * Called by {@link TemplateServlet#init(ServletConfig)} and returns just
  350. * <code>new groovy.text.SimpleTemplateEngine()</code> if the init parameter
  351. * <code>template.engine</code> is not set by the container configuration.
  352. *
  353. * @param config Current servlet configuration passed by the container.
  354. * @return The underlying template engine or <code>null</code> on error.
  355. */
  356. protected TemplateEngine initTemplateEngine(ServletConfig config) {
  357. String name = config.getInitParameter("template.engine");
  358. if (name == null) {
  359. return new SimpleTemplateEngine();
  360. }
  361. try {
  362. return (TemplateEngine) Class.forName(name).newInstance();
  363. } catch (InstantiationException e) {
  364. log("Could not instantiate template engine: " + name, e);
  365. } catch (IllegalAccessException e) {
  366. log("Could not access template engine class: " + name, e);
  367. } catch (ClassNotFoundException e) {
  368. log("Could not find template engine class: " + name, e);
  369. }
  370. return null;
  371. }
  372. /**
  373. * Services the request with a response.
  374. * <p>
  375. * First the request is parsed for the source file uri. If the specified file
  376. * could not be found or can not be read an error message is sent as response.
  377. *
  378. * @param request The http request.
  379. * @param response The http response.
  380. * @throws IOException if an input or output error occurs while the servlet is handling the HTTP request
  381. * @throws ServletException if the HTTP request cannot be handled
  382. */
  383. public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  384. if (verbose) {
  385. log("Creating/getting cached template...");
  386. }
  387. //
  388. // Get the template source file handle.
  389. //
  390. Template template;
  391. long getMillis;
  392. String name;
  393. File file = super.getScriptUriAsFile(request);
  394. if (file != null) {
  395. name = file.getName();
  396. if (!file.exists()) {
  397. response.sendError(HttpServletResponse.SC_NOT_FOUND);
  398. return; // throw new IOException(file.getAbsolutePath());
  399. }
  400. if (!file.canRead()) {
  401. response.sendError(HttpServletResponse.SC_FORBIDDEN, "Can not read \"" + name + "\"!");
  402. return; // throw new IOException(file.getAbsolutePath());
  403. }
  404. getMillis = System.currentTimeMillis();
  405. template = getTemplate(file);
  406. getMillis = System.currentTimeMillis() - getMillis;
  407. } else {
  408. name = super.getScriptUri(request);
  409. URL url = servletContext.getResource(name);
  410. getMillis = System.currentTimeMillis();
  411. template = getTemplate(url);
  412. getMillis = System.currentTimeMillis() - getMillis;
  413. }
  414. //
  415. // Create new binding for the current request.
  416. //
  417. ServletBinding binding = new ServletBinding(request, response, servletContext);
  418. setVariables(binding);
  419. //
  420. // Prepare the response buffer content type _before_ getting the writer.
  421. // and set status code to ok
  422. //
  423. response.setContentType(CONTENT_TYPE_TEXT_HTML + "; charset=" + encoding);
  424. response.setStatus(HttpServletResponse.SC_OK);
  425. //
  426. // Get the output stream writer from the binding.
  427. //
  428. Writer out = (Writer) binding.getVariable("out");
  429. if (out == null) {
  430. out = response.getWriter();
  431. }
  432. //
  433. // Evaluate the template.
  434. //
  435. if (verbose) {
  436. log("Making template \"" + name + "\"...");
  437. }
  438. // String made = template.make(binding.getVariables()).toString();
  439. // log(" = " + made);
  440. long makeMillis = System.currentTimeMillis();
  441. template.make(binding.getVariables()).writeTo(out);
  442. makeMillis = System.currentTimeMillis() - makeMillis;
  443. if (generateBy) {
  444. StringBuilder sb = new StringBuilder(100);
  445. sb.append("\n<!-- Generated by Groovy TemplateServlet [create/get=");
  446. sb.append(Long.toString(getMillis));
  447. sb.append(" ms, make=");
  448. sb.append(Long.toString(makeMillis));
  449. sb.append(" ms] -->\n");
  450. out.write(sb.toString());
  451. }
  452. //
  453. // flush the response buffer.
  454. //
  455. response.flushBuffer();
  456. if (verbose) {
  457. log("Template \"" + name + "\" request responded. [create/get=" + getMillis + " ms, make=" + makeMillis + " ms]");
  458. }
  459. }
  460. }