KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > freemarker > cache > TemplateCache


1 /*
2  * Copyright (c) 2003 The Visigoth Software Society. All rights
3  * reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1. Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above copyright
13  * notice, this list of conditions and the following disclaimer in
14  * the documentation and/or other materials provided with the
15  * distribution.
16  *
17  * 3. The end-user documentation included with the redistribution, if
18  * any, must include the following acknowledgement:
19  * "This product includes software developed by the
20  * Visigoth Software Society (http://www.visigoths.org/)."
21  * Alternately, this acknowledgement may appear in the software itself,
22  * if and wherever such third-party acknowledgements normally appear.
23  *
24  * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
25  * project contributors may be used to endorse or promote products derived
26  * from this software without prior written permission. For written
27  * permission, please contact visigoths@visigoths.org.
28  *
29  * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
30  * nor may "FreeMarker" or "Visigoth" appear in their names
31  * without prior written permission of the Visigoth Software Society.
32  *
33  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
34  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
35  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
36  * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
37  * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
38  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
39  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
40  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
41  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
42  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
43  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
44  * SUCH DAMAGE.
45  * ====================================================================
46  *
47  * This software consists of voluntary contributions made by many
48  * individuals on behalf of the Visigoth Software Society. For more
49  * information on the Visigoth Software Society, please see
50  * http://www.visigoths.org/
51  */

52
53 package freemarker.cache;
54
55 import java.io.IOException JavaDoc;
56 import java.io.Reader JavaDoc;
57 import java.io.StringWriter JavaDoc;
58 import java.util.ArrayList JavaDoc;
59 import java.util.List JavaDoc;
60 import java.util.Locale JavaDoc;
61 import java.util.StringTokenizer JavaDoc;
62
63 import freemarker.core.Environment;
64 import freemarker.log.Logger;
65 import freemarker.template.Configuration;
66 import freemarker.template.Template;
67
68 /**
69  * A class that performs caching and on-demand loading of the templates.
70  * The actual loading is delegated to a {@link TemplateLoader}. Also,
71  * various constructors provide you with convenient caches with predefined
72  * behavior. Typically you don't use this class directly - in normal
73  * circumstances it is hidden behind a {@link Configuration}.
74  * @author Attila Szegedi, szegedia at freemail dot hu
75  * @version $Id: TemplateCache.java,v 1.62 2004/03/17 01:14:33 ddekany Exp $
76  */

