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

@ -651,10 +651,80 @@
</tr>
<tr>
<td colspan="2">
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.
</td>
</tr>
<tr class="odd_row">
<td>
languages.forceLocale
</td>
<td>
en_US
</td>
</tr>
<tr>
<td colspan="2">
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.
</td>
</tr>
<tr class="odd_row">
<td>
languages.selectableLocales
</td>
<td>
en, es, fr_FR
</td>
</tr>
<tr>
<td colspan="2">
<b>For developers only.</b>
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 <code>false</code>, which
means that a cached copy of each template will be used for 60 seconds
before the disk is checked for a new version.
<br/><b>Setting this option to "true" slows down VIVO performance.</b>
</td>
</tr>
<tr class="odd_row">
<td>
developer.defeatFreemarkerCache
</td>
<td>
false
</td>
</tr>
<tr>
<td colspan="2">
<b>For developers only.</b>
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 <code>false</code>, which means that the language file is
read when VIVO starts up, or when a new theme is selected.
<br/><b>Setting this option to "true" slows down VIVO performance.</b>
</td>
</tr>
<tr class="odd_row">
<td>
developer.defeatI18nCache = true
</td>
<td>
false
</td>
</tr>
</tbody>
</table>
<h3 id="deploy">VI. Compile and deploy</h3>

View file

@ -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 <code>false</code>, 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
# <code>false</code>, which means that the language file is read when
# VIVO starts up, or when a new theme is selected.
#
# developer.defeatI18nCache = true

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

View file

@ -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<Locale> 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<Locale> actual = Collections.emptyList();
Object o = ctx.getAttribute(ATTRIBUTE_NAME);
if (o instanceof ContextSelectedLocale) {
actual = ((ContextSelectedLocale) o).getSelectableLocales();
}
List<Locale> 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<Locale> list = new ArrayList<Locale>();
for (String string : strings) {
list.add(stringToLocale(string));
}
this.expectedSelectableLocales = list;
}
private Locale stringToLocale(String string) {
return LocaleUtils.toLocale(string);
}
}

View file

@ -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<StatusItem> getStatusItems() {
throw new RuntimeException(
"StartupStatusStub.getStatusItems() not implemented.");
}
@Override
public List<StatusItem> getErrorItems() {
throw new RuntimeException(
"StartupStatusStub.getErrorItems() not implemented.");
}
@Override
public List<StatusItem> getWarningItems() {
throw new RuntimeException(
"StartupStatusStub.getWarningItems() not implemented.");
}
@Override
public List<StatusItem> getItemsForListener(ServletContextListener listener) {
throw new RuntimeException(
"StartupStatusStub.getItemsForListener() not implemented.");
}
}

View file

@ -9,6 +9,7 @@
<nav role="navigation">
<ul id="header-nav" role="list">
<#include "languageSelector.ftl">
<li role="listitem"><a href="${urls.index}" title="index">Index</a></li>
<#if user.loggedIn>
<#if user.hasSiteAdminAccess>

View file

@ -55,6 +55,9 @@ edu.cornell.mannlib.vitro.webapp.services.shortview.ShortViewServiceSetup
edu.ucsf.vitro.opensocial.OpenSocialSmokeTests
# For multiple language support
edu.cornell.mannlib.vitro.webapp.i18n.selection.LocaleSelectionSetup
# The Solr index uses a "public" permission, so the PropertyRestrictionPolicyHelper
# and the PermissionRegistry must already be set up.
edu.cornell.mannlib.vitro.webapp.search.solr.SolrSetup

View file

@ -78,6 +78,16 @@
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<description>Override the Locale in the HttpRequest, if appropriate.</description>
<filter-name>Locale selection filter</filter-name>
<filter-class>edu.cornell.mannlib.vitro.webapp.i18n.selection.LocaleSelectionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>Locale selection filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>JSession Strip Filter</filter-name>
<filter-class>edu.cornell.mannlib.vitro.webapp.filters.JSessionStripFilter</filter-class>
@ -1338,6 +1348,16 @@
<url-pattern>/orng/*</url-pattern>
</servlet-mapping>
<servlet>
<description>Multiple-language support. Allows user to select his preferred langauge</description>
<servlet-name>LocaleSelectionController</servlet-name>
<servlet-class>edu.cornell.mannlib.vitro.webapp.i18n.selection.LocaleSelectionController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LocaleSelectionController</servlet-name>
<url-pattern>/selectLocale</url-pattern>
</servlet-mapping>
<!-- ==================== tag libraries ============================== -->
<jsp-config>

View file

@ -8,6 +8,7 @@
<nav role="navigation">
<ul id="header-nav" role="list">
<#include "languageSelector.ftl">
<#if user.loggedIn>
<li role="listitem">${user.loginName}</li>
<li role="listitem"><a href="${urls.logout}" title="End your session">Log out</a></li>

View file

@ -0,0 +1,31 @@
<#-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<#--
How can this done with images instead of buttons containing images?
Why don't the "alt" values show as tooltips?"
What was the right way to do this?
-->
<#-- This is included by identity.ftl -->
<#if selectLocale??>
<li>
<form method="get" action="${selectLocale.selectLocaleUrl}" >
<#list selectLocale.locales as locale>
<button type="submit" name="selection" value="${locale.code}">
<img src="${locale.imageUrl}" height="15" align="middle" alt="${locale.label}"/>
</button>
<#if locale_has_next>|</#if>
</#list>
</form>
</li>
</#if>
<#--
* selectLocale
* -- selectLocaleUrl
* -- locales [list of maps]
* -- [map]
* -- code
* -- label (tooltip relative to the current Locale)
* -- imageUrl
-->