| /* |
| * Copyright 1999-2004 The Apache Software Foundation. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.cocoon.matching; |
| |
| import org.apache.avalon.framework.activity.Disposable; |
| import org.apache.avalon.framework.configuration.Configurable; |
| import org.apache.avalon.framework.configuration.Configuration; |
| import org.apache.avalon.framework.logger.AbstractLogEnabled; |
| import org.apache.avalon.framework.parameters.Parameters; |
| import org.apache.avalon.framework.service.ServiceException; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.avalon.framework.service.Serviceable; |
| import org.apache.avalon.framework.thread.ThreadSafe; |
| |
| import org.apache.cocoon.i18n.I18nUtils; |
| import org.apache.cocoon.sitemap.PatternException; |
| |
| import org.apache.commons.lang.StringUtils; |
| import org.apache.excalibur.source.Source; |
| import org.apache.excalibur.source.SourceResolver; |
| |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| /** |
| * A matcher that locates and identifies to the pipeline a source document to |
| * be used as the content for an i18n site, based upon a locale provided in a |
| * range of ways. |
| * |
| * <h1>Configuration</h1> |
| * <p>A sample configuration (given in the <map:matchers> section of the |
| * sitemap) is given below. This configuration shows default values. |
| * </p> |
| * <pre> |
| * <map:matcher name="i18n" src="org.apache.cocoon.matching.LocaleMatcher"> |
| * <locale-attribute>locale</locale-attribute> |
| * <negotiate>false</negotiate> |
| * <use-locale>true</use-locale> |
| * <use-locales>false</use-locales> |
| * <use-blank-locale>true</use-blank-locale> |
| * <default-locale language="en" country="US"/> |
| * <store-in-request>false<store-in-request> |
| * <create-session>false<create-session> |
| * <store-in-session>false<store-in-session> |
| * <store-in-cookie>false<store-in-cookie> |
| * </map:matcher> |
| * </pre> |
| * |
| * <p>Above configuration parameters mean: |
| * <ul> |
| * <li><b>locale-attribute</b> specifies the name of the request |
| * parameter / session attribute / cookie that is to be used as a locale |
| * (defaults to <code>locale</code>)</li> |
| * <li><b>negotiate</b> specifies whether matcher should check that |
| * resource exists. If set to true, matcher will look for the locale |
| * till matching resource is found. If no resource found even with |
| * default or blank locale, matcher will not match.</li> |
| * <li><b>use-locale</b> specifies whether the primary locale provided |
| * by the user agent (or server default, is no locale passed by the agent) |
| * is to be used</li> |
| * <li><b>use-locales</b> specifies whether each locale provided by the |
| * user agent should be tested in turn (makes sense only when |
| * <code>negotiate</code> is set to <code>true</code>)</li> |
| * <li><b>default-locale</b> specifies the default locale to be used when |
| * none matches any of the previous ones.</li> |
| * <li><b>use-blank-locale</b> specifies whether a file should be looked |
| * for without a locale in its filename or filepath (e.g. after looking |
| * for index.en.html, try index.html) if none matches any of the previous |
| * locales.</li> |
| * <li><b>store-in-request</b> specifies whether found locale should be |
| * stored as request attribute.</li> |
| * <li><b>create-session</b> specifies whether session should be created |
| * when storing found locale as session attribute.</li> |
| * <li><b>store-in-session</b> specifies whether found locale should be |
| * stored as session attribute.</li> |
| * <li><b>store-in-cookie</b> specifies whether found locale should be |
| * stored as cookie.</li> |
| * </ul> |
| * </p> |
| * |
| * <h1>Usage</h1> |
| * <p>This matcher will be used in a pipeline like so:</p> |
| * <pre> |
| * <map:match pattern="*.html"> |
| * <map:match type="i18n" pattern="xml/{1}.*.xml"> |
| * <map:generate src="{source}"/> |
| * ... |
| * </map:match> |
| * </map:match> |
| * </pre> |
| * <p><code>*</code> in the pattern identifies the place where locale should |
| * be inserted. In case of a blank locale, if character before and after |
| * <code>*</code> is the same (like in example above), duplicate will |
| * be removed (<code>xml/{1}.*.xml</code> becomes <code>xml/{1}.xml</code>).</p> |
| * |
| * <h1>Locale Identification</h1> |
| * <p>Locales will be tested in following order:</p> |
| * <ul> |
| * <li>Locale provided as a request parameter</li> |
| * <li>Locale provided as a session attribute</li> |
| * <li>Locale provided as a cookie</li> |
| * <li>Locale provided using a sitemap parameter<br> |
| * (<map:parameter name="locale" value="{1}"/> style parameter within |
| * the <map:match> node)</li> |
| * <li>Locale provided by the user agent, or server default, |
| * if <code>use-locale</code> is set to <code>true</code></li> |
| * <li>Locales provided by the user agent, if <code>use-locales</code> |
| * is set to <code>true</code>.</li> |
| * <li>The default locale, if specified in the matcher's configuration</li> |
| * <li>Resources with no defined locale (blank locale)</li> |
| * </ul> |
| * <p>If <code>negotiate</code> mode is set to <code>true</code>, a source will |
| * be looked up using each locale. Where the full locale (language, country, |
| * variant) doesn't match, it will fall back first to language and country, |
| * and then just language, before moving on to the next locale.</p> |
| * <p>If <code>negotiate</code> mode is set to <code>false</code> (default), |
| * first found locale will be returned.</p> |
| * |
| * <h1>Sitemap Variables</h1> |
| * <p>Once a matching locale has been found, the following sitemap variables |
| * will be available to sitemap elements contained within the matcher:</p> |
| * <ul> |
| * <li>{source}: The URI of the source that matched</li> |
| * <li>{locale}: The locale that matched that resource</li> |
| * <li>{matched-locale}: The part of the locale that matched the resource</li> |
| * <li>{language}: The language of the matching resource</li> |
| * <li>{country}: The country of the matching resource</li> |
| * <li>{variant}: The variant of the matching resource</li> |
| * </ul> |
| * |
| * @since 2.1.6 |
| * @version $Id$ |
| */ |
| public class LocaleMatcher extends AbstractLogEnabled |
| implements Matcher, ThreadSafe, Serviceable, Configurable, Disposable { |
| |
| private static final String DEFAULT_LOCALE_ATTRIBUTE = "locale"; |
| private static final String DEFAULT_DEFAULT_LANG = "en"; |
| private static final String DEFAULT_DEFAULT_COUNTRY = "US"; |
| private static final String DEFAULT_DEFAULT_VARIANT = ""; |
| |
| private ServiceManager manager; |
| private SourceResolver resolver; |
| |
| /** |
| * Name of the locale request parameter, session attribute, cookie. |
| */ |
| private String localeAttribute; |
| |
| /** |
| * Whether to query locale provided by the user agent or not. |
| */ |
| private boolean useLocale; |
| |
| private boolean useLocales; |
| private Locale defaultLocale; |
| private boolean useBlankLocale; |
| private boolean testResourceExists; |
| |
| /** |
| * Store the locale in request. Default is not to do this. |
| */ |
| private boolean storeInRequest; |
| |
| /** |
| * Store the locale in session, if available. Default is not to do this. |
| */ |
| private boolean storeInSession; |
| |
| /** |
| * Should we create a session if needed. Default is not to do this. |
| */ |
| private boolean createSession; |
| |
| /** |
| * Should we add a cookie with the locale. Default is not to do this. |
| */ |
| private boolean storeInCookie; |
| |
| |
| public void service(ServiceManager manager) throws ServiceException { |
| this.manager = manager; |
| this.resolver = (SourceResolver)this.manager.lookup(SourceResolver.ROLE); |
| } |
| |
| public void configure(Configuration config) { |
| this.storeInRequest = config.getChild("store-in-request").getValueAsBoolean(false); |
| this.createSession = config.getChild("create-session").getValueAsBoolean(false); |
| this.storeInSession = config.getChild("store-in-session").getValueAsBoolean(false); |
| this.storeInCookie = config.getChild("store-in-cookie").getValueAsBoolean(false); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug((this.storeInRequest ? "will" : "won't") + " set values in request"); |
| getLogger().debug((this.createSession ? "will" : "won't") + " create session"); |
| getLogger().debug((this.storeInSession ? "will" : "won't") + " set values in session"); |
| getLogger().debug((this.storeInCookie ? "will" : "won't") + " set values in cookies"); |
| } |
| |
| this.localeAttribute = config.getChild("locale-attribute").getValue(DEFAULT_LOCALE_ATTRIBUTE); |
| this.testResourceExists = config.getChild("negotiate").getValueAsBoolean(false); |
| |
| this.useLocale = config.getChild("use-locale").getValueAsBoolean(true); |
| this.useLocales = config.getChild("use-locales").getValueAsBoolean(false); |
| this.useBlankLocale = config.getChild("use-blank-locale").getValueAsBoolean(true); |
| |
| Configuration child = config.getChild("default-locale", false); |
| if (child != null) { |
| this.defaultLocale = new Locale(child.getAttribute("language", DEFAULT_DEFAULT_LANG), |
| child.getAttribute("country", DEFAULT_DEFAULT_COUNTRY), |
| child.getAttribute("variant", DEFAULT_DEFAULT_VARIANT)); |
| } |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Locale attribute name is " + this.localeAttribute); |
| getLogger().debug((this.testResourceExists ? "will" : "won't") + " negotiate locale"); |
| getLogger().debug((this.useLocale ? "will" : "won't") + " use request locale"); |
| getLogger().debug((this.useLocales ? "will" : "won't") + " use request locales"); |
| getLogger().debug((this.useBlankLocale ? "will" : "won't") + " blank locales"); |
| getLogger().debug("default locale " + this.defaultLocale); |
| } |
| } |
| |
| public void dispose() { |
| this.manager.release(this.resolver); |
| this.resolver = null; |
| this.manager = null; |
| } |
| |
| |
| public Map match(final String pattern, Map objectModel, Parameters parameters) |
| throws PatternException { |
| final Map map = new HashMap(); |
| |
| I18nUtils.LocaleValidator validator = new I18nUtils.LocaleValidator() { |
| public boolean test(String name, Locale locale) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Testing " + name + " locale: '" + locale + "'"); |
| } |
| return isValidResource(pattern, locale, map); |
| } |
| }; |
| |
| Locale locale = I18nUtils.findLocale(objectModel, |
| localeAttribute, |
| parameters, |
| defaultLocale, |
| useLocale, |
| useLocales, |
| useBlankLocale, |
| validator); |
| |
| if (locale == null) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("No locale found for resource: " + pattern); |
| } |
| return null; |
| } |
| |
| String localeStr = locale.toString(); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Locale " + localeStr + " found for resource: " + pattern); |
| } |
| |
| I18nUtils.storeLocale(objectModel, |
| localeAttribute, |
| localeStr, |
| storeInRequest, |
| storeInSession, |
| storeInCookie, |
| createSession); |
| |
| return map; |
| } |
| |
| private boolean isValidResource(String pattern, Locale locale, Map map) { |
| Locale testLocale; |
| |
| // Test "language, country, variant" locale |
| if (locale.getVariant().length() > 0) { |
| if (isValidResource(pattern, locale, locale, map)) { |
| return true; |
| } |
| } |
| |
| // Test "language, country" locale |
| if (locale.getCountry().length() > 0) { |
| testLocale = new Locale(locale.getLanguage(), locale.getCountry()); |
| if (isValidResource(pattern, locale, testLocale, map)) { |
| return true; |
| } |
| } |
| |
| // Test "language" locale (or empty - if language is "") |
| testLocale = new Locale(locale.getLanguage(), ""); |
| if (isValidResource(pattern, locale, testLocale, map)) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean isValidResource(String pattern, Locale locale, Locale testLocale, Map map) { |
| String url; |
| |
| String testLocaleStr = testLocale.toString(); |
| if ("".equals(testLocaleStr)) { |
| // If same character found before and after the '*', leave only one. |
| int starPos = pattern.indexOf("*"); |
| if (starPos < pattern.length() - 1 && starPos > 1 && |
| pattern.charAt(starPos - 1) == pattern.charAt(starPos + 1)) { |
| url = pattern.substring(0, starPos - 1) + pattern.substring(starPos + 1); |
| } else { |
| url = StringUtils.replace(pattern, "*", ""); |
| } |
| } else { |
| url = StringUtils.replace(pattern, "*", testLocaleStr); |
| } |
| |
| boolean result = true; |
| if (testResourceExists) { |
| Source source = null; |
| try { |
| source = resolver.resolveURI(url); |
| result = source.exists(); |
| } catch (IOException e) { |
| result = false; |
| } finally { |
| if (source != null) { |
| resolver.release(source); |
| } |
| } |
| } |
| |
| if (result) { |
| map.put("source", url); |
| map.put("matched-locale", testLocaleStr); |
| if (locale != null) { |
| map.put("locale", locale.toString()); |
| map.put("language", locale.getLanguage()); |
| map.put("country", locale.getCountry()); |
| map.put("variant", locale.getVariant()); |
| } |
| } |
| |
| return result; |
| } |
| } |