001: /*
002: * Copyright (c) 2003 The Visigoth Software Society. All rights
003: * reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions
007: * are met:
008: *
009: * 1. Redistributions of source code must retain the above copyright
010: * notice, this list of conditions and the following disclaimer.
011: *
012: * 2. Redistributions in binary form must reproduce the above copyright
013: * notice, this list of conditions and the following disclaimer in
014: * the documentation and/or other materials provided with the
015: * distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowledgement:
019: * "This product includes software developed by the
020: * Visigoth Software Society (http://www.visigoths.org/)."
021: * Alternately, this acknowledgement may appear in the software itself,
022: * if and wherever such third-party acknowledgements normally appear.
023: *
024: * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
025: * project contributors may be used to endorse or promote products derived
026: * from this software without prior written permission. For written
027: * permission, please contact visigoths@visigoths.org.
028: *
029: * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
030: * nor may "FreeMarker" or "Visigoth" appear in their names
031: * without prior written permission of the Visigoth Software Society.
032: *
033: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
034: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
035: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036: * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
037: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
038: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
039: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
040: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
042: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
043: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
044: * SUCH DAMAGE.
045: * ====================================================================
046: *
047: * This software consists of voluntary contributions made by many
048: * individuals on behalf of the Visigoth Software Society. For more
049: * information on the Visigoth Software Society, please see
050: * http://www.visigoths.org/
051: */
052:
053: package freemarker.cache;
054:
055: import java.io.IOException;
056: import java.io.Reader;
057: import java.io.StringWriter;
058: import java.util.ArrayList;
059: import java.util.List;
060: import java.util.Locale;
061: import java.util.StringTokenizer;
062:
063: import freemarker.core.Environment;
064: import freemarker.log.Logger;
065: import freemarker.template.Configuration;
066: import freemarker.template.Template;
067:
068: /**
069: * A class that performs caching and on-demand loading of the templates.
070: * The actual loading is delegated to a {@link TemplateLoader}. Also,
071: * various constructors provide you with convenient caches with predefined
072: * behavior. Typically you don't use this class directly - in normal
073: * circumstances it is hidden behind a {@link Configuration}.
074: * @author Attila Szegedi, szegedia at freemail dot hu
075: * @version $Id: TemplateCache.java,v 1.62.2.2 2007/04/03 18:06:07 szegedia Exp $
076: */
077: public class TemplateCache {
078: private static final String ASTERISKSTR = "*";
079: private static final String LOCALE_SEPARATOR = "_";
080: private static final char ASTERISK = '*';
081: private static final String CURRENT_DIR_PATH_PREFIX = "./";
082: private static final String CURRENT_DIR_PATH = "/./";
083: private static final String PARENT_DIR_PATH_PREFIX = "../";
084: private static final String PARENT_DIR_PATH = "/../";
085: private static final char SLASH = '/';
086: private static final Logger logger = Logger
087: .getLogger("freemarker.cache");
088:
089: private final TemplateLoader mainLoader;
090: /** DD: [noFallback]
091: ,
092: fallbackTemplateLoader = new ClassTemplateLoader(TemplateCache.class, "/");
093: */
094: /** Here we keep our cached templates */
095: private final CacheStorage storage;
096: /** The default refresh delay in milliseconds. */
097: private long delay = 5000;
098: /** Specifies if localized template lookup is enabled or not */
099: private boolean localizedLookup = true;
100:
101: private Configuration config;
102:
103: /**
104: * Returns a template cache that will first try to load a template from
105: * the file system relative to the current user directory (i.e. the value
106: * of the system property <code>user.dir</code>), then from the classpath.
107: * This default template cache suits many applications.
108: */
109: public TemplateCache() {
110: this (createDefaultTemplateLoader());
111: }
112:
113: private static TemplateLoader createDefaultTemplateLoader() {
114: try {
115: return new FileTemplateLoader();
116: } catch (Exception e) {
117: logger
118: .warn(
119: "Could not create a file template loader for current directory",
120: e);
121: return null;
122: }
123: }
124:
125: /**
126: * Creates a new template cache with a custom template loader that is used
127: * to load the templates.
128: * @param loader the template loader to use.
129: */
130: public TemplateCache(TemplateLoader loader) {
131: this (loader, new MruCacheStorage(0, Integer.MAX_VALUE));
132: }
133:
134: /**
135: * Creates a new template cache with a custom template loader that is used
136: * to load the templates.
137: * @param loader the template loader to use.
138: */
139: public TemplateCache(TemplateLoader loader, CacheStorage storage) {
140: this .mainLoader = loader;
141: this .storage = storage;
142: if (storage == null) {
143: throw new IllegalArgumentException("storage == null");
144: }
145: }
146:
147: /**
148: * Sets the configuration object to which this cache belongs. This
149: * method is called by the configuration itself to establish the
150: * relation, and should not be called by users.
151: */
152: public void setConfiguration(Configuration config) {
153: this .config = config;
154: clear();
155: }
156:
157: public TemplateLoader getTemplateLoader() {
158: return mainLoader;
159: }
160:
161: public CacheStorage getCacheStorage() {
162: return storage;
163: }
164:
165: /**
166: * Loads a template with the given name, in the specified locale and
167: * using the specified character encoding.
168: *
169: * @param name the name of the template. Can't be null. The exact syntax of the name
170: * is interpreted by the underlying {@link TemplateLoader}, but the
171: * cache makes some assumptions. First, the name is expected to be
172: * a hierarchical path, with path components separated by a slash
173: * character (not with backslash!). The path (the name) must <em>not</em> begin with slash;
174: * the path is always relative to the "template root directory".
175: * Then, the <tt>..</tt> and <tt>.</tt> path metaelements will be resolved.
176: * For example, if the name is <tt>a/../b/./c.ftl</tt>, then it will be
177: * simplified to <tt>b/c.ftl</tt>. The rules regarding this are same as with conventional
178: * UN*X paths. The path must not reach outside the template root directory, that is,
179: * it can't be something like <tt>"../templates/my.ftl"</tt> (not even if the pervious path
180: * happens to be equivalent with <tt>"/my.ftl"</tt>).
181: * Further, the path is allowed to contain at most
182: * one path element whose name is <tt>*</tt> (asterisk). This path metaelement triggers the
183: * <i>acquisition mechanism</i>. If the template is not found in
184: * the location described by the concatenation of the path left to the
185: * asterisk (called base path) and the part to the right of the asterisk
186: * (called resource path), the cache will attempt to remove the rightmost
187: * path component from the base path ("go up one directory") and concatenate
188: * that with the resource path. The process is repeated until either a
189: * template is found, or the base path is completely exhausted.
190: *
191: * @param locale the requested locale of the template. Can't be null.
192: * Assuming you have specified <code>en_US</code> as the locale and
193: * <code>myTemplate.html</code> as the name of the template, the cache will
194: * first try to retrieve <code>myTemplate_en_US.html</code>, then
195: * <code>myTemplate.html_en.html</code>, and finally
196: * <code>myTemplate.html</code>.
197: *
198: * @param encoding the character encoding used to interpret the template
199: * source bytes. Can't be null.
200: *
201: * @param parse if true, the loaded template is parsed and interpreted
202: * as a regular FreeMarker template. If false, the loaded template is
203: * treated as an unparsed block of text.
204: *
205: * @return the loaded template, or null if the template is not found.
206: */
207: public Template getTemplate(String name, Locale locale,
208: String encoding, boolean parse) throws IOException {
209: if (name == null) {
210: throw new IllegalArgumentException(
211: "Argument \"name\" can't be null");
212: }
213: if (locale == null) {
214: throw new IllegalArgumentException(
215: "Argument \"locale\" can't be null");
216: }
217: if (encoding == null) {
218: throw new IllegalArgumentException(
219: "Argument \"encoding\" can't be null");
220: }
221: name = normalizeName(name);
222: if (name == null) {
223: return null;
224: }
225: Template result = null;
226: if (mainLoader != null) {
227: result = getTemplate(mainLoader, name, locale, encoding,
228: parse);
229: }
230: /** DD: [noFallback]
231: if (result == null && name.toLowerCase().endsWith(".ftl")) {
232: result = getTemplate(fallbackTemplateLoader, name, locale, encoding, parse);
233: }
234: */
235: return result;
236: }
237:
238: private Template getTemplate(TemplateLoader loader, String name,
239: Locale locale, String encoding, boolean parse)
240: throws IOException {
241: boolean debug = logger.isDebugEnabled();
242: String debugName = debug ? name + "[" + locale + "," + encoding
243: + (parse ? ",parsed] " : ",unparsed] ") : null;
244: TemplateKey tk = new TemplateKey(name, locale, encoding, parse);
245: synchronized (storage) {
246: CachedTemplate cachedTemplate = (CachedTemplate) storage
247: .get(tk);
248: long now = System.currentTimeMillis();
249: long lastModified = -1L;
250: Object newlyFoundSource = null;
251: try {
252: if (cachedTemplate != null) {
253: // If we're within the refresh delay, return the cached copy
254: if (now - cachedTemplate.lastChecked < delay) {
255: if (debug) {
256: logger
257: .debug(debugName
258: + "cached copy not yet stale; using cached.");
259: }
260: return cachedTemplate.template;
261: }
262: // Else, update the last-checked flag
263: cachedTemplate.lastChecked = now;
264:
265: // Find the template source
266: newlyFoundSource = findTemplateSource(name, locale);
267:
268: // Template source was removed
269: if (newlyFoundSource == null) {
270: if (debug) {
271: logger
272: .debug(debugName
273: + "no source found (removing from cache if it was cached).");
274: }
275: storage.remove(tk);
276: return null;
277: }
278:
279: // If the source didn't change and its last modified date
280: // also didn't change, return the cached version.
281: lastModified = loader
282: .getLastModified(newlyFoundSource);
283: boolean lastModifiedNotChanged = lastModified == cachedTemplate.lastModified;
284: boolean sourceEquals = newlyFoundSource
285: .equals(cachedTemplate.source);
286: if (lastModifiedNotChanged && sourceEquals) {
287: if (debug) {
288: logger.debug(debugName
289: + "using cached since "
290: + newlyFoundSource
291: + " didn't change.");
292: }
293: cachedTemplate.lastChecked = now;
294: return cachedTemplate.template;
295: } else {
296: if (debug && !sourceEquals) {
297: logger
298: .debug("Updating source, info for cause: "
299: + "sourceEquals="
300: + sourceEquals
301: + ", newlyFoundSource="
302: + newlyFoundSource
303: + ", cachedTemplate.source="
304: + cachedTemplate.source);
305: }
306: if (debug && !lastModifiedNotChanged) {
307: logger
308: .debug("Updating source, info for cause: "
309: + "lastModifiedNotChanged="
310: + lastModifiedNotChanged
311: + ", cache lastModified="
312: + cachedTemplate.lastModified
313: + " != file lastModified="
314: + lastModified);
315: }
316: // Update the source
317: cachedTemplate.source = newlyFoundSource;
318: }
319: } else {
320: if (debug) {
321: logger
322: .debug("Could not find template in cache, "
323: + "creating new one; id=["
324: + tk.name
325: + "["
326: + tk.locale
327: + ","
328: + tk.encoding
329: + (tk.parse ? ",parsed] "
330: : ",unparsed] ") + "]");
331: }
332:
333: // Construct a new CachedTemplate entry. Note we set the
334: // cachedTemplate.lastModified to Long.MIN_VALUE. This is
335: // a flag that signs it has to be explicitly queried later on.
336: newlyFoundSource = findTemplateSource(name, locale);
337: if (newlyFoundSource == null) {
338: return null;
339: }
340: cachedTemplate = new CachedTemplate();
341: cachedTemplate.source = newlyFoundSource;
342: cachedTemplate.lastChecked = now;
343: cachedTemplate.lastModified = lastModified = Long.MIN_VALUE;
344: storage.put(tk, cachedTemplate);
345: }
346: if (debug) {
347: logger.debug("Compiling FreeMarker template "
348: + debugName + " from " + newlyFoundSource);
349: }
350: // If we get here, then we need to (re)load the template
351: Object source = cachedTemplate.source;
352: cachedTemplate.template = loadTemplate(loader, name,
353: locale, encoding, parse, source);
354: cachedTemplate.lastModified = lastModified == Long.MIN_VALUE ? loader
355: .getLastModified(source)
356: : lastModified;
357: return cachedTemplate.template;
358: } finally {
359: if (newlyFoundSource != null) {
360: loader.closeTemplateSource(newlyFoundSource);
361: }
362: }
363: }
364: }
365:
366: private Template loadTemplate(TemplateLoader loader, String name,
367: Locale locale, String encoding, boolean parse, Object source)
368: throws IOException {
369: Template template;
370: Reader reader = loader.getReader(source, encoding);
371: try {
372: if (parse) {
373: try {
374: template = new Template(name, reader, config,
375: encoding);
376: } catch (Template.WrongEncodingException wee) {
377: encoding = wee.specifiedEncoding;
378: reader = loader.getReader(source, encoding);
379: template = new Template(name, reader, config,
380: encoding);
381: }
382: template.setLocale(locale);
383: } else {
384: // Read the contents into a StringWriter, then construct a single-textblock
385: // template from it.
386: StringWriter sw = new StringWriter();
387: char[] buf = new char[4096];
388: for (;;) {
389: int charsRead = reader.read(buf);
390: if (charsRead > 0) {
391: sw.write(buf, 0, charsRead);
392: } else if (charsRead == -1) {
393: break;
394: }
395: }
396: template = Template.getPlainTextTemplate(name, sw
397: .toString(), config);
398: template.setLocale(locale);
399: }
400: template.setEncoding(encoding);
401: } finally {
402: reader.close();
403: }
404: return template;
405: }
406:
407: /**
408: * Gets the delay in milliseconds between checking for newer versions of a
409: * template source.
410: * @return the current value of the delay
411: */
412: public synchronized long getDelay() {
413: return delay;
414: }
415:
416: /**
417: * Sets the delay in milliseconds between checking for newer versions of a
418: * template sources.
419: * @param delay the new value of the delay
420: */
421: public synchronized void setDelay(long delay) {
422: this .delay = delay;
423: }
424:
425: /**
426: * Returns if localized template lookup is enabled or not.
427: */
428: public synchronized boolean getLocalizedLookup() {
429: return localizedLookup;
430: }
431:
432: /**
433: * Setis if localized template lookup is enabled or not.
434: */
435: public synchronized void setLocalizedLookup(boolean localizedLookup) {
436: this .localizedLookup = localizedLookup;
437: }
438:
439: /**
440: * Removes all entries from the cache, forcing reloading of templates
441: * on subsequent {@link #getTemplate(String, Locale, String, boolean)}
442: * calls. If the configured template loader is
443: * {@link StatefulTemplateLoader stateful}, then its
444: * {@link StatefulTemplateLoader#resetState()} method is invoked as well.
445: */
446: public void clear() {
447: synchronized (storage) {
448: storage.clear();
449: if (mainLoader instanceof StatefulTemplateLoader) {
450: ((StatefulTemplateLoader) mainLoader).resetState();
451: }
452: }
453: }
454:
455: public static String getFullTemplatePath(Environment env,
456: String parentTemplateDir, String templateNameString) {
457: if (!env.isClassicCompatible()) {
458: if (templateNameString.indexOf("://") > 0) {
459: ;
460: } else if (templateNameString.length() > 0
461: && templateNameString.charAt(0) == '/') {
462: int protIndex = parentTemplateDir.indexOf("://");
463: if (protIndex > 0) {
464: templateNameString = parentTemplateDir.substring(0,
465: protIndex + 2)
466: + templateNameString;
467: } else {
468: templateNameString = templateNameString
469: .substring(1);
470: }
471: } else {
472: templateNameString = parentTemplateDir
473: + templateNameString;
474: }
475: }
476: return templateNameString;
477: }
478:
479: private Object findTemplateSource(String name, Locale locale)
480: throws IOException {
481: if (localizedLookup) {
482: int lastDot = name.lastIndexOf('.');
483: String prefix = lastDot == -1 ? name : name.substring(0,
484: lastDot);
485: String suffix = lastDot == -1 ? "" : name
486: .substring(lastDot);
487: String localeName = LOCALE_SEPARATOR + locale.toString();
488: StringBuffer buf = new StringBuffer(name.length()
489: + localeName.length());
490: buf.append(prefix);
491: for (;;) {
492: buf.setLength(prefix.length());
493: String path = buf.append(localeName).append(suffix)
494: .toString();
495: Object templateSource = acquireTemplateSource(path);
496: if (templateSource != null) {
497: return templateSource;
498: }
499: int lastUnderscore = localeName.lastIndexOf('_');
500: if (lastUnderscore == -1)
501: break;
502: localeName = localeName.substring(0, lastUnderscore);
503: }
504: return null;
505: } else {
506: return acquireTemplateSource(name);
507: }
508: }
509:
510: private Object acquireTemplateSource(String path)
511: throws IOException {
512: int asterisk = path.indexOf(ASTERISK);
513: // Shortcut in case there is no acquisition
514: if (asterisk == -1) {
515: return mainLoader.findTemplateSource(path);
516: }
517: StringTokenizer tok = new StringTokenizer(path, "/");
518: int lastAsterisk = -1;
519: List tokpath = new ArrayList();
520: while (tok.hasMoreTokens()) {
521: String pathToken = tok.nextToken();
522: if (pathToken.equals(ASTERISKSTR)) {
523: if (lastAsterisk != -1) {
524: tokpath.remove(lastAsterisk);
525: }
526: lastAsterisk = tokpath.size();
527: }
528: tokpath.add(pathToken);
529: }
530: String basePath = concatPath(tokpath, 0, lastAsterisk);
531: String resourcePath = concatPath(tokpath, lastAsterisk + 1,
532: tokpath.size());
533: if (resourcePath.endsWith("/")) {
534: resourcePath = resourcePath.substring(0, resourcePath
535: .length() - 1);
536: }
537: StringBuffer buf = new StringBuffer(path.length())
538: .append(basePath);
539: int l = basePath.length();
540: boolean debug = logger.isDebugEnabled();
541: for (;;) {
542: String fullPath = buf.append(resourcePath).toString();
543: if (debug) {
544: logger.debug("Trying to find template source "
545: + fullPath);
546: }
547: Object templateSource = mainLoader
548: .findTemplateSource(fullPath);
549: if (templateSource != null) {
550: return templateSource;
551: }
552: if (l == 0) {
553: return null;
554: }
555: l = basePath.lastIndexOf(SLASH, l - 2) + 1;
556: buf.setLength(l);
557: }
558: }
559:
560: private String concatPath(List path, int from, int to) {
561: StringBuffer buf = new StringBuffer((to - from) * 16);
562: for (int i = from; i < to; ++i) {
563: buf.append(path.get(i)).append('/');
564: }
565: return buf.toString();
566: }
567:
568: private static String normalizeName(String name) {
569: if (name.indexOf("://") > 0) {
570: return name;
571: }
572: for (;;) {
573: int parentDirPathLoc = name.indexOf(PARENT_DIR_PATH);
574: if (parentDirPathLoc == 0) {
575: // If it starts with /../, then it reaches outside the template
576: // root.
577: return null;
578: }
579: if (parentDirPathLoc == -1) {
580: if (name.startsWith(PARENT_DIR_PATH_PREFIX)) {
581: // Another attempt to reach out of template root.
582: return null;
583: }
584: break;
585: }
586: int previousSlashLoc = name.lastIndexOf(SLASH,
587: parentDirPathLoc - 1);
588: name = name.substring(0, previousSlashLoc + 1)
589: + name.substring(parentDirPathLoc
590: + PARENT_DIR_PATH.length());
591: }
592: for (;;) {
593: int currentDirPathLoc = name.indexOf(CURRENT_DIR_PATH);
594: if (currentDirPathLoc == -1) {
595: if (name.startsWith(CURRENT_DIR_PATH_PREFIX)) {
596: name = name.substring(CURRENT_DIR_PATH_PREFIX
597: .length());
598: }
599: break;
600: }
601: name = name.substring(0, currentDirPathLoc)
602: + name.substring(currentDirPathLoc
603: + CURRENT_DIR_PATH.length() - 1);
604: }
605: // Editing can leave us with a leading slash; strip it.
606: if (name.length() > 1 && name.charAt(0) == SLASH) {
607: name = name.substring(1);
608: }
609: return name;
610: }
611:
612: /**
613: * This class holds a (name, locale) pair and is used as the key in
614: * the cached templates map.
615: */
616: private static final class TemplateKey {
617: private final String name;
618: private final Locale locale;
619: private final String encoding;
620: private final boolean parse;
621:
622: TemplateKey(String name, Locale locale, String encoding,
623: boolean parse) {
624: this .name = name;
625: this .locale = locale;
626: this .encoding = encoding;
627: this .parse = parse;
628: }
629:
630: public boolean equals(Object o) {
631: if (o instanceof TemplateKey) {
632: TemplateKey tk = (TemplateKey) o;
633: return parse == tk.parse && name.equals(tk.name)
634: && locale.equals(tk.locale)
635: && encoding.equals(tk.encoding);
636: }
637: return false;
638: }
639:
640: public int hashCode() {
641: return name.hashCode() ^ locale.hashCode()
642: ^ encoding.hashCode()
643: ^ (parse ? Boolean.FALSE : Boolean.TRUE).hashCode();
644: }
645: }
646:
647: /**
648: * This class holds the cached template and associated information
649: * (the source object, and the last-checked and last-modified timestamps).
650: * It is used as the value in the cached templates map.
651: */
652: private static final class CachedTemplate {
653: Template template;
654: Object source;
655: long lastChecked;
656: long lastModified;
657: }
658: }
|