diff --git a/doc/install.html b/doc/install.html index 845306e91..1e4064aa9 100644 --- a/doc/install.html +++ b/doc/install.html @@ -651,10 +651,80 @@ - An absolute file path, pointing to the root directory of the Harvester utility. - You must include the final slash. + Force VIVO to use a specific language or Locale instead of those + specified by the browser. + This affects RDF data retrieved from the model, if RDFService.languageFilter is true. + This also affects the text of pages that have been modified to support multiple languages. + + + languages.forceLocale + + + en_US + + + + + + A list of supported languages or Locales that the user may choose to + use instead of the one specified by the browser. Selection images must + be available in the i18n/images directory of the theme. + This affects RDF data retrieved from the model, if RDFService.languageFilter is true. + This also affects the text of pages that have been modified to support multiple languages. + + + + + languages.selectableLocales + + + en, es, fr_FR + + + + + + For developers only. + Defeat the Freemarker template cache, so each template + is read from disk on each request. This permits developers to immediately + see the effect of changes to the template. The default is false, which + means that a cached copy of each template will be used for 60 seconds + before the disk is checked for a new version. +
Setting this option to "true" slows down VIVO performance. + + + + + developer.defeatFreemarkerCache + + + false + + + + + + For developers only. + Defeat the cache of language-specific text strings, + so the language file is read from disk on each request. + This permits developers to immediately + see the effect of changes to the text strings. + The default is false, which means that the language file is + read when VIVO starts up, or when a new theme is selected. +
Setting this option to "true" slows down VIVO performance. + + + + + developer.defeatI18nCache = true + + + false + + +

VI. Compile and deploy

diff --git a/webapp/config/example.runtime.properties b/webapp/config/example.runtime.properties index 691f98139..d0284a389 100644 --- a/webapp/config/example.runtime.properties +++ b/webapp/config/example.runtime.properties @@ -119,3 +119,46 @@ proxy.eligibleTypeList = http://www.w3.org/2002/07/owl#Thing # header supplied by the browser. Default is true if not set. # RDFService.languageFilter = true + +# +# Force VIVO to use a specific language or Locale instead of those +# specified by the browser. This affects RDF data retrieved from the model, +# if RDFService.languageFilter is true. This also affects the text of pages +# that have been modified to support multiple languages. +# +# languages.forceLocale = en_US + +# +# A list of supported languages or Locales that the user may choose to +# use instead of the one specified by the browser. Selection images must +# be available in the i18n/images directory of the theme. This affects +# RDF data retrieved from the model, if RDFService.languageFilter is true. +# This also affects the text of pages that have been modified to support +# multiple languages. +# +# This should not be used with languages.forceLocale, which will override it. +# +# languages.selectableLocales = en, es, fr + +# +# For developers only: Setting this option to "true" slows down VIVO performance. +# +# Defeat the Freemarker template cache, so each template is read from disk +# on each request. This permits developers to immediately see the effect of +# changes to the template. The default is false, which means +# that a cached copy of each template will be used for 60 seconds before +# the disk is checked for a new version. +# +# developer.defeatFreemarkerCache = true + +# +# For developers only: Setting this option to "true" slows down VIVO performance. +# +# Defeat the cache of language-specific text strings, so the language file +# is read from disk on each request. This permits developers to immediately +# see the effect of changes to the text strings. The default is +# false, which means that the language file is read when +# VIVO starts up, or when a new theme is selected. +# +# developer.defeatI18nCache = true + 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 07a370779..6f8f46511 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 @@ -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; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java index 00af47192..5a6e72bd8 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java @@ -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); } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/VivoResourceBundle.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/VitroResourceBundle.java similarity index 94% rename from webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/VivoResourceBundle.java rename to webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/VitroResourceBundle.java index ba314ea36..d4c9754f9 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/VivoResourceBundle.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/VitroResourceBundle.java @@ -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; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionController.java new file mode 100644 index 000000000..523a263a2 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionController.java @@ -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 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); + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionDataGetter.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionDataGetter.java new file mode 100644 index 000000000..627ad04ca --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionDataGetter.java @@ -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: + * + *
+ * {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]
+ *       }
+ *     }  
+ *   }
+ * }
+ * 
+ */ +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 getData(Map valueMap) { + List selectables = SelectedLocale.getSelectableLocales(vreq); + if (selectables.isEmpty()) { + return Collections.emptyMap(); + } + + Map result = new HashMap(); + result.put("selectLocaleUrl", UrlBuilder.getUrl("/selectLocale")); + result.put("locales", buildLocalesList(selectables)); + + Map bodyMap = new HashMap(); + bodyMap.put("selectLocale", result); + log.debug("Sending these values: " + bodyMap); + return bodyMap; + } + + private List> buildLocalesList(List selectables) { + Locale currentLocale = SelectedLocale.getCurrentLocale(vreq); + List> list = new ArrayList>(); + 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 buildLocaleMap(Locale locale, + Locale currentLocale) throws FileNotFoundException { + Map map = new HashMap(); + map.put("code", locale.toString()); + map.put("label", locale.getDisplayName(currentLocale)); + map.put("imageUrl", LocaleSelectorUtilities.getImageUrl(vreq, locale)); + return map; + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionFilter.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionFilter.java new file mode 100644 index 000000000..377638977 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionFilter.java @@ -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); + } + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionSetup.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionSetup.java new file mode 100644 index 000000000..c89819421 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionSetup.java @@ -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 locales = new ArrayList(); + 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. + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectorUtilities.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectorUtilities.java new file mode 100644 index 000000000..0e5eeb492 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectorUtilities.java @@ -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); + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/SelectedLocale.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/SelectedLocale.java new file mode 100644 index 000000000..7ae4586cb --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/SelectedLocale.java @@ -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: + *
    + *
  • The forced Locale in the servlet context
  • + *
  • The selected Locale in the session
  • + *
  • null
  • + *
