/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.common.impl.language;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.impl.util.xml.XPathHelper;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.language.Localizable;
import ch.entwine.weblounge.common.language.UnknownLanguageException;
import ch.entwine.weblounge.common.site.Site;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
/**
* <code>LanguageSupport</code> is a helper class the facilitates the handling
* of languages in numerous ways.
*/
public final class LanguageUtils {
/** Logging facility */
private static final Logger logger = LoggerFactory.getLogger(LanguageUtils.class);
/** Regular expression to extract CH_de style Accept-Language headers */
private static final Pattern ACCEPT_LANGUAGE_HEADER = Pattern.compile("([\\w][\\w])_([\\w][\\w])");
/** Globally available languages */
private static final Map<String, Language> systemLanguages = new HashMap<String, Language>();
/**
* This class is not meant to be instantiated.
*/
private LanguageUtils() {
// Nothing to be done here
}
/**
* Returns the language object that represents the given locale.
*
* @param locale
* the locale
* @return the language
* @throws UnknownLanguageException
* if there is no language for the given locale
*/
public static Language getLanguage(Locale locale)
throws UnknownLanguageException {
// Do we know this language already?
Language language = systemLanguages.get(locale.getLanguage());
if (language != null)
return language;
// Makes sure we get the locale in the right format (might be hand crafted)
Matcher matcher = ACCEPT_LANGUAGE_HEADER.matcher(locale.getLanguage());
if (matcher.matches()) {
locale = new Locale(matcher.group(2), matcher.group(1));
}
// Check the system locales for a match
Locale systemLocale = null;
try {
for (Locale l : Locale.getAvailableLocales()) {
if (l.getISO3Language().equals(locale.getISO3Language())) {
systemLocale = l;
break;
} else if (l.getLanguage().equals(l.getLanguage())) {
systemLocale = l;
break;
}
}
} catch (MissingResourceException e) {
logger.debug("No 3 found for '{}': {}", locale, e.getMessage());
}
// Is there a matching system locale?
if (systemLocale != null) {
language = new LanguageImpl(locale);
systemLanguages.put(locale.getLanguage(), language);
return language;
}
// Apparently not...
throw new UnknownLanguageException(locale.getLanguage());
}
/**
* Returns the language object identified by the language identifier.
*
* @param languageCode
* the language identifier
* @return the language
* @throws UnknownLanguageException
* if there is no language for the given locale
*/
public static Language getLanguage(String languageCode)
throws UnknownLanguageException {
Language language = systemLanguages.get(languageCode);
if (language != null)
return language;
for (Locale locale : Locale.getAvailableLocales()) {
if (locale.getLanguage().equals(languageCode)) {
language = new LanguageImpl(new Locale(languageCode, "", ""));
systemLanguages.put(languageCode, language);
break;
}
}
if (language == null)
throw new UnknownLanguageException(languageCode);
return language;
}
/**
* Reads the names of an object described in a weblounge configuration file in
* various languages and applies them to the multilingual object
* <code>o</code>.
* <p>
* The localized content is looked up in the tags specified by parameter
* <code>tagName</code>, language identifier are expected in the
* <code>language</code> attribute of these tags.
* <p>
* If the description is found in the default language, then
* {@link LocalizableContent#setDefaultLanguage(Language)} is called.
* <p>
* The required format of the input node is as follows:
*
* <pre>
* <role>
* <id>editor</id>
* <name language="de">Editor</name>
* <name language="fr">Editeur</name>
* <name language="it">Editore</name>
* </role>
* </pre>
* <p>
* The method throws a <code>ConfigurationException</code> if no
* name is provided the site default language.
*
* @param configuration
* the XML configuration node containing the descriptions
* @param tagName
* the tag name containing the localized content
* @param defaultLanguage
* the default language
* @param o
* the localizable object
* @param escape
* <code>true</code> to filter out " and '
* @return the localized content
*/
public static LocalizableContent<String> addDescriptions(Node configuration,
String tagName, Language defaultLanguage, LocalizableContent<String> o,
boolean escape) {
XPath xpath = XPathFactory.newInstance().newXPath();
return addDescriptions(configuration, tagName, defaultLanguage, o, escape, xpath);
}
/**
* Reads the names of an object described in a weblounge configuration file in
* various languages and applies them to the multilingual object
* <code>o</code>.
* <p>
* The localized content is looked up in the tags specified by parameter
* <code>tagName</code>, language identifier are expected in the
* <code>language</code> attribute of these tags.
* <p>
* If the description is found in the default language, then
* {@link LocalizableContent#setDefaultLanguage(Language)} is called.
* <p>
* The required format of the input node is as follows:
*
* <pre>
* <role>
* <id>editor</id>
* <name language="de">Editor</name>
* <name language="fr">Editeur</name>
* <name language="it">Editore</name>
* </role>
* </pre>
* <p>
* The method throws a <code>ConfigurationException</code> if no
* name is provided the site default language.
*
* @param configuration
* the XML configuration node containing the descriptions
* @param tagName
* the tag name containing the localized content
* @param defaultLanguage
* the default language
* @param o
* the localizable object
* @param escape
* <code>true</code> to filter out " and '
* @param xpath
* the xpath processor
* @return the localized content
*/
public static LocalizableContent<String> addDescriptions(Node configuration,
String tagName, Language defaultLanguage, LocalizableContent<String> o,
boolean escape, XPath xpath) {
if (configuration == null)
throw new IllegalArgumentException("Cannot extract from empty configuration");
if (tagName == null)
throw new IllegalArgumentException("Tagname must be specified");
if (o == null)
o = new LocalizableContent<String>();
NodeList nodes = XPathHelper.selectList(configuration, tagName, xpath);
for (int i = 0; i < nodes.getLength(); i++) {
Node name = nodes.item(i);
String description = XPathHelper.valueOf(name, "text()", xpath);
String lAttrib = XPathHelper.valueOf(name, "@language", xpath);
Language language = LanguageUtils.getLanguage(lAttrib);
if (language == null) {
logger.debug("Found name in unsupported language {}", lAttrib);
continue;
}
// Escape?
if (escape) {
description = description.replaceAll("\"", "");
description = description.replaceAll("'", "");
}
// Add the entry
logger.debug("Found description {}", description);
o.put(description, language);
if (language.equals(defaultLanguage)) {
o.setDefaultLanguage(defaultLanguage);
}
}
return o;
}
/**
* Returns the localized variant for the given language.
* <p>
* For example, if <code>s</code> is <tt>file.jsp</tt> and
* <code>language</code> is <tt>German</tt>, then this method returns
* <tt>file_de.jsp</tt>.
*
* @param s
* the file name
* @param language
* the language variant to obtain
* @return the localized variant of the text
*/
public static String getLanguageVariant(String s, Language language) {
int suffixPosition = s.lastIndexOf(".");
String suffix = "";
if (suffixPosition > -1) {
suffix = s.substring(suffixPosition);
s = s.substring(0, suffixPosition);
}
s += "_" + language.getIdentifier() + suffix;
return s;
}
/**
* Returns all language variants of the filename <code>s</code>, including
* <code>s</code> itself (last in line).
*
* @param s
* the filename
* @param languages
* the languages used to build the variants
*/
public static String[] getLanguageVariants(String s, Language... languages) {
String[] result = new String[languages.length + 1];
result[languages.length] = s;
for (int i = 0; i < languages.length; i++) {
Language language = languages[i];
result[i] = getLanguageVariant(s, language);
}
return result;
}
/**
* Returns the original version string of text <code>s</code>.
* <p>
* For example, if <tt>s</tt> equals <tt>file_de.jsp</tt> then this method
* returns <tt>file.jsp</tt>.
*
* @param s
* the language filename
* @param languages
* the languages
* @return the original filename
*/
public static String getBaseVersion(String s) {
Language l = extractLanguage(s);
if (l == null) {
return s;
}
int languagePos = s.indexOf("_" + l.getIdentifier());
String original = s.substring(0, languagePos);
if (s.length() > languagePos + 3) {
String suffix = s.substring(languagePos + 3);
original += suffix;
}
return original;
}
/**
* Returns the language of this file. For example, if <tt>s</tt> is
* <tt>file_de.jsp</tt> then this method returns the German language object.
* <p>
* <b>Note:</b> This method returns <code>null</code> if the string contains
* an unknown language identifier or no language identifier at all.
*
* @param s
* the filename
* @return the language object or <code>null</code>
*/
public static Language extractLanguage(String s) {
int languagePosition = s.lastIndexOf("_");
if ((languagePosition < 0) || (languagePosition + 1 > s.length()))
return null;
Language l = null;
String languageId = s.substring(languagePosition + 1, languagePosition + 3);
l = getLanguage(languageId);
return l;
}
/**
* Returns the language out of <code>choices</code> that matches the client's
* requirements as indicated through the <code>Accept-Language</code> header.
* If no match is possible, <code>null</code> is returned.
*
* @param choices
* the available locales
* @param request
* the http request
*/
public static Language getPreferredLanguage(Set<Language> choices,
HttpServletRequest request) {
if (request.getHeader("Accept-Language") != null) {
Enumeration<?> locales = request.getLocales();
while (locales.hasMoreElements()) {
try {
Language l = getLanguage((Locale) locales.nextElement());
if (choices.contains(l))
return l;
} catch (UnknownLanguageException e) {
// never mind, some clients will send stuff like "*" as the locale
}
}
}
return null;
}
/**
* Returns the preferred one out of of those languages that are requested by
* the client through the <code>Accept-Language</code> header and are
* supported by both the localizable and the site.
* <p>
* The preferred one is defined by the following priorities:
* <ul>
* <li>Requested by the client</li>
* <li>The localizable's original language</li>
* <li>The site default language</li>
* <li>The first language of what is supported by both the localizable and the
* site</li>
* </ul>
*
* @param localizable
* the localizable
* @param request
* the http request
* @param site
* the site
*/
public static Language getPreferredLanguage(Localizable localizable,
HttpServletRequest request, Site site) {
// Path
String[] pathElements = StringUtils.split(request.getRequestURI(), "/");
for (String element : pathElements) {
for (Language l : localizable.languages()) {
if (l.getIdentifier().equals(element)) {
return l;
}
}
}
// Accept-Language header
if (request.getHeader("Accept-Language") != null) {
Enumeration<?> locales = request.getLocales();
while (locales.hasMoreElements()) {
try {
Language l = getLanguage((Locale) locales.nextElement());
if (localizable != null && !localizable.supportsLanguage(l))
continue;
if (!site.supportsLanguage(l))
continue;
return l;
} catch (UnknownLanguageException e) {
// never mind, some clients will send stuff like "*" as the locale
}
}
}
// The localizable's original language
if (localizable != null && localizable instanceof Resource) {
Resource<?> r = (Resource<?>) localizable;
if (r.getOriginalContent() != null) {
if (site.supportsLanguage(r.getOriginalContent().getLanguage()))
return r.getOriginalContent().getLanguage();
}
}
// Site default language
if (localizable != null && localizable.supportsLanguage(site.getDefaultLanguage())) {
return site.getDefaultLanguage();
}
// Any match
if (localizable != null) {
for (Language l : site.getLanguages()) {
if (localizable.supportsLanguage(l)) {
return l;
}
}
}
return null;
}
/**
* Returns the preferred one out of of those languages that are requested by
* the client through the <code>Accept-Language</code> header and are
* supported by both the resource in that there is resource content in that
* language and the site.
* <p>
* The preferred one is defined by the following priorities:
* <ul>
* <li>Requested by the client</li>
* <li>The resource's original language</li>
* <li>The site default language</li>
* <li>The first language of what is supported by both the resource and the
* site</li>
* </ul>
*
* @param resource
* the resource
* @param request
* the http request
* @param site
* the site
*/
public static Language getPreferredContentLanguage(Resource<?> resource,
HttpServletRequest request, Site site) {
if (resource == null)
throw new IllegalArgumentException("Resource must not be null");
// Path
String[] pathElements = StringUtils.split(request.getRequestURI(), "/");
for (String element : pathElements) {
for (Language l : resource.contentLanguages()) {
if (l.getIdentifier().equals(element)) {
return l;
}
}
}
// Accept-Language header
if (request.getHeader("Accept-Language") != null) {
Enumeration<?> locales = request.getLocales();
while (locales.hasMoreElements()) {
try {
Language l = getLanguage((Locale) locales.nextElement());
if (l == null)
continue;
if (!resource.supportsContentLanguage(l))
continue;
if (!site.supportsLanguage(l))
continue;
return l;
} catch (UnknownLanguageException e) {
// never mind, some clients will send stuff like "*" as the locale
}
}
}
// Original content
if (resource.getOriginalContent() != null) {
if (site.supportsLanguage(resource.getOriginalContent().getLanguage()))
return resource.getOriginalContent().getLanguage();
}
// Site default language
if (resource.supportsContentLanguage(site.getDefaultLanguage())) {
return site.getDefaultLanguage();
}
// Any match
for (Language l : site.getLanguages()) {
if (resource.supportsContentLanguage(l)) {
return l;
}
}
return null;
}
/**
* Returns the preferred one out of of those languages that are requested by
* the client through the <code>Accept-Language</code> header and are
* supported by the site. If there is no match, the site's default language is
* returned.
* <p>
* The preferred one is defined by the following priorities:
* <ul>
* <li>Requested by the client</li>
* <li>The site default language</li>
* </ul>
*
* @param request
* the http request
* @param site
* the site
*/
public static Language getPreferredLanguage(HttpServletRequest request,
Site site) {
// Accept-Language header
if (request.getHeader("Accept-Language") != null) {
Enumeration<?> locales = request.getLocales();
while (locales.hasMoreElements()) {
try {
Language l = getLanguage((Locale) locales.nextElement());
if (site.supportsLanguage(l))
return l;
} catch (UnknownLanguageException e) {
// never mind, some clients will send stuff like "*" as the locale
}
}
}
return site.getDefaultLanguage();
}
/**
* Returns the first language out of <code>choices</code> that is supported by
* the <code>localizable</code>. If there is no such language,
* <code>null</code> is returned.
*
* @param localizable
* the localizable object
* @param languages
* the prioritized list of possible languages
* @return the first matching language or <code>null</code>
* @throws IllegalArgumentException
* if <code>localizable</code> is <code>null</code>
*/
public static Language getPreferredLanguage(Localizable localizable,
Language... languages) {
if (localizable == null)
throw new IllegalArgumentException("Localizable cannot be null");
if (languages == null)
return null;
for (Language l : languages) {
if (localizable.supportsLanguage(l))
return l;
}
return null;
}
/**
* Returns the first language out of <code>choices</code> that is supported by
* the <code>localizable</code>. If there is no such language,
* <code>null</code> is returned.
*
* @param localizable
* the localizable object
* @param languages
* the prioritized list of possible languages
* @return the first matching language or <code>null</code>
* @throws IllegalArgumentException
* if <code>localizable</code> is <code>null</code>
*/
public static Language getPreferredContentLanguage(Resource<?> resource,
Language... languages) {
if (resource == null)
throw new IllegalArgumentException("Resource cannot be null");
if (languages == null)
return null;
for (Language l : languages) {
if (resource.supportsContentLanguage(l))
return l;
}
return null;
}
}