77 public class TemplateCache
78 {
79     private static final String JavaDoc ASTERISKSTR = "*";
80     private static final String JavaDoc LOCALE_SEPARATOR = "_";
81     private static final char ASTERISK = '*';
82     private static final String JavaDoc CURRENT_DIR_PATH_PREFIX = "./";
83     private static final String JavaDoc CURRENT_DIR_PATH = "/./";
84     private static final String JavaDoc PARENT_DIR_PATH_PREFIX = "../";
85     private static final String JavaDoc PARENT_DIR_PATH = "/../";
86     private static final char SLASH = '/';
87     private static final Logger logger = Logger.getLogger("freemarker.cache");
88
89     private final TemplateLoader mainLoader;
90         /** DD: [noFallback]
91         ,
92         fallbackTemplateLoader = new ClassTemplateLoader(TemplateCache.class, "/");
93         */

94     /** Here we keep our cached templates */
95     private final CacheStorage storage;
96     /** The default refresh delay in milliseconds. */
97     private long delay = 5000;
98     /** Specifies if localized template lookup is enabled or not */
99     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     {
111         this(createDefaultTemplateLoader());
112     }
113
114     private static TemplateLoader createDefaultTemplateLoader() {
115         try {
116             return new FileTemplateLoader();
117         } catch(Exception JavaDoc e) {
118             logger.warn("Could not create a file template loader for current directory", e);
119             return null;
120         }
121     }
122     
123     /**
124      * Creates a new template cache with a custom template loader that is used
125      * to load the templates.
126      * @param loader the template loader to use.
127      */

128     public TemplateCache(TemplateLoader loader)
129     {
130         this(loader, new MruCacheStorage(0, Integer.MAX_VALUE));
131     }
132
133     /**
134      * Creates a new template cache with a custom template loader that is used
135      * to load the templates.
136      * @param loader the template loader to use.
137      */

138     public TemplateCache(TemplateLoader loader, CacheStorage storage)
139     {
140         this.mainLoader = loader;
141         this.storage = storage;
142         if(storage == null)
143         {
144             throw new IllegalArgumentException JavaDoc("storage == null");
145         }
146     }
147
148     /**
149      * Sets the configuration object to which this cache belongs. This
150      * method is called by the configuration itself to establish the
151      * relation, and should not be called by users.
152      */

153     public void setConfiguration(Configuration config)
154     {
155         this.config = config;
156         clear();
157     }
158
159     public TemplateLoader getTemplateLoader()
160     {
161         return mainLoader;
162     }
163     
164     public CacheStorage getCacheStorage()
165     {
166         return storage;
167     }
168     
169     /**
170      * Loads a template with the given name, in the specified locale and
171      * using the specified character encoding.
172      *
173      * @param name the name of the template. Can't be null. The exact syntax of the name
174      * is interpreted by the underlying {@link TemplateLoader}, but the
175      * cache makes some assumptions. First, the name is expected to be
176      * a hierarchical path, with path components separated by a slash
177      * character (not with backslash!). The path (the name) must <em>not</em> begin with slash;
178      * the path is always relative to the "template root directory".
179      * Then, the <tt>..</tt> and <tt>.</tt> path metaelements will be resolved.
180      * For example, if the name is <tt>a/../b/./c.ftl</tt>, then it will be
181      * simplified to <tt>b/c.ftl</tt>. The rules regarding this are same as with conventional
182      * UN*X paths. The path must not reach outside the template root directory, that is,
183      * it can't be something like <tt>"../templates/my.ftl"</tt> (not even if the pervious path
184      * happens to be equivalent with <tt>"/my.ftl"</tt>).
185      * Further, the path is allowed to contain at most
186      * one path element whose name is <tt>*</tt> (asterisk). This path metaelement triggers the
187      * <i>acquisition mechanism</i>. If the template is not found in
188      * the location described by the concatenation of the path left to the
189      * asterisk (called base path) and the part to the right of the asterisk
190      * (called resource path), the cache will attempt to remove the rightmost
191      * path component from the base path ("go up one directory") and concatenate
192      * that with the resource path. The process is repeated until either a
193      * template is found, or the base path is completely exhausted.
194      *
195      * @param locale the requested locale of the template. Can't be null.
196      * Assuming you have specified <code>en_US</code> as the locale and
197      * <code>myTemplate.html</code> as the name of the template, the cache will
198      * first try to retrieve <code>myTemplate_en_US.html</code>, then
199      * <code>myTemplate.html_en.html</code>, and finally
200      * <code>myTemplate.html</code>.
201      *
202      * @param encoding the character encoding used to interpret the template
203      * source bytes. Can't be null.
204      *
205      * @param parse if true, the loaded template is parsed and interpreted
206      * as a regular FreeMarker template. If false, the loaded template is
207      * treated as an unparsed block of text.
208      *
209      * @return the loaded template, or null if the template is not found.
210      */

211     public Template getTemplate(String JavaDoc name, Locale JavaDoc locale, String JavaDoc encoding, boolean parse)
212     throws IOException JavaDoc
213     {
214         if (name == null) {
215             throw new IllegalArgumentException JavaDoc("Argument \"name\" can't be null");
216         }
217         if (locale == null) {
218             throw new IllegalArgumentException JavaDoc("Argument \"locale\" can't be null");
219         }
220         if (encoding == null) {
221             throw new IllegalArgumentException JavaDoc("Argument \"encoding\" can't be null");
222         }
223         name = normalizeName(name);
224         if(name == null) {
225             return null;
226         }
227         Template result = null;
228         if (mainLoader != null) {
229             result = getTemplate(mainLoader, name, locale, encoding, parse);
230         }
231         /** DD: [noFallback]
232         if (result == null && name.toLowerCase().endsWith(".ftl")) {
233             result = getTemplate(fallbackTemplateLoader, name, locale, encoding, parse);
234         }
235         */

236         return result;
237     }
238     
239     private Template getTemplate(TemplateLoader loader, String JavaDoc name, Locale JavaDoc locale, String JavaDoc encoding, boolean parse)
240     throws IOException JavaDoc
241     {
242         boolean debug = logger.isDebugEnabled();
243         String JavaDoc debugName = debug ? name + "[" + locale + "," + encoding + (parse ? ",parsed] " : ",unparsed] ") : null;
244         TemplateKey tk = new TemplateKey(name, locale, encoding, parse);
245         synchronized (storage)
246         {
247             CachedTemplate cachedTemplate = (CachedTemplate)storage.get(tk);
248             long now = System.currentTimeMillis();
249             long lastModified = -1L;
250             Object JavaDoc newlyFoundSource = null;
251             try
252             {
253                 if (cachedTemplate != null)
254                 {
255                     // If we're within the refresh delay, return the cached copy
256
if (now - cachedTemplate.lastChecked < delay)
257                     {
258                         if(debug)
259                         {
260                             logger.debug(debugName + "cached copy not yet stale; using cached.");
261                         }
262                         return cachedTemplate.template;
263                     }
264                     // Else, update the last-checked flag
265
cachedTemplate.lastChecked = now;
266
267                     // Find the template source
268
newlyFoundSource = findTemplateSource(name, locale);
269
270                     // Template source was removed
271
if (newlyFoundSource == null)
272                     {
273                         if(debug)
274                         {
275                             logger.debug(debugName + "no source found (removing from cache if it was cached).");
276                         }
277                         storage.remove(tk);
278                         return null;
279                     }
280
281                     // If the source didn't change and its last modified date
282
// also didn't change, return the cached version.
283
lastModified = loader.getLastModified(newlyFoundSource);
284                     boolean lastModifiedNotChanged = lastModified == cachedTemplate.lastModified;
285                     boolean sourceEquals = newlyFoundSource.equals(cachedTemplate.source);
286                     if(lastModifiedNotChanged && sourceEquals)
287                     {
288                         if(debug)
289                         {
290                             logger.debug(debugName + "using cached since " +
291                                     newlyFoundSource + " didn't change.");
292                         }
293                         cachedTemplate.lastChecked = now;
294                         return cachedTemplate.template;
295                     }
296                     else
297                     {
298                         if(debug && !sourceEquals)
299                         {
300                             logger.debug("Updating source, info for cause: " +
301                                 "sourceEquals=" + sourceEquals +
302                                 ", newlyFoundSource=" + newlyFoundSource +
303                                 ", cachedTemplate.source=" + cachedTemplate.source);
304                         }
305                         if(debug && !lastModifiedNotChanged)
306                         {
307                             logger.debug("Updating source, info for cause: " +
308                                 "lastModifiedNotChanged=" + lastModifiedNotChanged +
309                                 ", cache lastModified=" + cachedTemplate.lastModified +
310                                 " != file lastModified=" + lastModified);
311                         }
312                         // Update the source
313
cachedTemplate.source = newlyFoundSource;
314                     }
315                 }
316                 else
317                 {
318                     if(debug)
319                     {
320                         logger.debug("Could not find template in cache, "
321                             + "creating new one; id=[" +
322                             tk.name + "[" + tk.locale + "," + tk.encoding +
323                             (tk.parse ? ",parsed] " : ",unparsed] ") + "]");
324                     }
325                     
326                     // Construct a new CachedTemplate entry. Note we set the
327
// cachedTemplate.lastModified to Long.MIN_VALUE. This is
328
// a flag that signs it has to be explicitly queried later on.
329
newlyFoundSource = findTemplateSource(name, locale);
330                     if (newlyFoundSource == null)
331                     {
332                         return null;
333                     }
334                     cachedTemplate = new CachedTemplate();
335                     cachedTemplate.source = newlyFoundSource;
336                     cachedTemplate.lastChecked = now;
337                     cachedTemplate.lastModified = lastModified = Long.MIN_VALUE;
338                     storage.put(tk, cachedTemplate);
339                 }
340                 if(debug)
341                 {
342                     logger.debug("Compiling FreeMarker template " +
343                         debugName + " from " + newlyFoundSource);
344                 }
345                 // If we get here, then we need to (re)load the template
346
Object JavaDoc source = cachedTemplate.source;
347                 cachedTemplate.template =
348                     loadTemplate(loader, name, locale, encoding, parse, source);
349                 cachedTemplate.lastModified =
350                     lastModified == Long.MIN_VALUE
351                         ? loader.getLastModified(source)
352                         : lastModified;
353                 return cachedTemplate.template;
354             }
355             finally
356             {
357                 if(newlyFoundSource != null)
358                 {
359                     loader.closeTemplateSource(newlyFoundSource);
360                 }
361             }
362         }
363     }
364
365     private Template loadTemplate(TemplateLoader loader, String JavaDoc name, Locale JavaDoc locale, String JavaDoc encoding,
366                                    boolean parse, Object JavaDoc source)
367     throws IOException JavaDoc
368     {
369         Template template;
370         Reader JavaDoc reader = loader.getReader(source, encoding);
371         try
372         {
373             if(parse)
374             {
375                 try {
376                     template = new Template(name, reader, config, encoding);
377                 }
378                 catch (Template.WrongEncodingException wee) {
379                     encoding = wee.specifiedEncoding;
380                     reader = loader.getReader(source, encoding);
381                     template = new Template(name, reader, config, encoding);
382                 }
383                 template.setLocale(locale);
384             }
385             else
386             {
387                 // Read the contents into a StringWriter, then construct a single-textblock
388
// template from it.
389
StringWriter JavaDoc sw = new StringWriter JavaDoc();
390                 char[] buf = new char[4096];
391                 for(;;)
392                 {
393                     int charsRead = reader.read(buf);
394                     if (charsRead > 0)
395                     {
396                         sw.write(buf, 0, charsRead);
397                     }
398                     else if(charsRead == -1)
399                     {
400                         break;
401                     }
402                 }
403                 template = Template.getPlainTextTemplate(name, sw.toString(), config);
404                 template.setLocale(locale);
405             }
406             template.setEncoding(encoding);
407         }
408         finally
409         {
410             reader.close();
411         }
412         return template;
413     }
414
415     /**
416      * Gets the delay in milliseconds between checking for newer versions of a
417      * template source.
418      * @return the current value of the delay
419      */

420     public synchronized long getDelay()
421     {
422         return delay;
423     }
424
425     /**
426      * Sets the delay in milliseconds between checking for newer versions of a
427      * template sources.
428      * @param delay the new value of the delay
429      */

430     public synchronized void setDelay(long delay)
431     {
432         this.delay = delay;
433     }
434
435     /**
436      * Returns if localized template lookup is enabled or not.
437      */

438     public synchronized boolean getLocalizedLookup()
439     {
440         return localizedLookup;
441     }
442
443     /**
444      * Setis if localized template lookup is enabled or not.
445      */

446     public synchronized void setLocalizedLookup(boolean localizedLookup)
447     {
448         this.localizedLookup = localizedLookup;
449     }
450
451     /**
452      * Removes all entries from the cache, forcing reloading of templates
453      * on subsequent {@link #getTemplate(String, Locale, String, boolean)}
454      * calls.
455      */

456     public void clear()
457     {
458         synchronized (storage) {
459             storage.clear();
460         }
461     }
462
463     public static String JavaDoc getFullTemplatePath(Environment env, String JavaDoc parentTemplateDir, String JavaDoc templateNameString)
464     {
465         if (!env.isClassicCompatible()) {
466             if (templateNameString.indexOf("://") >0) {
467                 ;
468             }
469             else if (templateNameString.length() > 0 && templateNameString.charAt(0) == '/') {
470                 int protIndex = parentTemplateDir.indexOf("://");
471                 if (protIndex >0) {
472                     templateNameString = parentTemplateDir.substring(0, protIndex + 2) + templateNameString;
473                 } else {
474                     templateNameString = templateNameString.substring(1);
475                 }
476             }
477             else {
478                 templateNameString = parentTemplateDir + templateNameString;
479             }
480         }
481         return templateNameString;
482     }
483
484     private Object JavaDoc findTemplateSource(String JavaDoc name, Locale JavaDoc locale)
485     throws
486         IOException JavaDoc
487     {
488         if (localizedLookup) {
489             int lastDot = name.lastIndexOf('.');
490             String JavaDoc prefix = lastDot == -1 ? name : name.substring(0, lastDot);
491             String JavaDoc suffix = lastDot == -1 ? "" : name.substring(lastDot);
492             String JavaDoc localeName = LOCALE_SEPARATOR + locale.toString();
493             StringBuffer JavaDoc buf = new StringBuffer JavaDoc(name.length() + localeName.length());
494             buf.append(prefix);
495             for (;;)
496             {
497                 buf.setLength(prefix.length());
498                 String JavaDoc path = buf.append(localeName).append(suffix).toString();
499                 Object JavaDoc templateSource = acquireTemplateSource(path);
500                 if (templateSource != null)
501                 {
502                     return templateSource;
503                 }
504                 int lastUnderscore = localeName.lastIndexOf('_');
505                 if (lastUnderscore == -1)
506                     break;
507                 localeName = localeName.substring(0, lastUnderscore);
508             }
509             return null;
510         }
511         else
512         {
513             return acquireTemplateSource(name);
514         }
515     }
516
517     private Object JavaDoc acquireTemplateSource(String JavaDoc path) throws IOException JavaDoc
518     {
519         int asterisk = path.indexOf(ASTERISK);
520         // Shortcut in case there is no acquisition
521
if(asterisk == -1)
522         {
523             return mainLoader.findTemplateSource(path);
524         }
525         StringTokenizer JavaDoc tok = new StringTokenizer JavaDoc(path, "/");
526         int lastAsterisk = -1;
527         List JavaDoc tokpath = new ArrayList JavaDoc();
528         while(tok.hasMoreTokens())
529         {
530             String JavaDoc pathToken = tok.nextToken();
531             if(pathToken.equals(ASTERISKSTR))
532             {
533                 if(lastAsterisk != -1)
534                 {
535                     tokpath.remove(lastAsterisk);
536                 }
537                 lastAsterisk = tokpath.size();
538             }
539             tokpath.add(pathToken);
540         }
541         String JavaDoc basePath = concatPath(tokpath, 0, lastAsterisk);
542         String JavaDoc resourcePath = concatPath(tokpath, lastAsterisk + 1, tokpath.size());
543
544         StringBuffer JavaDoc buf = new StringBuffer JavaDoc(path.length()).append(basePath);
545         int l = basePath.length();
546         boolean debug = logger.isDebugEnabled();
547         for(;;)
548         {
549             String JavaDoc fullPath = buf.append(resourcePath).toString();
550             if(debug)
551             {
552                 logger.debug("Trying to find template source " + fullPath);
553             }
554             Object JavaDoc templateSource = mainLoader.findTemplateSource(fullPath);
555             if(templateSource != null)
556             {
557                 return templateSource;
558             }
559             if(l == 0)
560             {
561                 return null;
562             }
563             l = basePath.lastIndexOf(SLASH, l - 2) + 1;
564             buf.setLength(l);
565         }
566     }
567
568     private String JavaDoc concatPath(List JavaDoc path, int from, int to)
569     {
570         StringBuffer JavaDoc buf = new StringBuffer JavaDoc((to - from) * 16);
571         for(int i = from; i < to; ++i)
572         {
573             buf.append(path.get(i)).append('/');
574         }
575         return buf.toString();
576     }
577     
578     private static String JavaDoc normalizeName(String JavaDoc name) {
579         if (name.indexOf("://") >0) {
580             return name;
581         }
582         for(;;) {
583             int parentDirPathLoc = name.indexOf(PARENT_DIR_PATH);
584             if(parentDirPathLoc == 0) {
585                 // If it starts with /../, then it reaches outside the template
586
// root.
587
return null;
588             }
589             if(parentDirPathLoc == -1) {
590                 if(name.startsWith(PARENT_DIR_PATH_PREFIX)) {
591                     // Another attempt to reach out of template root.
592
return null;
593                 }
594                 break;
595             }
596             int previousSlashLoc = name.lastIndexOf(SLASH, parentDirPathLoc - 1);
597             name = name.substring(0, previousSlashLoc + 1) +
598                    name.substring(parentDirPathLoc + PARENT_DIR_PATH.length());
599         }
600         for(;;) {
601             int currentDirPathLoc = name.indexOf(CURRENT_DIR_PATH);
602             if(currentDirPathLoc == -1) {
603                 if(name.startsWith(CURRENT_DIR_PATH_PREFIX)) {
604                     name = name.substring(CURRENT_DIR_PATH_PREFIX.length());
605                 }
606                 break;
607             }
608             name = name.substring(0, currentDirPathLoc) +
609                    name.substring(currentDirPathLoc + CURRENT_DIR_PATH.length() - 1);
610         }
611         // Editing can leave us with a leading slash; strip it.
612
if(name.length() > 1 && name.charAt(0) == SLASH) {
613             name = name.substring(1);
614         }
615         return name;
616     }
617
618     /**
619      * This class holds a (name, locale) pair and is used as the key in
620      * the cached templates map.
621      */

622     private static final class TemplateKey
623     {
624         private final String JavaDoc name;
625         private final Locale JavaDoc locale;
626         private final String JavaDoc encoding;
627         private final boolean parse;
628
629         TemplateKey(String JavaDoc name, Locale JavaDoc locale, String JavaDoc encoding, boolean parse)
630         {
631             this.name = name;
632             this.locale = locale;
633             this.encoding = encoding;
634             this.parse = parse;
635         }
636
637         public boolean equals(Object JavaDoc o)
638         {
639             if (o instanceof TemplateKey)
640             {
641                 TemplateKey tk = (TemplateKey)o;
642                 return
643                     parse == tk.parse &&
644                     name.equals(tk.name) &&
645                     locale.equals(tk.locale) &&
646                     encoding.equals(tk.encoding);
647             }
648             return false;
649         }
650
651         public int hashCode()
652         {
653             return
654                 name.hashCode() ^
655                 locale.hashCode() ^
656                 encoding.hashCode() ^
657                 (parse ? Boolean.FALSE : Boolean.TRUE).hashCode();
658         }
659     }
660
661     /**
662      * This class holds the cached template and associated information
663      * (the source object, and the last-checked and last-modified timestamps).
664      * It is used as the value in the cached templates map.
665      */

666     private static final class CachedTemplate
667     {
668         Template template;
669         Object JavaDoc source;
670         long lastChecked;
671         long lastModified;
672     }
673 }
674
Popular Tags