/* * FreeMarker: a tool that allows Java programs to generate HTML * output using templates. * Copyright (C) 1998-2004 Benjamin Geer * Email: beroul@users.sourceforge.net * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ package freemarker.template.cache; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.LinkedList; import java.util.List; import freemarker.template.Compileable; import freemarker.template.InputSource; import freemarker.template.TemplateException; import freemarker.template.TextEncoding; /** * Retrieves cacheable objects through the file system. This is the default * retriever for <code>FileTemplateCache</code>. * * @version $Id: FileRetriever.java 1130 2005-10-04 11:42:11Z run2000 $ */ public class FileRetriever implements CacheRetriever, TextEncoding, RegistryAccepter { /** The root directory where the retriever will get files. */ protected File directoryRoot; /** The filename suffix required for a file to be retrieved. */ protected String filenameSuffix; /** The text encoding of the template files. */ protected String encoding; /** The template registry to use to instantiate objects. */ protected TemplateRegistry registry; /** Creates new FileRetriever. */ public FileRetriever() { } /** * Constructs a FileRetriever with a directory in which it will look for * template files. * * @param path * the absolute path of the directory containing templates for * this retriever * @throws IllegalArgumentException * the root directory is null */ public FileRetriever(String path) { setConnection(path); } /** * Creates a new FileRetriever, with a directory root. * * @param rootDir * the root directory for the file system * @throws IllegalArgumentException * the root directory is null */ public FileRetriever(File rootDir) { setPath(rootDir); } /** * Corresponds to checkCacheDir for file-system implementations. * * @return <code>true</code> if the connection is ok, otherwise * <code>false</code> * @throws TemplateException * the directory no longer exists, or is not a directory */ public boolean connectionOk() throws TemplateException { if (directoryRoot == null) { throw new TemplateException("Root directory is not defined"); } if (!directoryRoot.isDirectory()) { throw new TemplateException('"' + directoryRoot.getAbsolutePath() + "\" is not a directory or does not exist"); } return true; } /** * Sets the root directory for this retriever. * * @param path * the absolute path of the directory containing files for this * retriever. * @throws IllegalArgumentException * the root directory is null */ public void setConnection(String path) { if (path == null) { throw new IllegalArgumentException("Root cache path cannot be null"); } setPath(new File(path)); } /** * Gets the connection for this retriever. Corresponds to getPath for * file-system implementations. */ public String getConnection() { if (directoryRoot == null) { return null; } return directoryRoot.toString(); } /** * Sets the root directory for this retriever. * * @param dir * the root directory containing files for this retriever * @throws IllegalArgumentException * the root directory is null */ public void setPath(File dir) { if (dir == null) { throw new IllegalArgumentException("Root cache directory cannot be null"); } this.directoryRoot = dir; } /** * Returns the root directory for this retriever. * * @return the root directory containing files for this retriever */ public File getPath() { return directoryRoot; } /** * Sets the file suffix. If set, files that do not have this suffix will be * ignored when read into the cache. * * @param filenameSuffix * the optional filename suffix of files to be read for this * retriever. */ public void setFilenameSuffix(String filenameSuffix) { this.filenameSuffix = filenameSuffix; } /** * Returns the file suffix. If set, files that do not have this suffix will * be ignored when read into the cache. * * @return the optional filename suffix of files to be read for this * retriever. */ public String getFilenameSuffix() { return filenameSuffix; } /** * Tests whether the object still exists in the template repository. This * may be redundant. Instead, lastModified could throw an appropriate * exception. * * @param location * the location of the object to be tested * @return <code>true</code> if the object still exists in the repository, * otherwise <code>false</code> * @see #lastModified */ public boolean exists(String location) { try { File file = nameToFile(location); return (file.exists()); } catch (TemplateException e) { return false; } } /** * Returns a list of objects (<code>String</code>s) to pre-load the cache * with. * * @return a <code>List</code> of <code>String</code>s to preload the cache * with */ public List getPreloadData() throws TemplateException { List visitedFiles = new LinkedList(); try { readDirectory(directoryRoot, "", visitedFiles); } catch (IOException e) { throw new TemplateException("Could not get preload data", e); } return visitedFiles; } /** * Recursively updates the cache from the files in a (sub)directory and its * subdirectories. * * @param dir * the directory to be read. * @param relativeDirPath * a string representing the directory's path relative to the * root cache directory. * @param visitedFiles * a List of files that have been visited so far. */ protected void readDirectory(File dir, String relativeDirPath, List visitedFiles) throws IOException { String[] filenames = dir.list(); if (filenames == null) { throw new IOException("Could not get file list from directory \"" + dir.getAbsolutePath() + '"'); } // Iterate through the items in the directory. for (int fileNum = 0; fileNum < filenames.length; fileNum++) { String filename = filenames[fileNum]; File file = new File(dir, filename); String elementName = relativeDirPath + filename; // If the item is a file, see if we need to to read it. if (file.isFile()) { // If we have no filename suffix, or if we have one and this // file ends with it, check the file. if (filenameSuffix == null || filename.endsWith(filenameSuffix)) { visitedFiles.add(elementName); } } else if (file.isDirectory()) { // If the item is a directory, recursively read it. readDirectory(file, elementName + '/', visitedFiles); } } } /** * <p> * Determines when the object in the template repository was last modified. * </p> * * @throws TemplateException * is thrown whenever the item: * <ul> * <li>does not exist</li> * <li>is the wrong type (eg. directory, not file)</li> * <li>has an invalid file suffix</li> * </ul> */ public long lastModified(String location) throws TemplateException { File file; if (!isSuffixValid(location)) { throw new TemplateException("Invalid suffix in filename \"" + location + '"'); } file = nameToFile(location); if (!file.isFile()) { throw new TemplateException('"' + file.getPath() + "\" is not a file or does not exist"); } return file.lastModified(); } /** * Determine whether the filename ends with the appropriate filename suffix. * * @param name * the filename to be checked * @return is the filename suffix ok? * @throws TemplateException * the suffix is invalid */ protected boolean isSuffixValid(String name) throws TemplateException { if (!(filenameSuffix == null || name.endsWith(filenameSuffix))) { throw new TemplateException("The requested name, \"" + name + "\", does not have the filename suffix \"" + filenameSuffix + '"'); } return true; } /** * Converts a cache element name to a <tt>File</tt>. * * @param name * the filename relative to the directory root of the retriever * @return the fully qualified filename */ protected File nameToFile(final String name) throws TemplateException { String pathBuf = new String(name); // As a sanity check, make sure Windows users can't escape path // checking by using Windows file separators. if (File.separatorChar != '/') { pathBuf.replace(File.separatorChar, '/'); } // Make sure the path is absolutely-positioned if (pathBuf.charAt(0) != '/') pathBuf = '/' + pathBuf; // Resolve occurrences of "//" in the normalized path while (true) { int index = pathBuf.indexOf("//"); if (index < 0) break; pathBuf = pathBuf.substring(0, index) + pathBuf.substring(index + 1); } // Resolve occurrences of "/./" in the normalized path while (true) { int index = pathBuf.indexOf("/./"); if (index < 0) break; pathBuf = pathBuf.substring(0, index) + pathBuf.substring(index + 2); } // Resolve occurrences of "/../" in the normalized path while (true) { int index = pathBuf.indexOf("/../"); if (index < 0) break; if (index == 0) { // Trying to go outside our context throw new TemplateException("Invalid relative path found in filename \"" + name + '"'); } int index2 = pathBuf.lastIndexOf('/', index - 1); pathBuf = pathBuf.substring(0, index2) + pathBuf.substring(index + 3); } // Remove leading '/' character prior to appending to root directory pathBuf = pathBuf.substring(1); // Replace forward slashes with the operating system's // file separator, if it's not a forward slash. if (File.separatorChar != '/') { pathBuf.replace('/', File.separatorChar); } return new File(directoryRoot, pathBuf); } /** * Retrieves the appropriate data to be stored in the cache. * * @param location * the filename, relative to the root directory, of the template * data to load * @param type * the type of item to be loaded * @return the template data */ public Cacheable loadData(String location, String type) throws TemplateException { File file = nameToFile(location); try { FileInputStream inputStream = new FileInputStream(file); Compileable template = (Compileable) registry.getTemplate(type); if (encoding == null) { template.compile(new InputSource(inputStream)); } else { template.compile(new InputSource(inputStream, encoding)); } inputStream.close(); return (Cacheable) template; } catch (java.io.IOException e) { throw new TemplateException("Could not load data", e); } catch (NullPointerException e) { throw new TemplateException("Could not load data", e); } } /** * Sets the character encoding to be used when reading template files. * * @param encoding * the name of the encoding to be used; this will be passed to * the constructor of <tt>InputStreamReader</tt>. */ public void setEncoding(String encoding) { this.encoding = encoding; } /** * Returns the character encoding to be used when reading template files. * * @return the name of the encoding to be used; this will be passed to the * constructor of <tt>InputStreamReader</tt>. */ public String getEncoding() { return encoding; } /** * Sets a template registry implementation to use when creating new * templates. */ public void setTemplateRegistry(TemplateRegistry cRegistry) { registry = cRegistry; } /** * Gets the current template registry implementation in use. */ public TemplateRegistry getTemplateRegistry() { return registry; } /** * Is this file retriever equal to another object? * * @param o * the object to compare this object with * @return <code>true</code> if the objects are equal, otherwise * <code>false</code> */ public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof FileRetriever)) return false; final FileRetriever fileRetriever = (FileRetriever) o; if (directoryRoot == null ? fileRetriever.directoryRoot != null : !directoryRoot.equals(fileRetriever.directoryRoot)) return false; if (encoding == null ? fileRetriever.encoding != null : !encoding.equals(fileRetriever.encoding)) return false; if (filenameSuffix == null ? fileRetriever.filenameSuffix != null : !filenameSuffix.equals(fileRetriever.filenameSuffix)) return false; if (registry == null ? fileRetriever.registry != null : !registry.equals(fileRetriever.registry)) return false; return true; } /** * Retrieve the hash code for this object * * @return the hash code */ public int hashCode() { int result = 13; result = 29 * result + (directoryRoot != null ? directoryRoot.hashCode() : 0); result = 29 * result + (filenameSuffix != null ? filenameSuffix.hashCode() : 0); result = 29 * result + (encoding != null ? encoding.hashCode() : 0); result = 29 * result + (registry != null ? registry.hashCode() : 0); return result; } /** * Returns a string representation of the object. * * @return a <code>String</code> representation of the object */ public String toString() { StringBuffer buffer = new StringBuffer(); if (directoryRoot != null) { buffer.append("Root path: "); buffer.append(directoryRoot); } if (filenameSuffix != null) { buffer.append(", filename suffix: "); buffer.append(filenameSuffix); } if (encoding != null) { buffer.append(", encoding: "); buffer.append(encoding); } if (registry != null) { buffer.append(", registry: "); buffer.append(registry); } return buffer.toString(); } }