VIVO-12 NIHVIVO-4011 Provide config and GUI for selecting Locale

This commit is contained in:
j2blake 2013-01-24 16:21:36 -05:00
parent 1ba6204815
commit bb6b2fa970
19 changed files with 1346 additions and 10 deletions

View file

@ -5,7 +5,6 @@ package edu.cornell.mannlib.vitro.webapp.controller.freemarker;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

View file

@ -200,7 +200,7 @@ public class I18n {
log.debug("Paths are '" + themeI18nPath + "' and '" + appI18nPath
+ "'");
return VivoResourceBundle.getBundle(bundleName, ctx, appI18nPath,
return VitroResourceBundle.getBundle(bundleName, ctx, appI18nPath,
themeI18nPath, this);
}

View file

@ -41,8 +41,8 @@ import org.apache.commons.logging.LogFactory;
*
* 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);
public class VitroResourceBundle extends ResourceBundle {
private static final Log log = LogFactory.getLog(VitroResourceBundle.class);
private static final String FILE_FLAG = "@@file ";
private static final String MESSAGE_FILE_NOT_FOUND = "File {1} not found for property {0}.";
@ -51,11 +51,11 @@ public class VivoResourceBundle extends ResourceBundle {
// Factory method
// ----------------------------------------------------------------------
public static VivoResourceBundle getBundle(String bundleName,
public static VitroResourceBundle getBundle(String bundleName,
ServletContext ctx, String appI18nPath, String themeI18nPath,
Control control) {
try {
return new VivoResourceBundle(bundleName, ctx, appI18nPath,
return new VitroResourceBundle(bundleName, ctx, appI18nPath,
themeI18nPath, control);
} catch (FileNotFoundException e) {
log.debug(e);
@ -78,7 +78,7 @@ public class VivoResourceBundle extends ResourceBundle {
private final Properties defaults;
private final Properties properties;
private VivoResourceBundle(String bundleName, ServletContext ctx,
private VitroResourceBundle(String bundleName, ServletContext ctx,
String appI18nPath, String themeI18nPath, Control control)
throws IOException {
this.bundleName = bundleName;

View file

@ -0,0 +1,100 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.i18n.selection;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.LocaleUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.beans.DisplayMessage;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
/**
* Call this at /selectLocale&selection=[locale_string]
*
* For example: /selectLocale&selection=en_US or /selectLocale&selection=es
*
* Write an error to the log (and to DisplayMessage) if the selection is not
* syntactically valid.
*
* Write a warning to the log if the selection code is not one of the selectable
* Locales from runtime.properties, or if the selection code is not recognized
* by the system.
*
* Set the new Locale in the Session using SelectedLocale and return to the
* referrer.
*/
public class LocaleSelectionController extends HttpServlet {
private static final Log log = LogFactory
.getLog(LocaleSelectionController.class);
public static final String PARAMETER_SELECTION = "selection";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String referrer = req.getHeader("referer");
String selectedLocale = req.getParameter(PARAMETER_SELECTION);
try {
processSelectedLocale(req, selectedLocale);
} catch (Exception e) {
log.error("Failed to process the user's Locale selection", e);
}
if (StringUtils.isEmpty(referrer)) {
resp.sendRedirect(UrlBuilder.getHomeUrl());
} else {
resp.sendRedirect(referrer);
}
}
private void processSelectedLocale(HttpServletRequest req,
String selectedLocale) {
if (StringUtils.isBlank(selectedLocale)) {
log.debug("No '" + PARAMETER_SELECTION + "' parameter");
return;
}
Locale locale = null;
try {
locale = LocaleUtils.toLocale(selectedLocale.trim());
log.debug("Locale selection is " + locale);
} catch (IllegalArgumentException e) {
log.error("Failed to convert the selection to a Locale", e);
DisplayMessage.setMessage(req,
"There was a problem in the system. "
+ "Your language choice was rejected.");
return;
}
List<Locale> selectables = SelectedLocale.getSelectableLocales(req);
if (!selectables.contains(locale)) {
log.warn("User selected a locale '" + locale
+ "' that was not in the list: " + selectables);
} else if (!LocaleUtils.isAvailableLocale(locale)) {
log.warn("User selected an unrecognized locale: '" + locale + "'");
}
SelectedLocale.setSelectedLocale(req, locale);
log.debug("Setting selected locale to " + locale);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}

View file

@ -0,0 +1,95 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.i18n.selection;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
import edu.cornell.mannlib.vitro.webapp.utils.dataGetter.DataGetter;
/**
* Get the data for the selectable Locales, so the Freemarker template can
* create a row of flag images that will select the desired locale.
*
* If there are no selectable Locales in runtime.properties, we return an empty
* map. (selectLocale?? will return false)
*
* If the Locale has been forced by runtime.properties, we do the same.
*
* If there are selectable Locales, the returned map will contain a structure
* like this:
*
* <pre>
* {selectLocale={
* selectLocaleUrl = [the URL for the form action to select a Locale]
* locales={ [a list of maps]
* { [a map for each Locale]
* code = [the code for the Locale, e.g. "en_US"]
* label = [the alt text for the Locale, e.g. "Spanish (Spain)"]
* imageUrl = [the URL of the image that represents the Locale]
* }
* }
* }
* }
* </pre>
*/
public class LocaleSelectionDataGetter implements DataGetter {
private static final Log log = LogFactory
.getLog(LocaleSelectionDataGetter.class);
private final VitroRequest vreq;
public LocaleSelectionDataGetter(VitroRequest vreq) {
this.vreq = vreq;
}
@Override
public Map<String, Object> getData(Map<String, Object> valueMap) {
List<Locale> selectables = SelectedLocale.getSelectableLocales(vreq);
if (selectables.isEmpty()) {
return Collections.emptyMap();
}
Map<String, Object> result = new HashMap<String, Object>();
result.put("selectLocaleUrl", UrlBuilder.getUrl("/selectLocale"));
result.put("locales", buildLocalesList(selectables));
Map<String, Object> bodyMap = new HashMap<String, Object>();
bodyMap.put("selectLocale", result);
log.debug("Sending these values: " + bodyMap);
return bodyMap;
}
private List<Map<String, Object>> buildLocalesList(List<Locale> selectables) {
Locale currentLocale = SelectedLocale.getCurrentLocale(vreq);
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
for (Locale locale : selectables) {
try {
list.add(buildLocaleMap(locale, currentLocale));
} catch (FileNotFoundException e) {
log.warn("Can't show the Locale selector for '" + locale
+ "': " + e);
}
}
return list;
}
private Map<String, Object> buildLocaleMap(Locale locale,
Locale currentLocale) throws FileNotFoundException {
Map<String, Object> map = new HashMap<String, Object>();
map.put("code", locale.toString());
map.put("label", locale.getDisplayName(currentLocale));
map.put("imageUrl", LocaleSelectorUtilities.getImageUrl(vreq, locale));
return map;
}
}

View file

@ -0,0 +1,113 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.i18n.selection;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.collections.EnumerationUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Check for a Locale in the ServletContext or the Session that should override
* the Locale in the ServletRequest.
*
* If there is such a Locale, wrap the ServletRequest so it behaves as if that
* is the preferred Locale.
*
* Otherwise, just process the request as usual.
*/
public class LocaleSelectionFilter implements Filter {
private static final Log log = LogFactory
.getLog(LocaleSelectionFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Nothing to do at startup.
}
@Override
public void destroy() {
// Nothing to do at shutdown.
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
HttpServletRequest hreq = (HttpServletRequest) request;
Locale overridingLocale = SelectedLocale.getOverridingLocale(hreq);
log.debug("overriding Locale is " + overridingLocale);
if (overridingLocale != null) {
request = new LocaleSelectionRequestWrapper(hreq,
overridingLocale);
}
} else {
log.debug("Not an HttpServletRequest.");
}
chain.doFilter(request, response);
}
// ----------------------------------------------------------------------
// Helper classes
// ----------------------------------------------------------------------
/**
* Uses the selected Locale as the preferred Locale of the request.
*/
private static class LocaleSelectionRequestWrapper extends
HttpServletRequestWrapper {
private final HttpServletRequest request;
private final Locale selectedLocale;
public LocaleSelectionRequestWrapper(HttpServletRequest request,
Locale selectedLocale) {
super(request);
if (request == null) {
throw new NullPointerException("request may not be null.");
}
this.request = request;
if (selectedLocale == null) {
throw new NullPointerException(
"selectedLocale may not be null.");
}
this.selectedLocale = selectedLocale;
}
@Override
public Locale getLocale() {
return selectedLocale;
}
/**
* Put the selected Locale on the front of the list of acceptable
* Locales.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Enumeration getLocales() {
List list = EnumerationUtils.toList(request.getLocales());
list.remove(selectedLocale);
list.add(0, selectedLocale);
return Collections.enumeration(list);
}
}
}

View file

@ -0,0 +1,148 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.i18n.selection;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.apache.commons.lang.LocaleUtils;
import org.apache.commons.lang.StringUtils;
import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
import edu.cornell.mannlib.vitro.webapp.startup.StartupStatus;
/**
* Check the ConfigurationProperties for a forced locale, or for a
* comma-separate list of selectable locales.
*
* Create the appropriate Locale objects and store them in the ServletContext.
*/
public class LocaleSelectionSetup implements ServletContextListener {
/**
* If this is set, the locale is forced. No selection will be offered to the
* user, and browser locales will be ignored.
*/
public static final String PROPERTY_FORCE_LOCALE = "languages.forceLocale";
/**
* This is the list of locales that the user may select. There should be a
* national flag or symbol available for each supported locale.
*/
public static final String PROPERTY_SELECTABLE_LOCALES = "languages.selectableLocales";
private ServletContext ctx;
private StartupStatus ss;
private ConfigurationProperties props;
private String forceString;
private String selectableString;
@Override
public void contextInitialized(ServletContextEvent sce) {
ctx = sce.getServletContext();
ss = StartupStatus.getBean(ctx);
props = ConfigurationProperties.getBean(sce);
readProperties();
if (isForcing() && hasSelectables()) {
warnAboutOverride();
}
if (isForcing()) {
forceLocale();
} else if (hasSelectables()) {
setUpSelections();
} else {
reportNoLocales();
}
}
private void readProperties() {
forceString = props.getProperty(PROPERTY_FORCE_LOCALE, "");
selectableString = props.getProperty(PROPERTY_SELECTABLE_LOCALES, "");
}
private boolean isForcing() {
return StringUtils.isNotBlank(forceString);
}
private boolean hasSelectables() {
return StringUtils.isNotBlank(selectableString);
}
private void warnAboutOverride() {
ss.warning(this, "'" + PROPERTY_FORCE_LOCALE + "' will override '"
+ PROPERTY_SELECTABLE_LOCALES + "'.");
}
private void forceLocale() {
try {
Locale forceLocale = buildLocale(forceString);
SelectedLocale.setForcedLocale(ctx, forceLocale);
ssInfo("Setting forced locale to '" + forceLocale + "'.");
} catch (IllegalArgumentException e) {
ssWarning("Problem in '" + PROPERTY_FORCE_LOCALE + "': "
+ e.getMessage());
}
}
private void setUpSelections() {
List<Locale> locales = new ArrayList<Locale>();
for (String string : splitSelectables()) {
try {
locales.add(buildLocale(string));
} catch (IllegalArgumentException e) {
ssWarning("Problem in '" + PROPERTY_SELECTABLE_LOCALES + "': "
+ e.getMessage());
}
}
SelectedLocale.setSelectableLocales(ctx, locales);
ssInfo("Setting selectable locales to '" + locales + "'.");
}
private String[] splitSelectables() {
return selectableString.split("\\s*,\\s*");
}
private void reportNoLocales() {
ssInfo("There is no Locale information.");
}
private void ssInfo(String message) {
ss.info(this, message + showPropertyValues());
}
private void ssWarning(String message) {
ss.warning(this, message + showPropertyValues());
}
private String showPropertyValues() {
return " In runtime.properties, '" + PROPERTY_FORCE_LOCALE
+ "' is set to '" + forceString + "', '"
+ PROPERTY_SELECTABLE_LOCALES + "' is set to '"
+ selectableString + "'";
}
private Locale buildLocale(String localeString)
throws IllegalArgumentException {
Locale locale = LocaleUtils.toLocale(localeString);
if (!LocaleUtils.isAvailableLocale(locale)) {
ssWarning("'" + locale + "' is not a recognized locale.");
}
return locale;
}
@Override
public void contextDestroyed(ServletContextEvent arg0) {
// Nothing to do at shutdown.
}
}

View file

@ -0,0 +1,54 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.i18n.selection;
import java.io.FileNotFoundException;
import java.util.Locale;
import javax.servlet.ServletContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
/**
* Some static methods for the GUI aspects of selecting a Locale.
*/
public class LocaleSelectorUtilities {
private static final Log log = LogFactory
.getLog(LocaleSelectorUtilities.class);
/**
* Look in the current theme directory to find a selection image for this
* Locale.
*
* Images are expected at a resource path like
* /[themeDir]/i18n/images/select_locale_[locale_code].*
*
* For example, /themes/wilma/i18n/images/select_locale_en.png
* /themes/wilma/i18n/images/select_locale_en.JPEG
* /themes/wilma/i18n/images/select_locale_en.gif
*
* To create a proper URL, prepend the context path.
*/
public static String getImageUrl(VitroRequest vreq, Locale locale)
throws FileNotFoundException {
String filename = "select_locale_" + locale + ".";
String themeDir = vreq.getAppBean().getThemeDir();
String imageDirPath = "/" + themeDir + "i18n/images/";
ServletContext ctx = vreq.getSession().getServletContext();
for (Object o : ctx.getResourcePaths(imageDirPath)) {
String resourcePath = (String) o;
if (resourcePath.contains(filename)) {
String fullPath = vreq.getContextPath() + resourcePath;
log.debug("Found image for " + locale + " at '" + fullPath
+ "'");
return fullPath;
}
}
throw new FileNotFoundException("Can't find an image for " + locale);
}
}

View file

@ -0,0 +1,204 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.i18n.selection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* A utility class for storing and retrieving Locale information.
*
* The static methods create beans and store them in the ServletContext or the
* session, where the information can be found later.
*/
public abstract class SelectedLocale {
private static final Log log = LogFactory.getLog(SelectedLocale.class);
/** Use this attribute on both the ServletContext and the Session. */
protected static final String ATTRIBUTE_NAME = "SELECTED_LOCALE";
/**
* Store the forced locale in the servlet context. Clear any selectable
* Locales.
*/
public static void setForcedLocale(ServletContext ctx, Locale forcedLocale) {
log.debug("Set forced locale: " + forcedLocale);
ctx.setAttribute(ATTRIBUTE_NAME,
new ContextSelectedLocale(forcedLocale));
}
/**
* Store the selected locale in the current session.
*/
public static void setSelectedLocale(HttpServletRequest req,
Locale selectedLocale) {
log.debug("Set selected locale: " + selectedLocale);
req.getSession().setAttribute(ATTRIBUTE_NAME,
new SessionSelectedLocale(selectedLocale));
}
/**
* Do we need to override the Locale in the current request? return the
* first of these to be found:
* <ul>
* <li>The forced Locale in the servlet context</li>
* <li>The selected Locale in the session</li>
* <li>null</li>
* </ul>
*/
public static Locale getOverridingLocale(HttpServletRequest req) {
HttpSession session = req.getSession();
ServletContext ctx = session.getServletContext();
Object ctxInfo = ctx.getAttribute(ATTRIBUTE_NAME);
if (ctxInfo instanceof ContextSelectedLocale) {
Locale forcedLocale = ((ContextSelectedLocale) ctxInfo)
.getForcedLocale();
if (forcedLocale != null) {
log.debug("Found forced locale in the context: " + forcedLocale);
return forcedLocale;
}
}
Object sessionInfo = session.getAttribute(ATTRIBUTE_NAME);
if (sessionInfo instanceof SessionSelectedLocale) {
Locale selectedLocale = ((SessionSelectedLocale) sessionInfo)
.getSelectedLocale();
if (selectedLocale != null) {
log.debug("Found selected locale in the session: "
+ selectedLocale);
return selectedLocale;
}
}
return null;
}
/**
* Get the current Locale to use, which is the first of these to be found:
* <ul>
* <li>The forced Locale in the servlet context</li>
* <li>The selected Locale in the session</li>
* <li>The Locale from the request</li>
* <li>The default Locale for the JVM</li>
* </ul>
*/
public static Locale getCurrentLocale(HttpServletRequest req) {
Locale overridingLocale = getOverridingLocale(req);
if (overridingLocale != null) {
return overridingLocale;
}
Locale requestLocale = req.getLocale();
if (requestLocale != null) {
log.debug("Found locale in the request: " + requestLocale);
return requestLocale;
}
log.debug("Using default locale: " + Locale.getDefault());
return Locale.getDefault();
}
/**
* Store a list of selectable Locales in the servlet context, so we can
* easily build the selection panel in the GUI. Clears any forced locale.
*/
public static void setSelectableLocales(ServletContext ctx,
List<Locale> selectableLocales) {
log.debug("Setting selectable locales: " + selectableLocales);
ctx.setAttribute(ATTRIBUTE_NAME, new ContextSelectedLocale(
selectableLocales));
}
/**
* Get the list of selectable Locales from the servlet context. May return
* an empty list, but never returns null.
*/
public static List<Locale> getSelectableLocales(HttpServletRequest req) {
ServletContext ctx = req.getSession().getServletContext();
Object ctxInfo = ctx.getAttribute(ATTRIBUTE_NAME);
if (ctxInfo instanceof ContextSelectedLocale) {
List<Locale> selectableLocales = ((ContextSelectedLocale) ctxInfo)
.getSelectableLocales();
if (selectableLocales != null) {
log.debug("Returning selectable locales: " + selectableLocales);
return selectableLocales;
}
}
log.debug("No selectable locales were found. Returning an empty list.");
return Collections.emptyList();
}
// ----------------------------------------------------------------------
// Bean classes
// ----------------------------------------------------------------------
/** Holds Locale information in the ServletContext. */
protected static class ContextSelectedLocale {
// Only one of these is populated.
private final Locale forcedLocale;
private final List<Locale> selectableLocales;
public ContextSelectedLocale(Locale forcedLocale) {
if (forcedLocale == null) {
throw new NullPointerException("forcedLocale may not be null.");
}
this.forcedLocale = forcedLocale;
this.selectableLocales = Collections.emptyList();
}
public ContextSelectedLocale(List<Locale> selectableLocales) {
if (selectableLocales == null) {
selectableLocales = Collections.emptyList();
}
this.forcedLocale = null;
this.selectableLocales = selectableLocales;
}
public Locale getForcedLocale() {
return forcedLocale;
}
public List<Locale> getSelectableLocales() {
return selectableLocales;
}
@Override
public String toString() {
return "ContextSelectedLocale[forced=" + forcedLocale
+ ", selectable=" + selectableLocales + "]";
}
}
/** Holds Locale information in the Session. */
protected static class SessionSelectedLocale {
private final Locale selectedLocale;
public SessionSelectedLocale(Locale selectedLocale) {
this.selectedLocale = selectedLocale;
}
public Locale getSelectedLocale() {
return selectedLocale;
}
@Override
public String toString() {
return "SessionSelectedLocale[" + selectedLocale + "]";
}
}
}

View file

@ -23,7 +23,7 @@ import org.apache.commons.logging.LogFactory;
public class StartupStatus {
private static final Log log = LogFactory.getLog(StartupStatus.class);
private static final String ATTRIBUTE_NAME = "STARTUP_STATUS";
protected static final String ATTRIBUTE_NAME = "STARTUP_STATUS";
// ----------------------------------------------------------------------
// static methods