diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerConfiguration.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerConfiguration.java index 957da18cd..cca16fc34 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerConfiguration.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerConfiguration.java @@ -19,6 +19,7 @@ import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.config.RevisionInfoBean; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.Route; import edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.EditConfigurationConstants; +import edu.cornell.mannlib.vitro.webapp.i18n.freemarker.I18nMethodModel; import edu.cornell.mannlib.vitro.webapp.web.directives.IndividualShortViewDirective; import edu.cornell.mannlib.vitro.webapp.web.methods.IndividualLocalNameMethod; import edu.cornell.mannlib.vitro.webapp.web.methods.IndividualPlaceholderImageUrlMethod; @@ -166,6 +167,7 @@ public class FreemarkerConfiguration extends Configuration { map.put("profileUrl", new IndividualProfileUrlMethod()); map.put("localName", new IndividualLocalNameMethod()); map.put("placeholderImageUrl", new IndividualPlaceholderImageUrlMethod()); + map.put("i18n", new I18nMethodModel()); return map; } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/TemplateProcessingHelper.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/TemplateProcessingHelper.java index ffca0bb65..5d91a919e 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/TemplateProcessingHelper.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/TemplateProcessingHelper.java @@ -51,6 +51,10 @@ public class TemplateProcessingHelper { env.setCustomAttribute("request", request); env.setCustomAttribute("context", context); + // Set the Locale from the request into the environment, so date builtins will be + // Locale-dependent + env.setLocale(request.getLocale()); + // Define a setup template to be included by every page template String templateType = (String) map.get("templateType"); if (FreemarkerHttpServlet.PAGE_TEMPLATE_TYPE.equals(templateType)) { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java new file mode 100644 index 000000000..00af47192 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java @@ -0,0 +1,228 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.i18n; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.ResourceBundle.Control; +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; + +/** + * Provides access to a bundle of text strings, based on the name of the bundle, + * the Locale of the requesting browser, and the current theme directory. + * + * If the bundle name is not specified, the default name of "all" is used. + * + * If a requested bundle is not found, no error is thrown. Instead, an empty + * bundle is returned that produces error message strings when asked for text. + */ +public class I18n { + private static final Log log = LogFactory.getLog(I18n.class); + + public static final String DEFAULT_BUNDLE_NAME = "all"; + private static final String PROPERTY_DEVELOPER_DEFEAT_CACHE = "developer.defeatI18nCache"; + + /** + * This is where the work gets done. Not declared final, so it can be + * modified in unit tests. + */ + private static I18n instance = new I18n(); + + // ---------------------------------------------------------------------- + // Static methods + // ---------------------------------------------------------------------- + + /** + * A convenience method to get a bundle and format the text. + */ + public static String text(String bundleName, HttpServletRequest req, + String key, Object... parameters) { + return bundle(bundleName, req).text(key, parameters); + } + + /** + * A convenience method to get the default bundle and format the text. + */ + public static String text(HttpServletRequest req, String key, + Object... parameters) { + return bundle(req).text(key, parameters); + } + + /** + * Get a I18nBundle by this name. + */ + public static I18nBundle bundle(String bundleName, HttpServletRequest req) { + return instance.getBundle(bundleName, req); + } + + /** + * Get the default I18nBundle. + */ + public static I18nBundle bundle(HttpServletRequest req) { + return instance.getBundle(DEFAULT_BUNDLE_NAME, req); + } + + // ---------------------------------------------------------------------- + // The instance + // ---------------------------------------------------------------------- + + /** Holds the current theme directory, as far as we know. */ + private AtomicReference themeDirectory = new AtomicReference( + ""); + + /** + * Get an I18nBundle by this name. The request provides the preferred + * Locale, the theme directory and the development mode flag. + * + * If the request indicates that the system is in development mode, then the + * cache is cleared on each request. + * + * If the theme directory has changed, the cache is cleared. + */ + private I18nBundle getBundle(String bundleName, HttpServletRequest req) { + log.debug("Getting bundle '" + bundleName + "'"); + + try { + checkDevelopmentMode(req); + checkForChangeInThemeDirectory(req); + + String dir = themeDirectory.get(); + ServletContext ctx = req.getSession().getServletContext(); + + ResourceBundle.Control control = getControl(ctx, dir); + ResourceBundle rb = ResourceBundle.getBundle(bundleName, + req.getLocale(), control); + return new I18nBundle(bundleName, rb); + } catch (MissingResourceException e) { + log.warn("Didn't find text bundle '" + bundleName + "'"); + return I18nBundle.emptyBundle(bundleName); + } catch (Exception e) { + log.error("Failed to create text bundle '" + bundleName + "'", e); + return I18nBundle.emptyBundle(bundleName); + } + } + + /** + * If we are in development mode, clear the cache on each request. + */ + private void checkDevelopmentMode(HttpServletRequest req) { + ConfigurationProperties bean = ConfigurationProperties.getBean(req); + + String flag = bean + .getProperty(PROPERTY_DEVELOPER_DEFEAT_CACHE, "false"); + if (Boolean.valueOf(flag.trim())) { + log.debug("In development mode - clearing the cache."); + ResourceBundle.clearCache(); + } + } + + /** + * If the theme directory has changed from before, clear the cache of all + * ResourceBundles. + */ + private void checkForChangeInThemeDirectory(HttpServletRequest req) { + String currentDir = new VitroRequest(req).getAppBean().getThemeDir(); + String previousDir = themeDirectory.getAndSet(currentDir); + if (!currentDir.equals(previousDir)) { + log.debug("Theme directory changed from '" + previousDir + "' to '" + + currentDir + "' - clearing the cache."); + ResourceBundle.clearCache(); + } + } + + /** + * Override this method in the unit tests, to return a more testable Control + * instance. + */ + protected Control getControl(ServletContext ctx, String dir) { + return new ThemeBasedControl(ctx, dir); + } + + // ---------------------------------------------------------------------- + // Control classes for instantiating ResourceBundles + // ---------------------------------------------------------------------- + + /** + * Instead of looking in the classpath, look in the theme directory. + */ + private static class ThemeBasedControl extends ResourceBundle.Control { + private static final String BUNDLE_DIRECTORY = "i18n/"; + private final ServletContext ctx; + private final String themeDirectory; + + public ThemeBasedControl(ServletContext ctx, String themeDirectory) { + this.ctx = ctx; + this.themeDirectory = themeDirectory; + } + + /** + * Don't look for classes to satisfy the request, just property files. + */ + @Override + public List getFormats(String baseName) { + return FORMAT_PROPERTIES; + } + + /** + * Don't look in the class path, look in the current servlet context, in + * the bundle directory under the theme directory. + */ + @Override + public ResourceBundle newBundle(String baseName, Locale locale, + String format, ClassLoader loader, boolean reload) + throws IllegalAccessException, InstantiationException, + IOException { + checkArguments(baseName, locale, format); + + log.debug("Creating bundle for '" + baseName + "', " + locale + + ", '" + format + "', " + reload); + + String bundleName = toBundleName(baseName, locale); + if (bundleName == null) { + throw new NullPointerException("bundleName may not be null."); + } + + String themeI18nPath = "/" + themeDirectory + BUNDLE_DIRECTORY; + String appI18nPath = "/" + BUNDLE_DIRECTORY; + + log.debug("Paths are '" + themeI18nPath + "' and '" + appI18nPath + + "'"); + + return VivoResourceBundle.getBundle(bundleName, ctx, appI18nPath, + themeI18nPath, this); + } + + /** + * The documentation for ResourceBundle.Control.newBundle() says I + * should throw these exceptions. + */ + private void checkArguments(String baseName, Locale locale, + String format) { + if (baseName == null) { + throw new NullPointerException("baseName may not be null."); + } + if (locale == null) { + throw new NullPointerException("locale may not be null."); + } + if (format == null) { + throw new NullPointerException("format may not be null."); + } + if (!FORMAT_DEFAULT.contains(format)) { + throw new IllegalArgumentException( + "format must be one of these: " + FORMAT_DEFAULT); + } + } + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java new file mode 100644 index 000000000..7ea5acc3d --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java @@ -0,0 +1,102 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.i18n; + +import java.text.MessageFormat; +import java.util.Collections; +import java.util.Enumeration; +import java.util.ResourceBundle; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A wrapper for a ResourceBundle that will not throw an exception, no matter + * what string you request. + * + * If the ResourceBundle was not found, or if it doesn't contain the requested + * key, an error message string is returned, to help the developer diagnose the + * problem. + */ +public class I18nBundle { + private static final Log log = LogFactory.getLog(I18nBundle.class); + + private static final String MESSAGE_BUNDLE_NOT_FOUND = "Text bundle ''{0}'' not found."; + private static final String MESSAGE_KEY_NOT_FOUND = "Text bundle ''{0}'' has no text for ''{1}''"; + + public static I18nBundle emptyBundle(String bundleName) { + return new I18nBundle(bundleName); + } + + private final String bundleName; + private final ResourceBundle resources; + private final String notFoundMessage; + + private I18nBundle(String bundleName) { + this(bundleName, new EmptyResourceBundle(), MESSAGE_BUNDLE_NOT_FOUND); + } + + public I18nBundle(String bundleName, ResourceBundle resources) { + this(bundleName, resources, MESSAGE_KEY_NOT_FOUND); + } + + private I18nBundle(String bundleName, ResourceBundle resources, String notFoundMessage) { + if (bundleName == null) { + throw new IllegalArgumentException("bundleName may not be null"); + } + if (bundleName.isEmpty()) { + throw new IllegalArgumentException("bundleName may not be empty"); + } + if (resources == null) { + throw new NullPointerException("resources may not be null."); + }if (notFoundMessage == null) { + throw new NullPointerException("notFoundMessage may not be null."); + } + this.bundleName = bundleName; + this.resources = resources; + this.notFoundMessage = notFoundMessage; + } + + public String text(String key, Object... parameters) { + log.debug("Asking for '" + key + "' from bundle '" + bundleName + "'"); + + String textString; + if (resources.containsKey(key)) { + textString = resources.getString(key); + return formatString(textString, parameters); + } else { + String message = MessageFormat.format(notFoundMessage, bundleName, + key); + log.warn(message); + return "ERROR: " + message; + } + } + + private static String formatString(String textString, Object... parameters) { + if (parameters.length == 0) { + return textString; + } else { + return MessageFormat.format(textString, parameters); + } + } + + /** + * A resource bundle that contains no strings. + */ + public static class EmptyResourceBundle extends ResourceBundle { + @Override + public Enumeration getKeys() { + return Collections.enumeration(Collections. emptySet()); + } + + @Override + protected Object handleGetObject(String key) { + if (key == null) { + throw new NullPointerException("key may not be null."); + } + return null; + } + + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/VivoResourceBundle.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/VivoResourceBundle.java new file mode 100644 index 000000000..ba314ea36 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/VivoResourceBundle.java @@ -0,0 +1,207 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.i18n; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Enumeration; +import java.util.Properties; +import java.util.ResourceBundle; + +import javax.servlet.ServletContext; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Works like a PropertyResourceBundle with two exceptions: + * + * It looks for the file in both the i18n directory of the theme and in the i18n + * directory of the application. Properties found in the theme override those + * found in the application. + * + * It allows a property to take its contents from a file. File paths are + * relative to the i18n directory. Again, a file in the theme will override one + * in the application. + * + * If a property has a value (after overriding) of "@@file ", the + * bundle looks for the file relative to the i18n directory of the theme, then + * relative to the i18n directory of the application. If the file is not found + * in either location, a warning is written to the log and the property will + * contain an error message for displayed. + * + * Note that the filename is not manipulated for Locale, so the author of the + * properties files must do it explicitly. For example: + * + * In all.properties: account_email_html = @@file accountEmail.html + * + * In all_es.properties: account_email_html = @@file accountEmail_es.html + */ +public class VivoResourceBundle extends ResourceBundle { + private static final Log log = LogFactory.getLog(VivoResourceBundle.class); + + private static final String FILE_FLAG = "@@file "; + private static final String MESSAGE_FILE_NOT_FOUND = "File {1} not found for property {0}."; + + // ---------------------------------------------------------------------- + // Factory method + // ---------------------------------------------------------------------- + + public static VivoResourceBundle getBundle(String bundleName, + ServletContext ctx, String appI18nPath, String themeI18nPath, + Control control) { + try { + return new VivoResourceBundle(bundleName, ctx, appI18nPath, + themeI18nPath, control); + } catch (FileNotFoundException e) { + log.debug(e); + return null; + } catch (Exception e) { + log.warn(e, e); + return null; + } + } + + // ---------------------------------------------------------------------- + // The instance + // ---------------------------------------------------------------------- + + private final String bundleName; + private final ServletContext ctx; + private final String appI18nPath; + private final String themeI18nPath; + private final Control control; + private final Properties defaults; + private final Properties properties; + + private VivoResourceBundle(String bundleName, ServletContext ctx, + String appI18nPath, String themeI18nPath, Control control) + throws IOException { + this.bundleName = bundleName; + this.ctx = ctx; + this.appI18nPath = appI18nPath; + this.themeI18nPath = themeI18nPath; + this.control = control; + + this.defaults = new Properties(); + this.properties = new Properties(this.defaults); + + loadProperties(); + loadReferencedFiles(); + } + + private void loadProperties() throws IOException { + String resourceName = control.toResourceName(bundleName, "properties"); + + String defaultsPath = joinPath(appI18nPath, resourceName); + String propertiesPath = joinPath(themeI18nPath, resourceName); + File defaultsFile = locateFile(defaultsPath); + File propertiesFile = locateFile(propertiesPath); + + if ((defaultsFile == null) && (propertiesFile == null)) { + throw new FileNotFoundException("Property file not found at '" + + defaultsPath + "' or '" + propertiesPath + "'"); + } + + if (defaultsFile != null) { + log.debug("Loading bundle '" + bundleName + "' defaults from '" + + defaultsPath + "'"); + FileInputStream stream = new FileInputStream(defaultsFile); + try { + this.defaults.load(stream); + } finally { + stream.close(); + } + } + if (propertiesFile != null) { + log.debug("Loading bundle '" + bundleName + "' overrides from '" + + propertiesPath + "'"); + FileInputStream stream = new FileInputStream(propertiesFile); + try { + this.properties.load(stream); + } finally { + stream.close(); + } + } + } + + private void loadReferencedFiles() throws IOException { + for (String key : this.properties.stringPropertyNames()) { + String value = this.properties.getProperty(key); + if (value.startsWith(FILE_FLAG)) { + String filepath = value.substring(FILE_FLAG.length()).trim(); + loadReferencedFile(key, filepath); + } + } + } + + private void loadReferencedFile(String key, String filepath) + throws IOException { + String appFilePath = joinPath(appI18nPath, filepath); + String themeFilePath = joinPath(themeI18nPath, filepath); + File appFile = locateFile(appFilePath); + File themeFile = locateFile(themeFilePath); + + if (themeFile != null) { + this.properties.setProperty(key, + FileUtils.readFileToString(themeFile, "UTF-8")); + } else if (appFile != null) { + this.properties.setProperty(key, + FileUtils.readFileToString(appFile, "UTF-8")); + } else { + String message = MessageFormat.format(MESSAGE_FILE_NOT_FOUND, key, + themeFilePath, appFilePath); + this.properties.setProperty(key, message); + log.warn(message); + } + } + + private String joinPath(String root, String twig) { + if ((root.charAt(root.length() - 1) == File.separatorChar) + || (twig.charAt(0) == File.separatorChar)) { + return root + twig; + } else { + return root + File.separatorChar + twig; + } + } + + private File locateFile(String path) { + String realPath = ctx.getRealPath(path); + if (realPath == null) { + log.debug("No real path for '" + path + "'"); + return null; + } + + File f = new File(realPath); + if (!f.isFile()) { + log.debug("No file at '" + realPath + "'"); + return null; + } + if (!f.canRead()) { + log.error("Can't read the file at '" + realPath + "'"); + return null; + } + log.debug("Located file '" + path + "' at '" + realPath + "'"); + return f; + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration getKeys() { + return (Enumeration) this.properties.propertyNames(); + } + + @Override + protected Object handleGetObject(String key) { + String value = this.properties.getProperty(key); + if (value == null) { + log.debug(bundleName + " has no value for '" + key + "'"); + } + return value; + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nBundleTemplateModel.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nBundleTemplateModel.java new file mode 100644 index 000000000..def580202 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nBundleTemplateModel.java @@ -0,0 +1,40 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.i18n.freemarker; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle; +import freemarker.template.TemplateHashModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +/** + * For Freemarker, this acts like a bundle of text strings. It is simply a + * wrapper around an I18nBundle. + */ +public class I18nBundleTemplateModel implements TemplateHashModel { + private static final Log log = LogFactory + .getLog(I18nBundleTemplateModel.class); + + private final String bundleName; + private final I18nBundle textBundle; + + public I18nBundleTemplateModel(String bundleName, I18nBundle textBundle) { + this.bundleName = bundleName; + this.textBundle = textBundle; + } + + @Override + public TemplateModel get(String key) throws TemplateModelException { + return new I18nStringTemplateModel(bundleName, key, + textBundle.text(key)); + } + + @Override + public boolean isEmpty() throws TemplateModelException { + return false; + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nMethodModel.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nMethodModel.java new file mode 100644 index 000000000..4e56896f4 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nMethodModel.java @@ -0,0 +1,50 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.i18n.freemarker; + +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.i18n.I18n; +import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle; +import freemarker.core.Environment; +import freemarker.template.TemplateMethodModel; +import freemarker.template.TemplateModelException; + +/** + * This Freemarker method will produce a bundle of text strings. It is simply a + * wrapper around I18n that produces a wrapped I18nBundle. + * + * If the bundle name is not provided, the default bundle is assumed. + */ +public class I18nMethodModel implements TemplateMethodModel { + private static final Log log = LogFactory.getLog(I18nMethodModel.class); + + @SuppressWarnings("rawtypes") + @Override + public Object exec(List args) throws TemplateModelException { + if (args.size() > 1) { + throw new TemplateModelException("Too many arguments: " + + "displayText method only requires a bundle name."); + } + Object arg = args.isEmpty() ? I18n.DEFAULT_BUNDLE_NAME : args.get(0); + if (!(arg instanceof String)) { + throw new IllegalArgumentException( + "Arguments to a TemplateMethodModel are supposed to be Strings!"); + } + + log.debug("Asking for this bundle: " + arg); + String bundleName = (String) arg; + + Environment env = Environment.getCurrentEnvironment(); + HttpServletRequest request = (HttpServletRequest) env + .getCustomAttribute("request"); + I18nBundle tb = I18n.bundle(bundleName, request); + return new I18nBundleTemplateModel(bundleName, tb); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nStringTemplateModel.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nStringTemplateModel.java new file mode 100644 index 000000000..e11ae6640 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/freemarker/I18nStringTemplateModel.java @@ -0,0 +1,79 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.i18n.freemarker; + +import java.text.MessageFormat; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateScalarModel; +import freemarker.template.utility.DeepUnwrap; + +/** + * A Freemarker representation of a text string. Because it implements + * TemplateScalarModel, you can use it as a string value. And because it + * implements TemplateMethodModel, you can pass arguments to it for formatting. + * + * So if the string is "His name is {0}!", then these references could be used: + * + * ${string} ==> "His name is {0}!" + * + * ${string("Bozo")} ==> "His name is Bozo!" + * + * Note that the format of the message is determined by java.text.MessageFormat, + * so argument indices start at 0 and you can escape a substring by wrapping it + * in apostrophes. + */ +public class I18nStringTemplateModel implements TemplateMethodModelEx, + TemplateScalarModel { + private static final Log log = LogFactory + .getLog(I18nStringTemplateModel.class); + + private final String bundleName; + private final String key; + private final String textString; + + public I18nStringTemplateModel(String bundleName, String key, + String textString) { + this.bundleName = bundleName; + this.key = key; + this.textString = textString; + } + + @Override + public String getAsString() throws TemplateModelException { + return textString; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Object exec(List args) throws TemplateModelException { + log.debug("Formatting string '" + key + "' from bundle '" + bundleName + + "' with these arguments: " + args); + + if (args.isEmpty()) { + return textString; + } else { + Object[] unwrappedArgs = new Object[args.size()]; + for (int i = 0; i < args.size(); i++) { + unwrappedArgs[i] = DeepUnwrap.unwrap((TemplateModel) args + .get(i)); + } + try { + return MessageFormat.format(textString, unwrappedArgs); + } catch (Exception e) { + String message = "Can't format '" + key + "' from bundle '" + + bundleName + "', wrong argument types: " + args + + " for message format'" + textString + "'"; + log.warn(message); + return message; + } + } + } + +}