/*
* 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.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import freemarker.template.Compileable;
import freemarker.template.InputSource;
import freemarker.template.TemplateException;
import freemarker.template.TextEncoding;
/**
* <p>
* Retrieves cacheable objects through the file system. This version performs
* locale-based searching for files: first it looks for the most-localized file,
* then works back to the default (base) filename. Filenames are of the format:
* </p>
*
* <pre>
* (filename)_(language)_(country).(file extension)
* </pre>
*
* <p>
* For instance, an HTML file encoded in Australian English would have the
* filename: helloworld_en_AU.html
* </p>
*
* <p>
* Based on code donated to the FreeMarker project by Jonathan Revusky as part
* of the <a href="http://niggle.sourceforge.net/" target="_top">Niggle</a> web
* application framework.
* </p>
*
* @version $Id: LocalizedFileRetriever.java 1130 2005-10-04 11:42:11Z run2000 $
*/
public class LocalizedFileRetriever 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;
/** The localization. */
protected Locale locale = Locale.getDefault();
protected List localeExtensions = new ArrayList(0);
private static final String defaultEncoding = System.getProperty("file.encoding");
private static final Map encodingMap = getEncodings();
private static Map getEncodings() {
Map encoding = new HashMap();
encoding.put("ar", "ISO-8859-6");
encoding.put("be", "ISO-8859-5");
encoding.put("bg", "ISO-8859-5");
encoding.put("ca", "ISO-8859-1");
encoding.put("cs", "ISO-8859-2");
encoding.put("da", "ISO-8859-1");
encoding.put("de", "ISO-8859-1");
encoding.put("el", "ISO-8859-7");
encoding.put("en", "ISO-8859-1");
encoding.put("es", "ISO-8859-1");
encoding.put("et", "ISO-8859-1");
encoding.put("fi", "ISO-8859-1");
encoding.put("fr", "ISO-8859-1");
encoding.put("hr", "ISO-8859-2");
encoding.put("hu", "ISO-8859-2");
encoding.put("is", "ISO-8859-1");
encoding.put("it", "ISO-8859-1");
encoding.put("iw", "ISO-8859-8");
encoding.put("ja", "Shift_JIS");
encoding.put("ko", "EUC-KR"); // Requires JDK 1.1.6
encoding.put("lt", "ISO-8859-2");
encoding.put("lv", "ISO-8859-2");
encoding.put("mk", "ISO-8859-5");
encoding.put("nl", "ISO-8859-1");
encoding.put("no", "ISO-8859-1");
encoding.put("pl", "ISO-8859-2");
encoding.put("pt", "ISO-8859-1");
encoding.put("ro", "ISO-8859-2");
encoding.put("ru", "ISO-8859-5");
encoding.put("sh", "ISO-8859-5");
encoding.put("sk", "ISO-8859-2");
encoding.put("sl", "ISO-8859-2");
encoding.put("sq", "ISO-8859-2");
encoding.put("sr", "ISO-8859-5");
encoding.put("sv", "ISO-8859-1");
encoding.put("tr", "ISO-8859-9");
encoding.put("uk", "ISO-8859-5");
encoding.put("zh", "GB2312");
encoding.put("zh_TW", "Big5");
return encoding;
}
/** Creates new FileRetriever. */
public LocalizedFileRetriever() {
}
/**
* 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
*/
public LocalizedFileRetriever(String path) {
setConnection(path);
}
/**
* Creates a new FileRetriever, with a directory root.
*
* @param rootDir
* the root directory for the file system
*/
public LocalizedFileRetriever(File rootDir) {
setPath(rootDir);
}
/**
* Corresponds to checkCacheDir for file-system implementations.
*
* @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.
*/
public void setConnection(String path) {
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
*/
public void setPath(File dir) {
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 {
FileLocale file = getLocalizedFile(location);
return (file != null);
} 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. For localization purposes, determine when we find any
* localization suffixes, and remove them.
*
* @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);
// 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(relativeDirPath + getRootFile(filename));
}
} else if (file.isDirectory()) {
// If the item is a directory, recursively read it.
readDirectory(file, relativeDirPath + filename + '/', visitedFiles);
}
}
}
/**
* <p>
* Determines when the object in the template repository was last modified.
* </p>
*
* @param location
* the location of the object to be tested
* @return milliseconds since 1970 of the time the item was last modified
* @throws TemplateException
* is thrown whenever the item:
* <ul>
* <li>does not exist</li>
* <li>is the wrong type (eg. a directory, not a file)</li>
* <li>has an invalid file suffix</li>
* </ul>
*/
public long lastModified(String location) throws TemplateException {
FileLocale file;
if (!isSuffixValid(location)) {
throw new TemplateException("Invalid suffix in filename \"" + location + '"');
}
file = getLocalizedFile(location);
if (file == null) {
throw new TemplateException('"' + location + "\" doesn't exist");
}
if (!file.cFile.isFile()) {
throw new TemplateException('"' + file.cFile.getAbsolutePath() + "\" is a directory");
}
return file.cFile.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 <code>File</code>.
*
* @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 {
FileLocale file = getLocalizedFile(location);
try {
FileInputStream inputStream = new FileInputStream(file.cFile);
Compileable template = (Compileable) registry.getTemplate(type);
if (encoding == null) {
String localeEncoding = getCharset(file.cLocale);
template.compile(new InputSource(inputStream, localeEncoding));
} 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 <code>InputStreamReader</code>.
*/
public String getEncoding() {
return encoding;
}
/**
* Sets a template registry implementation to use when creating new
* templates.
*
* @param cRegistry
* the registry to be used for creating new objects
*/
public void setTemplateRegistry(TemplateRegistry cRegistry) {
registry = cRegistry;
}
/**
* Retrieves the current TemplateRegistry in use.
*
* @return the registry currently in use when creating new objects
*/
public TemplateRegistry getTemplateRegistry() {
return registry;
}
/**
* Sets the locale to use when retrieving files.
*/
public void setLocale(Locale locale) {
this.locale = locale;
localeExtensions = getLocaleExtensions(locale);
}
/**
* Retrieves the locale used when retrieving files.
*/
public Locale getLocale() {
return locale;
}
/**
* Creates a list of locales and associated filenames to use when searching
* for localized files.
*/
protected List getLocaleExtensions(Locale locale) {
List cLocales = new ArrayList(5);
String variant = locale.getVariant();
String country = locale.getCountry();
String lang = locale.getLanguage();
LocaleMap cMap;
// Levels of full specification. We try them in order.
if (variant.length() > 0) {
cMap = new LocaleMap();
cMap.aName = '_' + lang + '_' + country + '_' + variant;
cMap.cLocale = locale;
cLocales.add(cMap);
}
if (country.length() > 0) {
cMap = new LocaleMap();
cMap.aName = '_' + lang + '_' + country;
cMap.cLocale = new Locale(locale.getLanguage(), locale.getCountry());
cLocales.add(cMap);
}
if (lang.length() > 0) {
cMap = new LocaleMap();
cMap.aName = '_' + lang;
cMap.cLocale = new Locale(locale.getLanguage(), "");
cLocales.add(cMap);
}
cMap = new LocaleMap();
cMap.aName = "";
cMap.cLocale = Locale.getDefault();
cLocales.add(cMap);
return cLocales;
}
/**
* Performs a reverse lookup of locale information: given a filename,
* determine whether a locale has been used, and if so, strips it back to
* the root filename.
*/
protected String getRootFile(String aFilename) {
int dotIndex = aFilename.lastIndexOf('.');
String basename;
String extension;
Iterator iFile = localeExtensions.iterator();
String aLocale;
if (dotIndex > 0) {
basename = aFilename.substring(0, dotIndex);
extension = aFilename.substring(dotIndex);
} else {
basename = aFilename;
extension = "";
}
while (iFile.hasNext()) {
aLocale = ((LocaleMap) iFile.next()).aName;
if (basename.endsWith(aLocale)) {
return basename.substring(0, basename.length() - aLocale.length()) + extension;
}
}
return aFilename;
}
/**
* Given a base filename, get a localized version, if one is available.
* Searches for the most specific localized version first, then works back
* to least specific.
*/
protected FileLocale getLocalizedFile(String aFilename) throws TemplateException {
Iterator iFile = localeExtensions.iterator();
File cFile;
FileLocale cFileLocale;
LocaleMap cMap;
while (iFile.hasNext()) {
cMap = (LocaleMap) iFile.next();
cFile = nameToFile(getFilenameFromLocale(aFilename, cMap));
if (cFile.exists()) {
cFileLocale = new FileLocale();
cFileLocale.cFile = cFile;
cFileLocale.cLocale = cMap.cLocale;
return cFileLocale;
}
}
return null;
}
/**
* Given a base filename, and a LocaleMap entry, work out what the filename
* should be.
*/
protected String getFilenameFromLocale(String aFilename, LocaleMap cLocale) {
int dotIndex = aFilename.lastIndexOf('.');
String basename;
String extension;
if (dotIndex > 0) {
basename = aFilename.substring(0, dotIndex);
extension = aFilename.substring(dotIndex);
} else {
basename = aFilename;
extension = "";
}
return basename + cLocale.aName + extension;
}
/**
* Gets the preferred charset for the given locale, or null if the locale is
* not recognized.
*
* @param loc
* the locale
* @return the preferred charset
*/
static public String getCharset(Locale loc) {
String charset;
// Try for a full name match (may include country)
charset = (String) encodingMap.get(loc.toString());
if (charset != null)
return charset;
// If a full name didn't match, try just the language
charset = (String) encodingMap.get(loc.getLanguage());
// tweaked so it doesn't return null.
return charset != null ? charset : defaultEncoding;
}
/**
* Holds a name to locale mapping
*/
static class LocaleMap {
protected String aName;
protected Locale cLocale;
}
/**
* Holds a file and the locale associated with it.
*/
static class FileLocale {
protected File cFile;
protected Locale cLocale;
}
/**
* 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);
}
if (locale != null) {
buffer.append(", locale: ");
buffer.append(locale);
}
return buffer.toString();
}
}