001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.matching;
018:
019: import org.apache.avalon.framework.activity.Disposable;
020: import org.apache.avalon.framework.configuration.Configurable;
021: import org.apache.avalon.framework.configuration.Configuration;
022: import org.apache.avalon.framework.logger.AbstractLogEnabled;
023: import org.apache.avalon.framework.parameters.Parameters;
024: import org.apache.avalon.framework.service.ServiceException;
025: import org.apache.avalon.framework.service.ServiceManager;
026: import org.apache.avalon.framework.service.Serviceable;
027: import org.apache.avalon.framework.thread.ThreadSafe;
028:
029: import org.apache.cocoon.i18n.I18nUtils;
030: import org.apache.cocoon.sitemap.PatternException;
031:
032: import org.apache.commons.lang.StringUtils;
033: import org.apache.excalibur.source.Source;
034: import org.apache.excalibur.source.SourceResolver;
035:
036: import java.io.IOException;
037: import java.util.HashMap;
038: import java.util.Locale;
039: import java.util.Map;
040:
041: /**
042: * A matcher that locates and identifies to the pipeline a source document to
043: * be used as the content for an i18n site, based upon a locale provided in a
044: * range of ways.
045: *
046: * <h1>Configuration</h1>
047: * <p>A sample configuration (given in the <map:matchers> section of the
048: * sitemap) is given below. This configuration shows default values.
049: * </p>
050: * <pre>
051: * <map:matcher name="i18n" src="org.apache.cocoon.matching.LocaleMatcher">
052: * <locale-attribute>locale</locale-attribute>
053: * <negotiate>false</negotiate>
054: * <use-locale>true</use-locale>
055: * <use-locales>false</use-locales>
056: * <use-blank-locale>true</use-blank-locale>
057: * <default-locale language="en" country="US"/>
058: * <store-in-request>false<store-in-request>
059: * <create-session>false<create-session>
060: * <store-in-session>false<store-in-session>
061: * <store-in-cookie>false<store-in-cookie>
062: * </map:matcher>
063: * </pre>
064: *
065: * <p>Above configuration parameters mean:
066: * <ul>
067: * <li><b>locale-attribute</b> specifies the name of the request
068: * parameter / session attribute / cookie that is to be used as a locale
069: * (defaults to <code>locale</code>)</li>
070: * <li><b>negotiate</b> specifies whether matcher should check that
071: * resource exists. If set to true, matcher will look for the locale
072: * till matching resource is found. If no resource found even with
073: * default or blank locale, matcher will not match.</li>
074: * <li><b>use-locale</b> specifies whether the primary locale provided
075: * by the user agent (or server default, is no locale passed by the agent)
076: * is to be used</li>
077: * <li><b>use-locales</b> specifies whether each locale provided by the
078: * user agent should be tested in turn (makes sense only when
079: * <code>negotiate</code> is set to <code>true</code>)</li>
080: * <li><b>default-locale</b> specifies the default locale to be used when
081: * none matches any of the previous ones.</li>
082: * <li><b>use-blank-locale</b> specifies whether a file should be looked
083: * for without a locale in its filename or filepath (e.g. after looking
084: * for index.en.html, try index.html) if none matches any of the previous
085: * locales.</li>
086: * <li><b>store-in-request</b> specifies whether found locale should be
087: * stored as request attribute.</li>
088: * <li><b>create-session</b> specifies whether session should be created
089: * when storing found locale as session attribute.</li>
090: * <li><b>store-in-session</b> specifies whether found locale should be
091: * stored as session attribute.</li>
092: * <li><b>store-in-cookie</b> specifies whether found locale should be
093: * stored as cookie.</li>
094: * </ul>
095: * </p>
096: *
097: * <h1>Usage</h1>
098: * <p>This matcher will be used in a pipeline like so:</p>
099: * <pre>
100: * <map:match pattern="*.html">
101: * <map:match type="i18n" pattern="xml/{1}.*.xml">
102: * <map:generate src="{source}"/>
103: * ...
104: * </map:match>
105: * </map:match>
106: * </pre>
107: * <p><code>*</code> in the pattern identifies the place where locale should
108: * be inserted. In case of a blank locale, if character before and after
109: * <code>*</code> is the same (like in example above), duplicate will
110: * be removed (<code>xml/{1}.*.xml</code> becomes <code>xml/{1}.xml</code>).</p>
111: *
112: * <h1>Locale Identification</h1>
113: * <p>Locales will be tested in following order:</p>
114: * <ul>
115: * <li>Locale provided as a request parameter</li>
116: * <li>Locale provided as a session attribute</li>
117: * <li>Locale provided as a cookie</li>
118: * <li>Locale provided using a sitemap parameter<br>
119: * (<map:parameter name="locale" value="{1}"/> style parameter within
120: * the <map:match> node)</li>
121: * <li>Locale provided by the user agent, or server default,
122: * if <code>use-locale</code> is set to <code>true</code></li>
123: * <li>Locales provided by the user agent, if <code>use-locales</code>
124: * is set to <code>true</code>.</li>
125: * <li>The default locale, if specified in the matcher's configuration</li>
126: * <li>Resources with no defined locale (blank locale)</li>
127: * </ul>
128: * <p>If <code>negotiate</code> mode is set to <code>true</code>, a source will
129: * be looked up using each locale. Where the full locale (language, country,
130: * variant) doesn't match, it will fall back first to language and country,
131: * and then just language, before moving on to the next locale.</p>
132: * <p>If <code>negotiate</code> mode is set to <code>false</code> (default),
133: * first found locale will be returned.</p>
134: *
135: * <h1>Sitemap Variables</h1>
136: * <p>Once a matching locale has been found, the following sitemap variables
137: * will be available to sitemap elements contained within the matcher:</p>
138: * <ul>
139: * <li>{source}: The URI of the source that matched</li>
140: * <li>{locale}: The locale that matched that resource</li>
141: * <li>{matched-locale}: The part of the locale that matched the resource</li>
142: * <li>{language}: The language of the matching resource</li>
143: * <li>{country}: The country of the matching resource</li>
144: * <li>{variant}: The variant of the matching resource</li>
145: * </ul>
146: *
147: * @since 2.1.6
148: * @author <a href="mailto:uv@upaya.co.uk">Upayavira</a>
149: * @author <a href="mailto:vgritsenko@apache.org">Vadim Gritsenko</a>
150: * @version CVS $Id: LocaleMatcher.java 433543 2006-08-22 06:22:54Z crossley $
151: */
152: public class LocaleMatcher extends AbstractLogEnabled implements
153: Matcher, ThreadSafe, Serviceable, Configurable, Disposable {
154:
155: private static final String DEFAULT_LOCALE_ATTRIBUTE = "locale";
156: private static final String DEFAULT_DEFAULT_LANG = "en";
157: private static final String DEFAULT_DEFAULT_COUNTRY = "US";
158: private static final String DEFAULT_DEFAULT_VARIANT = "";
159:
160: private ServiceManager manager;
161: private SourceResolver resolver;
162:
163: /**
164: * Name of the locale request parameter, session attribute, cookie.
165: */
166: private String localeAttribute;
167:
168: /**
169: * Whether to query locale provided by the user agent or not.
170: */
171: private boolean useLocale;
172:
173: private boolean useLocales;
174: private Locale defaultLocale;
175: private boolean useBlankLocale;
176: private boolean testResourceExists;
177:
178: /**
179: * Store the locale in request. Default is not to do this.
180: */
181: private boolean storeInRequest;
182:
183: /**
184: * Store the locale in session, if available. Default is not to do this.
185: */
186: private boolean storeInSession;
187:
188: /**
189: * Should we create a session if needed. Default is not to do this.
190: */
191: private boolean createSession;
192:
193: /**
194: * Should we add a cookie with the locale. Default is not to do this.
195: */
196: private boolean storeInCookie;
197:
198: public void service(ServiceManager manager) throws ServiceException {
199: this .manager = manager;
200: this .resolver = (SourceResolver) this .manager
201: .lookup(SourceResolver.ROLE);
202: }
203:
204: public void configure(Configuration config) {
205: this .storeInRequest = config.getChild("store-in-request")
206: .getValueAsBoolean(false);
207: this .createSession = config.getChild("create-session")
208: .getValueAsBoolean(false);
209: this .storeInSession = config.getChild("store-in-session")
210: .getValueAsBoolean(false);
211: this .storeInCookie = config.getChild("store-in-cookie")
212: .getValueAsBoolean(false);
213: if (getLogger().isDebugEnabled()) {
214: getLogger().debug(
215: (this .storeInRequest ? "will" : "won't")
216: + " set values in request");
217: getLogger().debug(
218: (this .createSession ? "will" : "won't")
219: + " create session");
220: getLogger().debug(
221: (this .storeInSession ? "will" : "won't")
222: + " set values in session");
223: getLogger().debug(
224: (this .storeInCookie ? "will" : "won't")
225: + " set values in cookies");
226: }
227:
228: this .localeAttribute = config.getChild("locale-attribute")
229: .getValue(DEFAULT_LOCALE_ATTRIBUTE);
230: this .testResourceExists = config.getChild("negotiate")
231: .getValueAsBoolean(false);
232:
233: this .useLocale = config.getChild("use-locale")
234: .getValueAsBoolean(true);
235: this .useLocales = config.getChild("use-locales")
236: .getValueAsBoolean(false);
237: this .useBlankLocale = config.getChild("use-blank-locale")
238: .getValueAsBoolean(true);
239:
240: Configuration child = config.getChild("default-locale", false);
241: if (child != null) {
242: this .defaultLocale = new Locale(child.getAttribute(
243: "language", DEFAULT_DEFAULT_LANG), child
244: .getAttribute("country", DEFAULT_DEFAULT_COUNTRY),
245: child.getAttribute("variant",
246: DEFAULT_DEFAULT_VARIANT));
247: }
248:
249: if (getLogger().isDebugEnabled()) {
250: getLogger().debug(
251: "Locale attribute name is " + this .localeAttribute);
252: getLogger().debug(
253: (this .testResourceExists ? "will" : "won't")
254: + " negotiate locale");
255: getLogger().debug(
256: (this .useLocale ? "will" : "won't")
257: + " use request locale");
258: getLogger().debug(
259: (this .useLocales ? "will" : "won't")
260: + " use request locales");
261: getLogger().debug(
262: (this .useBlankLocale ? "will" : "won't")
263: + " blank locales");
264: getLogger().debug("default locale " + this .defaultLocale);
265: }
266: }
267:
268: public void dispose() {
269: this .manager.release(this .resolver);
270: this .resolver = null;
271: this .manager = null;
272: }
273:
274: public Map match(final String pattern, Map objectModel,
275: Parameters parameters) throws PatternException {
276: final Map map = new HashMap();
277:
278: I18nUtils.LocaleValidator validator = new I18nUtils.LocaleValidator() {
279: public boolean test(String name, Locale locale) {
280: if (getLogger().isDebugEnabled()) {
281: getLogger().debug(
282: "Testing " + name + " locale: '" + locale
283: + "'");
284: }
285: return isValidResource(pattern, locale, map);
286: }
287: };
288:
289: Locale locale = I18nUtils.findLocale(objectModel,
290: localeAttribute, parameters, defaultLocale, useLocale,
291: useLocales, useBlankLocale, validator);
292:
293: if (locale == null) {
294: if (getLogger().isDebugEnabled()) {
295: getLogger().debug(
296: "No locale found for resource: " + pattern);
297: }
298: return null;
299: }
300:
301: String localeStr = locale.toString();
302: if (getLogger().isDebugEnabled()) {
303: getLogger().debug(
304: "Locale " + localeStr + " found for resource: "
305: + pattern);
306: }
307:
308: I18nUtils.storeLocale(objectModel, localeAttribute, localeStr,
309: storeInRequest, storeInSession, storeInCookie,
310: createSession);
311:
312: return map;
313: }
314:
315: private boolean isValidResource(String pattern, Locale locale,
316: Map map) {
317: Locale testLocale;
318:
319: // Test "language, country, variant" locale
320: if (locale.getVariant().length() > 0) {
321: if (isValidResource(pattern, locale, locale, map)) {
322: return true;
323: }
324: }
325:
326: // Test "language, country" locale
327: if (locale.getCountry().length() > 0) {
328: testLocale = new Locale(locale.getLanguage(), locale
329: .getCountry());
330: if (isValidResource(pattern, locale, testLocale, map)) {
331: return true;
332: }
333: }
334:
335: // Test "language" locale (or empty - if language is "")
336: testLocale = new Locale(locale.getLanguage(), ""); // Use JDK1.3 constructor
337: if (isValidResource(pattern, locale, testLocale, map)) {
338: return true;
339: }
340:
341: return false;
342: }
343:
344: private boolean isValidResource(String pattern, Locale locale,
345: Locale testLocale, Map map) {
346: String url;
347:
348: String testLocaleStr = testLocale.toString();
349: if ("".equals(testLocaleStr)) {
350: // If same character found before and after the '*', leave only one.
351: int starPos = pattern.indexOf("*");
352: if (starPos < pattern.length() - 1
353: && starPos > 1
354: && pattern.charAt(starPos - 1) == pattern
355: .charAt(starPos + 1)) {
356: url = pattern.substring(0, starPos - 1)
357: + pattern.substring(starPos + 1);
358: } else {
359: url = StringUtils.replace(pattern, "*", "");
360: }
361: } else {
362: url = StringUtils.replace(pattern, "*", testLocaleStr);
363: }
364:
365: boolean result = true;
366: if (testResourceExists) {
367: Source source = null;
368: try {
369: source = resolver.resolveURI(url);
370: result = source.exists();
371: } catch (IOException e) {
372: result = false;
373: } finally {
374: if (source != null) {
375: resolver.release(source);
376: }
377: }
378: }
379:
380: if (result) {
381: map.put("source", url);
382: map.put("matched-locale", testLocaleStr);
383: if (locale != null) {
384: map.put("locale", locale.toString());
385: map.put("language", locale.getLanguage());
386: map.put("country", locale.getCountry());
387: map.put("variant", locale.getVariant());
388: }
389: }
390:
391: return result;
392: }
393: }
|