NIHVIVO-4011 Create the I18n framework for multi-language support.
This commit is contained in:
parent
885bbabdae
commit
caf16c392b
8 changed files with 712 additions and 0 deletions
|
@ -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.config.RevisionInfoBean;
|
||||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.Route;
|
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.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.directives.IndividualShortViewDirective;
|
||||||
import edu.cornell.mannlib.vitro.webapp.web.methods.IndividualLocalNameMethod;
|
import edu.cornell.mannlib.vitro.webapp.web.methods.IndividualLocalNameMethod;
|
||||||
import edu.cornell.mannlib.vitro.webapp.web.methods.IndividualPlaceholderImageUrlMethod;
|
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("profileUrl", new IndividualProfileUrlMethod());
|
||||||
map.put("localName", new IndividualLocalNameMethod());
|
map.put("localName", new IndividualLocalNameMethod());
|
||||||
map.put("placeholderImageUrl", new IndividualPlaceholderImageUrlMethod());
|
map.put("placeholderImageUrl", new IndividualPlaceholderImageUrlMethod());
|
||||||
|
map.put("i18n", new I18nMethodModel());
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,10 @@ public class TemplateProcessingHelper {
|
||||||
env.setCustomAttribute("request", request);
|
env.setCustomAttribute("request", request);
|
||||||
env.setCustomAttribute("context", context);
|
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
|
// Define a setup template to be included by every page template
|
||||||
String templateType = (String) map.get("templateType");
|
String templateType = (String) map.get("templateType");
|
||||||
if (FreemarkerHttpServlet.PAGE_TEMPLATE_TYPE.equals(templateType)) {
|
if (FreemarkerHttpServlet.PAGE_TEMPLATE_TYPE.equals(templateType)) {
|
||||||
|
|
228
webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java
Normal file
228
webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java
Normal file
|
@ -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<String> themeDirectory = new AtomicReference<String>(
|
||||||
|
"");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
102
webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java
Normal file
102
webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java
Normal file
|
@ -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<String> getKeys() {
|
||||||
|
return Collections.enumeration(Collections.<String> emptySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Object handleGetObject(String key) {
|
||||||
|
if (key == null) {
|
||||||
|
throw new NullPointerException("key may not be null.");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 <filepath>", 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<String> getKeys() {
|
||||||
|
return (Enumeration<String>) 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue