001: /*
002: * Copyright 2003 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package velosurf.web.l10n;
018:
019: import velosurf.util.Logger;
020: import velosurf.util.StringLists;
021: import velosurf.util.ServletLogWriter;
022: import velosurf.util.ToolFinder;
023:
024: import javax.servlet.*;
025: import javax.servlet.http.HttpServletRequest;
026: import javax.servlet.http.HttpSession;
027: import javax.servlet.http.HttpServletResponse;
028: import javax.servlet.http.Cookie;
029: import java.io.IOException;
030: import java.util.Locale;
031: import java.util.List;
032: import java.util.ArrayList;
033: import java.util.Arrays;
034: import java.util.Enumeration;
035: import java.util.Set;
036: import java.util.Collections;
037: import java.util.regex.Pattern;
038: import java.util.regex.Matcher;
039:
040: /**
041: * <p>Localization filter. It's goal is to redirect or forward incoming unlocalized http requests (depending on the
042: * choosen method, <code>FORWARD</code> or <code>REDIRECT</code>) towards an address taking into account the best match
043: * between requested locales and supported locales, and also to deduce the locale from URLS (when <code>REDIRECT</code>
044: * is used).</p>
045: *
046: * <p>Optional init parameters:
047: * <ul>
048: * <li><code>supported-locales</code>: comma separated list of supported locales ; if not provided, there is an attempt to programatically determine it<sup>(1)</sup>.
049: * No default value provided.</li>
050: * <li><code>default-locale</code>: the locale to be used by default (after four checks: the incoming URI, the session, cookies, and the request headers).
051: * No default value provided.</li>
052: * <li><code>localization-method</code>: <code>forward</code> or <code>redirect</code>, default is <code>redirect</code>.
053: * <li><code>match-host</code> & <code>rewrite-host</code>: not yet implemented.<sup>(2)</sup></li>
054: * <li><code>match-uri</code> & <code>rewrite-uri</code>: the regular expression against which an unlocalized uri is matched, and the replacement uri, where
055: * <code>@</code> represents the locale and $1, $2, ... the matched sub-patterns. Defaults are <code>^/(.*)$</code>
056: * for <code>match-uri</code> and <code>/@/$1</code> for rewrite-uri.(2)</li>
057: * <li><code>match-query-string</code> & <code>rewrite-query-string</code>: not yet implemented.(2)</li>
058: * <li><code>match-url</code> & <code>rewrite-url</code>: not yet implemented.(2)</li>
059: * </ul>
060: * </p>
061: *
062: * <p><b><small>(1)</small></b> for now, to find supported locales if this parameter is not provided,
063: * the filter try to use the <code>rewrite-uri</code> param and to check for the existence of corresponding directories (only if the rewriting
064: * string contains a pattern like '/@/', that is if you use directories to store localized sites).
065: * <p><b><small>(2)</small></b> The different <code>match-</code> and <code>rewrite-</code> parameters pairs are mutually exclusive.
066: * All matches are case-insensitive.</p>
067: *
068: * <p>When the <code>redirect</code> method is used, these supplementary parameters (mutually exclusive) allow the filter to
069: * know whether or not an incoming URI is localized.
070: * <ul>
071: * <li><code>inspect-host</code>: not yet implemented.</li>
072: * <li><code>inspect-uri</code>: default is <code>^/(.+)(?:/|$)</code>.
073: * <li><code>inspect-query-string</code>: not yet implemented.</li>
074: * <li><code>inspect-url</code>: not yet implemented.</li>
075: * </ul>
076: * </p>
077: *
078: * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
079: */
080:
081: public class LocalizationFilter implements Filter {
082:
083: /** filter config. */
084: private FilterConfig config = null;
085:
086: /** supported locales. */
087: private List<Locale> supportedLocales = null;
088: /** default locale. */
089: private Locale defaultLocale = null;
090:
091: /** seconds in year (for setting cookies age). */
092: private static int SECONDS_IN_YEAR = 31536000;
093:
094: /** default match uri. */
095: private static String defaultMatchUri = "^/(.*)$";
096: /** default rewrite uri. */
097: private static String defaultRewriteUri = "/@/$1";
098: /** default inspect uri. */
099: private static String defaultInspectUri = "^/(.+)(?:/|$)";
100: /** match uri. */
101: private Pattern matchUri = null;
102: /** rewrite uri */
103: private String rewriteUri = null;
104: /** inspect uri. */
105: private Pattern inspectUri = null;
106:
107: /** forward method constant. */
108: private static final int FORWARD = 1;
109: /** redirect method constant. */
110: private static final int REDIRECT = 2;
111:
112: /** localization method. */
113: private int l10nMethod = REDIRECT;
114:
115: /** initialization.
116: *
117: * @param config filter config
118: * @throws ServletException
119: */
120: public synchronized void init(FilterConfig config)
121: throws ServletException {
122: this .config = config;
123:
124: /* logger initialization */
125: if (!Logger.isInitialized()) {
126: Logger.setWriter(new ServletLogWriter(config
127: .getServletContext()));
128: }
129:
130: String param;
131:
132: /* uri */
133: matchUri = Pattern.compile(getInitParameter("match-uri",
134: defaultMatchUri), Pattern.CASE_INSENSITIVE);
135: rewriteUri = getInitParameter("rewrite-uri", defaultRewriteUri);
136: inspectUri = Pattern.compile(getInitParameter("inspect-uri",
137: defaultInspectUri), Pattern.CASE_INSENSITIVE);
138:
139: /* method */
140: param = getInitParameter("localization-method", "redirect");
141: if (param.equalsIgnoreCase("redirect")) {
142: l10nMethod = REDIRECT;
143: } else if (param.equalsIgnoreCase("forward")) {
144: l10nMethod = FORWARD;
145: } else {
146: Logger
147: .error("LocalizationFilter: '"
148: + param
149: + "' is not a valid l10n method; should be 'forward' or 'redirect'.");
150: }
151:
152: /* supported locales */
153: findSupportedLocales(this .config);
154:
155: /* default locale */
156: defaultLocale = getMatchedLocale(getInitParameter("default-locale"));
157: }
158:
159: /**
160: * Filtering.
161: * @param servletRequest request
162: * @param servletResponse response
163: * @param chain filter chain
164: * @throws IOException
165: * @throws ServletException
166: */
167: public void doFilter(ServletRequest servletRequest,
168: ServletResponse servletResponse, FilterChain chain)
169: throws IOException, ServletException {
170: HttpServletRequest request = (HttpServletRequest) servletRequest;
171: HttpSession session = request.getSession(true); /* we'll store the active locale in it */
172: HttpServletResponse response = (HttpServletResponse) servletResponse;
173:
174: Locale locale = null;
175:
176: /* should an action (forward/redirect) be taken by the filter? */
177: boolean shouldAct = true;
178:
179: /* Now, what is the current locale ?
180: Guess #1 is the URI, if already localized (only for REDIRECT method).
181: Guess #2 is the session attribute 'active-locale'.
182: Guess #3 is a cookie 'used-locale'.
183: Guess #4 is from the Accepted-Language header.
184: */
185:
186: // Logger.trace("l10n: URI ="+request.getRequestURI());
187: /* Guess #1 - if using redirect method, deduce from URI (and, while looking at URI, fills the shouldAct vairable) */
188: if (l10nMethod == REDIRECT) {
189: Matcher matcher = inspectUri.matcher(request
190: .getRequestURI());
191: if (matcher.find()) {
192: String candidate = matcher.group(1);
193: locale = getMatchedLocale(candidate);
194: if (locale != null) {
195: shouldAct = false;
196: }
197: }
198: // Logger.trace("l10n: URI locale = "+locale);
199: } else {
200: /* for the forward method, shouldAct rule is: always only when not already forwarded */
201: Boolean forwarded = (Boolean) request
202: .getAttribute("velosurf.l10n.l10n-forwarded");
203: if (forwarded != null && forwarded.booleanValue()) {
204: shouldAct = false;
205: }
206: }
207:
208: if (locale == null) {
209: /* Guess #2 - is there an attribute in the session? */
210: locale = (Locale) session
211: .getAttribute("velosurf.l10n.active-locale");
212: // Logger.trace("l10n: session locale = "+locale);
213:
214: if (locale == null) {
215: /* Guess #3 - is there a cookie?*/
216: Cookie cookies[] = request.getCookies();
217: if (cookies != null) {
218: for (Cookie cookie : cookies) {
219: if ("velosurf.l10n.active-locale".equals(cookie
220: .getName())) {
221: locale = getMatchedLocale(cookie.getValue());
222: }
223: }
224: }
225: // Logger.trace("l10n: cookies locale = "+locale);
226:
227: if (locale == null) {
228: /* Guess #4 - use the Accepted-Language HTTP header */
229: List<Locale> requestedLocales = getRequestedLocales(request);
230: locale = getPreferredLocale(requestedLocales);
231: Logger
232: .trace("l10n: Accepted-Language header best matching locale = "
233: + locale);
234: }
235: }
236: }
237:
238: if (locale == null && defaultLocale != null) {
239: locale = defaultLocale;
240: }
241: /* not needed - the tool should find the active locale in the session
242: if (locale != null) {
243: Localizer tool = ToolFinder.findSessionTool(session,Localizer.class);
244: if (tool != null) {
245: tool.setLocale(locale);
246: } else {
247: Logger.warn("l10n: cannot find any Localizer tool!");
248: }
249: }
250: */
251: /* sets the session atribute and the cookies */
252: // Logger.trace("l10n: setting session current locale to "+locale);
253: session.setAttribute("velosurf.l10n.active-locale", locale);
254: Cookie localeCookie = new Cookie("velosurf.l10n.active-locale",
255: locale.toString());
256: localeCookie.setPath("/");
257: localeCookie.setMaxAge(SECONDS_IN_YEAR);
258: response.addCookie(localeCookie);
259:
260: Matcher match = matchUri.matcher(request.getRequestURI());
261: shouldAct &= match.find();
262:
263: if (shouldAct) {
264: // && (i = rewriteUri.indexOf("@")) != -1) ?
265:
266: String rewriteUri = this .rewriteUri.replaceFirst("@",
267: locale.toString());
268: String newUri = match.replaceFirst(rewriteUri);
269: RequestDispatcher dispatcher;
270:
271: String query = request.getQueryString();
272: if (query == null) {
273: query = "";
274: } else {
275: query = "?" + query;
276: }
277:
278: switch (l10nMethod) {
279: case REDIRECT:
280: Logger.trace("l10n: redirecting request to " + newUri
281: + query);
282: response.sendRedirect(newUri + query);
283: break;
284: case FORWARD:
285: dispatcher = config.getServletContext()
286: .getRequestDispatcher(newUri + query);
287: if (dispatcher == null) {
288: Logger
289: .error("l10n: cannot find a request dispatcher for path '"
290: + newUri + "'");
291: response
292: .sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
293: } else {
294: Logger.trace("l10n: forwarding request to "
295: + newUri + query);
296: request.setAttribute(
297: "velosurf.l10n.l10n-forwarded", Boolean
298: .valueOf(shouldAct));
299: dispatcher.forward(request, response);
300: }
301: break;
302: }
303: } else {
304: // Logger.trace("l10n: letting request pass towards "+request.getRequestURI());
305: chain.doFilter(request, response);
306: }
307: }
308:
309: /** Find supported locales.
310: *
311: * @param config filter config
312: */
313: private void findSupportedLocales(FilterConfig config) {
314: /* look in the filter init-params */
315: String param = config.getInitParameter("supported-locales");
316: if (param == null) {
317: /* look in the webapp context-params */
318: param = config.getServletContext().getInitParameter(
319: "supported-locales");
320: }
321:
322: if (param == null) {
323: /* try to determine it */
324: int i;
325: if (rewriteUri != null
326: && (i = rewriteUri.indexOf("@")) != -1) {
327: supportedLocales = guessSupportedLocales(this .config
328: .getServletContext(), rewriteUri
329: .substring(0, i));
330: if (Logger.getLogLevel() <= Logger.TRACE_ID) {
331: Logger.trace("l10n: supported locales = "
332: + StringLists.join(Arrays
333: .asList(supportedLocales), ","));
334: }
335: if (supportedLocales != null
336: && supportedLocales.size() > 0) {
337: return;
338: }
339: }
340: } else {
341: supportedLocales = new ArrayList<Locale>();
342: String[] list = param.split(",");
343: for (String code : list) {
344: supportedLocales.add(new Locale(code));
345: }
346: }
347: if (supportedLocales != null && supportedLocales.size() > 0) {
348: /* let other objects see it?
349: config.getServletContext().setAttribute("velosurf.l10n.supported-locales",supportedLocales);
350: */
351: } else {
352: Logger
353: .error("l10n: Cannot find any supported locale! Please add a 'supported-locales' context-param.");
354: }
355: }
356:
357: /** Helper function.
358: *
359: * @param key
360: * @return init-parameter
361: */
362: private String getInitParameter(String key) {
363: return config.getInitParameter(key);
364: }
365:
366: /** Helper function.
367: *
368: * @param key
369: * @param defaultValue
370: * @return init-parameter
371: */
372: private String getInitParameter(String key, String defaultValue) {
373: String param = config.getInitParameter(key);
374: return param == null ? defaultValue : param;
375: }
376:
377: /** Destroy the filter.
378: *
379: */
380: public void destroy() {
381: }
382:
383: /**
384: * Guess supported locales.
385: * @param ctx servlet context
386: * @param path path
387: * @return list of locales
388: */
389: private List<Locale> guessSupportedLocales(ServletContext ctx,
390: String path) {
391: List<Locale> locales = new ArrayList<Locale>();
392: String languages[] = Locale.getISOLanguages();
393: String countries[] = Locale.getISOCountries();
394: Arrays.sort(languages);
395: Arrays.sort(countries);
396: String language, country;
397: for (String resource : (Set<String>) ctx.getResourcePaths(path)) {
398: /* first, it must be a path */
399: if (resource.endsWith("/")) {
400: int len = resource.length();
401: int i = resource.lastIndexOf('/', len - 2);
402: String locale = resource.substring(i + 1, len - 1);
403: if ((i = locale.indexOf('_')) != -1) {
404: language = locale.substring(0, i);
405: country = locale.substring(i + 1);
406: } else {
407: language = locale;
408: country = null;
409: }
410: /* then it must contains valid language and country codes */
411: if (Arrays.binarySearch(languages, language) >= 0
412: && (country == null || Arrays.binarySearch(
413: countries, country) >= 0)) {
414: /* looks ok... */
415: locales.add(country == null ? new Locale(language)
416: : new Locale(language, country));
417: }
418: }
419: }
420: supportedLocales = locales;
421: return locales;
422: }
423:
424: /** get the list of requested locales.
425: *
426: * @param request request
427: * @return list of locales
428: */
429: private List<Locale> getRequestedLocales(HttpServletRequest request) {
430: List<Locale> list = (List<Locale>) Collections.list(request
431: .getLocales());
432: if (/*list.size() == 0 && */defaultLocale != null) {
433: list.add(defaultLocale);
434: }
435: return list;
436: }
437:
438: /**
439: * get matched locale.
440: * @param candidate candidate
441: * @return locale
442: */
443: private Locale getMatchedLocale(String candidate) {
444: if (candidate == null)
445: return null;
446: if (supportedLocales == null) {
447: Logger
448: .error("l10n: the list of supported locales is empty!");
449: return null;
450: }
451: for (Locale locale : supportedLocales) {
452: if (candidate.startsWith(locale.toString())) {
453: return locale;
454: }
455: }
456: for (Locale locale : supportedLocales) {
457: if (locale.toString().startsWith(candidate)) {
458: return locale;
459: }
460: }
461: return null;
462: }
463:
464: /**
465: * Get preferred locale.
466: * @param requestedLocales requested locales
467: * @return preferred locale
468: */
469: private Locale getPreferredLocale(List<Locale> requestedLocales) {
470: for (Locale locale : requestedLocales) {
471: if (supportedLocales.contains(locale)) {
472: return locale;
473: }
474: }
475: /* still there? Ok, second pass without the country. */
476: for (Locale locale : requestedLocales) {
477: if (locale.getCountry() != null) {
478: locale = new Locale(locale.getLanguage());
479: if (supportedLocales.contains(locale)) {
480: return locale;
481: }
482: }
483: }
484: Logger.warn("l10n: did not find a matching locale for "
485: + StringLists.join(requestedLocales, ","));
486: /* then return the default locale, even if it doesn't match...
487: if(defaultLocale != null) {
488: return defaultLocale;
489: }*/
490: /* Oh, well... */
491: return null;
492: }
493: }
|