+ */ + 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: + *
    + *
  • The forced Locale in the servlet context
  • + *
  • The selected Locale in the session
  • + *
  • The Locale from the request
  • + *
  • The default Locale for the JVM
  • + *
+ */ + 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 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 getSelectableLocales(HttpServletRequest req) { + ServletContext ctx = req.getSession().getServletContext(); + Object ctxInfo = ctx.getAttribute(ATTRIBUTE_NAME); + if (ctxInfo instanceof ContextSelectedLocale) { + List 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 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 selectableLocales) { + if (selectableLocales == null) { + selectableLocales = Collections.emptyList(); + } + + this.forcedLocale = null; + this.selectableLocales = selectableLocales; + } + + public Locale getForcedLocale() { + return forcedLocale; + } + + public List 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 + "]"; + } + + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/startup/StartupStatus.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/startup/StartupStatus.java index 0a0601910..6c3e4a3df 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/startup/StartupStatus.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/startup/StartupStatus.java @@ -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 diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionSetupTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionSetupTest.java new file mode 100644 index 000000000..3709d8a43 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionSetupTest.java @@ -0,0 +1,314 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.i18n.selection; + +import static edu.cornell.mannlib.vitro.webapp.i18n.selection.LocaleSelectionSetup.PROPERTY_FORCE_LOCALE; +import static edu.cornell.mannlib.vitro.webapp.i18n.selection.LocaleSelectionSetup.PROPERTY_SELECTABLE_LOCALES; +import static edu.cornell.mannlib.vitro.webapp.i18n.selection.SelectedLocale.ATTRIBUTE_NAME; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import javax.servlet.ServletContextEvent; + +import org.apache.commons.lang.LocaleUtils; +import org.apache.commons.lang.ObjectUtils; +import org.apache.log4j.Level; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import stubs.edu.cornell.mannlib.vitro.webapp.config.ConfigurationPropertiesStub; +import stubs.edu.cornell.mannlib.vitro.webapp.startup.StartupStatusStub; +import stubs.javax.servlet.ServletContextStub; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.i18n.selection.SelectedLocale.ContextSelectedLocale; + +/** + * TODO + */ +public class LocaleSelectionSetupTest extends AbstractTestClass { + // ---------------------------------------------------------------------- + // Infrastructure + // ---------------------------------------------------------------------- + + private LocaleSelectionSetup lss; + private ServletContextStub ctx; + private ServletContextEvent sce; + private ConfigurationPropertiesStub props; + private StartupStatusStub ss; + + private int[] expectedMessageCounts; + private Locale expectedForcedLocale; + private List expectedSelectableLocales; + + @Before + public void setup() { +// setLoggerLevel(LocaleSelectionSetup.class, Level.DEBUG); +// setLoggerLevel(StartupStatusStub.class, Level.DEBUG); + setLoggerLevel(ConfigurationProperties.class, Level.WARN); + + ctx = new ServletContextStub(); + sce = new ServletContextEvent(ctx); + + props = new ConfigurationPropertiesStub(); + props.setBean(ctx); + + ss = new StartupStatusStub(ctx); + + lss = new LocaleSelectionSetup(); + } + + @After + public void checkExpectations() { + if (expectedMessageCounts == null) { + fail("expecteMessages() was not called"); + } + + String message = compareMessageCount("info", ss.getInfoCount(), + expectedMessageCounts[0]) + + compareMessageCount("warning", ss.getWarningCount(), + expectedMessageCounts[1]) + + compareMessageCount("fatal", ss.getFatalCount(), + expectedMessageCounts[2]) + + checkForced() + + checkSelectable(); + if (!message.isEmpty()) { + fail(message); + } + } + + private String compareMessageCount(String label, int actual, int expected) { + if (expected == actual) { + return ""; + } else { + return "expecting " + expected + " " + label + + " messages, but received " + actual + "; "; + } + } + + private String checkForced() { + Locale actual = null; + Object o = ctx.getAttribute(ATTRIBUTE_NAME); + if (o instanceof ContextSelectedLocale) { + actual = ((ContextSelectedLocale) o).getForcedLocale(); + } + + Locale expected = expectedForcedLocale; + if (ObjectUtils.equals(expected, actual)) { + return ""; + } else { + return "expected forced locale of " + expectedForcedLocale + + ", but was " + actual + "; "; + } + } + + private String checkSelectable() { + List actual = Collections.emptyList(); + Object o = ctx.getAttribute(ATTRIBUTE_NAME); + if (o instanceof ContextSelectedLocale) { + actual = ((ContextSelectedLocale) o).getSelectableLocales(); + } + + List expected = expectedSelectableLocales; + if (expected == null) { + expected = Collections.emptyList(); + } + + if (ObjectUtils.equals(expected, actual)) { + return ""; + } else { + return "expected selectable locales of " + expected + ", but was " + + actual + "; "; + } + } + + // ---------------------------------------------------------------------- + // The tests + // ---------------------------------------------------------------------- + + // General functionality + + @Test + public void neitherPropertyIsSpecified() { + lss.contextInitialized(sce); + expectMessages(1, 0, 0); + } + + @Test + public void forceSuccessL() { + props.setProperty(PROPERTY_FORCE_LOCALE, "es"); + lss.contextInitialized(sce); + expectForced("es"); + expectMessages(1, 0, 0); + } + + @Test + public void forceSuccessL_C() { + props.setProperty(PROPERTY_FORCE_LOCALE, "es_ES"); + lss.contextInitialized(sce); + expectForced("es_ES"); + expectMessages(1, 0, 0); + } + + @Test + public void forceSuccessL_C_V() { + props.setProperty(PROPERTY_FORCE_LOCALE, "no_NO_NY"); + lss.contextInitialized(sce); + expectForced("no_NO_NY"); + expectMessages(1, 0, 0); + } + + @Test + public void oneSelectable() { + props.setProperty(PROPERTY_SELECTABLE_LOCALES, "fr_FR"); + lss.contextInitialized(sce); + expectSelectable("fr_FR"); + expectMessages(1, 0, 0); + } + + @Test + public void twoSelectables() { + props.setProperty(PROPERTY_SELECTABLE_LOCALES, "fr_FR, es_PE"); + lss.contextInitialized(sce); + expectSelectable("fr_FR", "es_PE"); + expectMessages(1, 0, 0); + } + + @Test + public void bothPropertiesAreSpecified() { + props.setProperty(PROPERTY_FORCE_LOCALE, "es_ES"); + props.setProperty(PROPERTY_SELECTABLE_LOCALES, "fr_FR"); + lss.contextInitialized(sce); + expectForced("es_ES"); + expectMessages(1, 1, 0); + } + + // Locale string syntax (common to both force and selectable) + + @Test + public void langaugeIsEmpty() { + props.setProperty(PROPERTY_FORCE_LOCALE, "_ES"); + lss.contextInitialized(sce); + expectMessages(0, 1, 0); + } + + @Test + public void languageWrongLength() { + props.setProperty(PROPERTY_FORCE_LOCALE, "e_ES"); + lss.contextInitialized(sce); + expectMessages(0, 1, 0); + } + + @Test + public void languageNotAlphabetic() { + props.setProperty(PROPERTY_FORCE_LOCALE, "e4_ES"); + lss.contextInitialized(sce); + expectMessages(0, 1, 0); + } + + @Test + public void languageNotLowerCase() { + props.setProperty(PROPERTY_FORCE_LOCALE, "eS_ES"); + lss.contextInitialized(sce); + expectMessages(0, 1, 0); + } + + @Test + public void countryIsEmpty() { + props.setProperty(PROPERTY_FORCE_LOCALE, "es_ _13"); + lss.contextInitialized(sce); + expectMessages(0, 1, 0); + } + + @Test + public void countryWrongLength() { + props.setProperty(PROPERTY_FORCE_LOCALE, "es_ESS"); + lss.contextInitialized(sce); + expectMessages(0, 1, 0); + } + + @Test + public void countryNotAlphabetic() { + props.setProperty(PROPERTY_FORCE_LOCALE, "es_E@"); + lss.contextInitialized(sce); + expectMessages(0, 1, 0); + } + + @Test + public void countryNotUpperCase() { + props.setProperty(PROPERTY_FORCE_LOCALE, "es_es"); + lss.contextInitialized(sce); + expectMessages(0, 1, 0); + } + + @Test + public void variantIsEmpty() { + props.setProperty(PROPERTY_FORCE_LOCALE, "es_ES_"); + lss.contextInitialized(sce); + expectMessages(0, 1, 0); + } + + @Test + public void funkyVariantIsAcceptable() { + props.setProperty(PROPERTY_FORCE_LOCALE, "es_ES_123_aa"); + lss.contextInitialized(sce); + expectForced("es_ES_123_aa"); + expectMessages(1, 1, 0); + } + + @Test + public void localeNotRecognizedProducesWarning() { + props.setProperty(PROPERTY_FORCE_LOCALE, "es_FR"); + lss.contextInitialized(sce); + expectForced("es_FR"); + expectMessages(1, 1, 0); + } + + // Syntax of selectable property + + @Test + public void emptySelectableLocaleProducesWarning() { + props.setProperty(PROPERTY_SELECTABLE_LOCALES, "es_ES, , fr_FR"); + lss.contextInitialized(sce); + expectSelectable("es_ES", "fr_FR"); + expectMessages(1, 1, 0); + } + + @Test + public void blanksAroundCommasAreIgnored() { + props.setProperty(PROPERTY_SELECTABLE_LOCALES, "es_ES,en_US \t , fr_FR"); + lss.contextInitialized(sce); + expectSelectable("es_ES", "en_US", "fr_FR"); + expectMessages(1, 0, 0); + } + + // ---------------------------------------------------------------------- + // helper methods + // ---------------------------------------------------------------------- + + private void expectMessages(int infoCount, int warningCount, int fatalCount) { + this.expectedMessageCounts = new int[] { infoCount, warningCount, + fatalCount }; + } + + private void expectForced(String localeString) { + this.expectedForcedLocale = stringToLocale(localeString); + } + + private void expectSelectable(String... strings) { + List list = new ArrayList(); + for (String string : strings) { + list.add(stringToLocale(string)); + } + this.expectedSelectableLocales = list; + } + + private Locale stringToLocale(String string) { + return LocaleUtils.toLocale(string); + } +} diff --git a/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/startup/StartupStatusStub.java b/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/startup/StartupStatusStub.java new file mode 100644 index 000000000..b7e068507 --- /dev/null +++ b/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/startup/StartupStatusStub.java @@ -0,0 +1,140 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package stubs.edu.cornell.mannlib.vitro.webapp.startup; + +import java.util.List; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextListener; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.startup.StartupStatus; + +/** + * Keep track of how many messages come in. + */ +public class StartupStatusStub extends StartupStatus { + private static final Log log = LogFactory.getLog(StartupStatusStub.class); + + // ---------------------------------------------------------------------- + // Stub infrastructure + // ---------------------------------------------------------------------- + + private int infoCount = 0; + private int warningCount = 0; + private int fatalCount = 0; + + public StartupStatusStub(ServletContext ctx) { + ctx.setAttribute(ATTRIBUTE_NAME, this); + } + + public int getInfoCount() { + return infoCount; + } + + public int getWarningCount() { + return warningCount; + } + + public int getFatalCount() { + return fatalCount; + } + + // ---------------------------------------------------------------------- + // Stub methods + // ---------------------------------------------------------------------- + + @Override + public void info(ServletContextListener listener, String message) { + log.debug("INFO: " + message); + infoCount++; + } + + @Override + public void info(ServletContextListener listener, String message, + Throwable cause) { + log.debug("INFO: " + message + " " + cause); + infoCount++; + } + + @Override + public void warning(ServletContextListener listener, String message) { + log.debug("WARNING: " + message); + warningCount++; + } + + @Override + public void warning(ServletContextListener listener, String message, + Throwable cause) { + log.debug("WARNING: " + message + " " + cause); + warningCount++; + } + + @Override + public void fatal(ServletContextListener listener, String message) { + log.debug("FATAL: " + message); + fatalCount++; + } + + @Override + public void fatal(ServletContextListener listener, String message, + Throwable cause) { + log.debug("FATAL: " + message + " " + cause); + fatalCount++; + } + + // ---------------------------------------------------------------------- + // Un-implemented methods + // ---------------------------------------------------------------------- + + @Override + public void listenerNotExecuted(ServletContextListener listener) { + throw new RuntimeException( + "StartupStatusStub.listenerNotExecuted() not implemented."); + } + + @Override + public void listenerExecuted(ServletContextListener listener) { + throw new RuntimeException( + "StartupStatusStub.listenerExecuted() not implemented."); + } + + @Override + public boolean allClear() { + throw new RuntimeException( + "StartupStatusStub.allClear() not implemented."); + } + + @Override + public boolean isStartupAborted() { + throw new RuntimeException( + "StartupStatusStub.isStartupAborted() not implemented."); + } + + @Override + public List getStatusItems() { + throw new RuntimeException( + "StartupStatusStub.getStatusItems() not implemented."); + } + + @Override + public List getErrorItems() { + throw new RuntimeException( + "StartupStatusStub.getErrorItems() not implemented."); + } + + @Override + public List getWarningItems() { + throw new RuntimeException( + "StartupStatusStub.getWarningItems() not implemented."); + } + + @Override + public List getItemsForListener(ServletContextListener listener) { + throw new RuntimeException( + "StartupStatusStub.getItemsForListener() not implemented."); + } + +} diff --git a/webapp/themes/vitro/templates/identity.ftl b/webapp/themes/vitro/templates/identity.ftl index f102bd740..127c66fc6 100644 --- a/webapp/themes/vitro/templates/identity.ftl +++ b/webapp/themes/vitro/templates/identity.ftl @@ -9,6 +9,7 @@