001: package org.tigris.scarab.tools;
002:
003: /* ================================================================
004: * Copyright (c) 2000 CollabNet. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are
008: * met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in the
015: * documentation and/or other materials provided with the distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowlegement: "This product includes
019: * software developed by CollabNet (http://www.collab.net/)."
020: * Alternately, this acknowlegement may appear in the software itself, if
021: * and wherever such third-party acknowlegements normally appear.
022: *
023: * 4. The hosted project names must not be used to endorse or promote
024: * products derived from this software without prior written
025: * permission. For written permission, please contact info@collab.net.
026: *
027: * 5. Products derived from this software may not use the "Tigris" name
028: * nor may "Tigris" appear in their names without prior written
029: * permission of CollabNet.
030: *
031: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
032: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
033: * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
034: * IN NO EVENT SHALL COLLAB.NET OR ITS CONTRIBUTORS BE LIABLE FOR ANY
035: * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
036: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
037: * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
038: * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
039: * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
040: * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
041: * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
042: *
043: * ====================================================================
044: *
045: * This software consists of voluntary contributions made by many
046: * individuals on behalf of CollabNet.
047: */
048:
049: import java.util.ArrayList;
050: import java.util.HashMap;
051: import java.util.Iterator;
052: import java.util.List;
053: import java.util.Locale;
054: import java.util.Map;
055: import java.util.MissingResourceException;
056: import java.util.ResourceBundle;
057:
058: import javax.servlet.http.HttpServletRequest;
059:
060: import org.apache.fulcrum.localization.LocaleTokenizer;
061: import org.apache.fulcrum.localization.Localization;
062: import org.apache.fulcrum.localization.LocalizationService;
063: import org.apache.turbine.RunData;
064: import org.apache.turbine.tool.LocalizationTool;
065: import org.tigris.scarab.tools.localization.Localizable;
066: import org.tigris.scarab.tools.localization.LocalizationKey;
067: import org.tigris.scarab.util.Log;
068: import org.tigris.scarab.util.ReferenceInsertionFilter;
069: import org.tigris.scarab.util.SkipFiltering;
070:
071: /**
072: * Scarab-specific localiztion tool. Uses a specific property
073: * format to map a generic i10n key to a specific screen.
074: *
075: * For example, the $i10n.title on the screen:
076: * admin/AddPermission.vm would be in ScarabBundle_en.properties
077: * <blockquote><code><pre>
078: * admin/AddPermission.vm.Title
079: * </pre></code> </blockquote>
080: *
081: *
082: * @author <a href="mailto:dlr@collab.net">Daniel Rall </a>
083: * @author <a href="mailto:epugh@opensourceconnections.com">Eric Pugh </a>
084: */
085: public class ScarabLocalizationTool extends LocalizationTool {
086: /**
087: * The Locale to be used, if the Resource could not be found in
088: * one of the Locales specified in the Browser's language preferences.
089: */
090: public static Locale DEFAULT_LOCALE = new Locale("en", "");
091:
092: /**
093: * The portion of a key denoting the title property.
094: */
095: private static final String TITLE_PROP = "Title";
096:
097: /**
098: * We need to keep a reference to the request's <code>RunData</code> so
099: * that we can extract the name of the target <i>after </i> the <code>Action</code>
100: * has run (which may have changed the target from its original value as a
101: * sort of internal redirect).
102: */
103: private RunData data;
104:
105: /**
106: * Initialized by <code>init()</code>,
107: * cleared by <code>refresh()</code>.
108: */
109: private String bundlePrefix;
110: private String oldBundlePrefix;
111:
112: /**
113: * Store the collection of locales to be used for ResourceBundle resolution.
114: * If the Class is instantiated from RunData, the collection contains all
115: * Locales in order of preference as specified in the Browser.
116: * If the Class is instantiated from a Locale, the collection contains
117: * just that Locale.
118: *
119: */
120: private List locales;
121:
122: /**
123: * true: enables cross-site scripting filtering.
124: * @see resolveArgumentTemplates
125: * @see format(String, Object[])
126: */
127: private boolean filterEnabled = true;
128:
129: /**
130: * Creates a new instance. Client should
131: * {@link #init(Object) initialize} the instance.
132: */
133: public ScarabLocalizationTool() {
134: }
135:
136: /**
137: * Return the localized property value.
138: * Take into account the Browser settings (in order of preference),
139: * the Turbine default settings and the System Locale,
140: * if the Turbine Default Locale is not defined.
141: */
142: public String get(Localizable key) {
143: String theKey = key.toString();
144: return this .get(theKey);
145: }
146:
147: /**
148: * Return the localized property value.
149: * Take into account the Browser settings (in order of preference),
150: * the Turbine default settings and the System Locale,
151: * if the Turbine Default Locale is not defined.
152: * Throws an Exception when an error occurs.
153: */
154: private String getInternal(String key) throws Exception {
155: String value = null;
156:
157: // Try with all defined "Browser"-Locales ordered by relevance
158: Iterator iter = locales.iterator();
159: while (value == null && iter.hasNext()) {
160: Locale locale = (Locale) iter.next();
161: value = resolveKey(key, locale);
162: }
163: return value;
164: }
165:
166: /**
167: * Return the localized property value.
168: * Take into account the Browser settings (in order of preference),
169: * the Turbine default settings and the System Locale,
170: * if the Turbine Default Locale is not defined.
171: * NOTE: Please don't use this method from the Java-code.
172: * It is intended for use with Velocity only!
173: * @deprecated Please use {@link #get(LocalizationKey)} instead
174: */
175: public String get(String key) {
176: String value;
177: try {
178: value = getInternal(key);
179: if (value == null) {
180: value = createMissingResourceValue(key);
181: }
182: } catch (Exception e) {
183: value = createBadResourceValue(key, e);
184: }
185: return value;
186: }
187:
188: /**
189: * Return the localized property value.
190: * Take into account the Browser settings (in order of preference),
191: * the Turbine default settings and the System Locale,
192: * if the Turbine Default Locale is not defined.
193: * NOTE: Please don't use this method from the Java-code.
194: * It is intended for use with Velocity only!
195: * @deprecated Please use {@link #get(LocalizationKey)} instead
196: */
197: public String getIgnoreMissingResource(String key) {
198: String value;
199: try {
200: value = getInternal(key);
201: if (value == null) {
202: value = key;
203: }
204: } catch (Exception e) {
205: value = createBadResourceValue(key, e);
206: }
207: return value;
208: }
209:
210: /**
211: * Formats a localized value using the provided object.
212: *
213: * @param key The identifier for the localized text to retrieve,
214: * @param arg1 The object to use as {0} when formatting the localized text.
215: * @return Formatted localized text.
216: * @see #format(String, List)
217: */
218: public String format(String key, Object arg1) {
219: return format(key, new Object[] { arg1 });
220: }
221:
222: /**
223: * Formats a localized value using the provided objects.
224: *
225: * @param key The identifier for the localized text to retrieve,
226: * @param arg1 The object to use as {0} when formatting the localized text.
227: * @param arg2 The object to use as {1} when formatting the localized text.
228: * @return Formatted localized text.
229: * @see #format(String, List)
230: */
231: public String format(String key, Object arg1, Object arg2) {
232: return format(key, new Object[] { arg1, arg2 });
233: }
234:
235: /**
236: * Formats a localized value using the provided objects.
237: *
238: * @param key The identifier for the localized text to retrieve,
239: * @param arg1 The object to use as {0} when formatting the localized text.
240: * @param arg2 The object to use as {1} when formatting the localized text.
241: * @param arg3 The object to use as {2} when formatting the localized text.
242: * @return Formatted localized text.
243: * @see #format(String, List)
244: */
245: public String format(String key, Object arg1, Object arg2,
246: Object arg3) {
247: return format(key, new Object[] { arg1, arg2, arg3 });
248: }
249:
250: /**
251: * <p>Formats a localized value using the provided objects.</p>
252: *
253: * <p>ResourceBundle:
254: * <blockquote><code><pre>
255: * VelocityUsersNotWrong={0} out of {1} users can't be wrong!
256: * </pre></code> </blockquote>
257: *
258: * Template:
259: * <blockquote><code><pre>
260: * $l10n.format("VelocityUsersNotWrong", ["9", "10"])
261: * </pre></code> </blockquote>
262: *
263: * Result:
264: * <blockquote><code><pre>
265: * 9 out of 10 Velocity users can't be wrong!
266: * </pre></code></blockquote></p>
267: *
268: * @param key The identifier for the localized text to retrieve,
269: * @param args The objects to use as {0}, {1}, etc. when formatting the
270: * localized text.
271: * @return Formatted localized text.
272: */
273: public String format(String key, List args) {
274: Object[] array = (args == null) ? null : args.toArray();
275: return format(key, array);
276: }
277:
278: /**
279: * Allow us to be able to enable/disable our cross-site scripting filter
280: * when rendering something from the format() method. The default is to
281: * have it enabled.
282: */
283: public void setFilterEnabled(boolean v) {
284: filterEnabled = v;
285: }
286:
287: /**
288: * Whether our cross-site scripting filter is enabled.
289: */
290: public boolean isFilterEnabled() {
291: return filterEnabled;
292: }
293:
294: /**
295: * Formats a localized value using the provided objects.
296: * Take into account the Browser settings (in order of preference),
297: * the Turbine default settings and the System Locale,
298: * if the Turbine Default Locale is not defined.
299: *
300: * @param key The identifier for the localized text to retrieve,
301: * @param args The <code>MessageFormat</code> data used when formatting
302: * the localized text.
303: * @return Formatted localized text.
304: * @see #format(String, List)
305: */
306: public String format(String key, Object[] args) {
307: String value = null;
308: resolveArgumentTemplates(args);
309: try {
310: // try with the "Browser"-Locale
311: Iterator iter = locales.iterator();
312: while (value == null && iter.hasNext()) {
313: Locale locale = (Locale) iter.next();
314: value = formatKey(key, args, locale);
315: }
316: /*if (value == null)
317: {
318: // try with the "Default"-Scope ??? This may be wrong (Hussayn)
319: String prefix = getPrefix(null);
320: setPrefix(DEFAULT_SCOPE + '.');
321: try
322: {
323: value = super.format(key, args);
324: }
325: catch (MissingResourceException itsNotThere)
326: {
327: value = createMissingResourceValue(key);
328: }
329: setPrefix(prefix);
330: }*/
331: } catch (Exception e) {
332: value = createBadResourceValue(key, e);
333: }
334: return value;
335: }
336:
337: /**
338: * Provides <code>$l10n.Title</code> to templates, grabbing it
339: * from the <code>title</code> property for the current template.
340: *
341: * @return The title for the template used in the current request, or
342: * <code>null</code> if title property was not found in
343: * the available resource bundles.
344: */
345: public String getTitle() {
346: String title = findProperty(TITLE_PROP);
347:
348: return title;
349: }
350:
351: /**
352: * Retrieves the localized version of the value of <code>property</code>.
353: *
354: * @param property
355: * The name of the property whose value to retrieve.
356: * @return The localized property value.
357: */
358: protected String findProperty(String property) {
359: String value = null;
360:
361: String templateName = data.getTarget().replace(',', '/');
362:
363: String l10nKey = property;
364: String prefix = getPrefix(templateName + '.');
365: if (prefix != null) {
366: l10nKey = prefix + l10nKey;
367: }
368: value = get(l10nKey);
369: Log.get().debug(
370: "ScarabLocalizationTool: Localized value is '" + value
371: + '\'');
372:
373: return value;
374: }
375:
376: /**
377: * Change the BundlePrefix. Keep the original value for later
378: * restore
379: * @param prefix
380: */
381: public void setBundlePrefix(String prefix) {
382: oldBundlePrefix = bundlePrefix;
383: bundlePrefix = prefix;
384: }
385:
386: /**
387: * Restore the old Bundle Prefix to it's previous value.
388: */
389: public void restoreBundlePrefix() {
390: bundlePrefix = oldBundlePrefix;
391: }
392:
393: /**
394: * Get the default ResourceBundle name
395: */
396: protected String getBundleName() {
397: String name = Localization.getDefaultBundleName();
398: return (bundlePrefix == null) ? name : bundlePrefix + name;
399: }
400:
401: /**
402: * Gets the primary locale.
403: * The primary locale is the locale which will be choosen
404: * at first from the set of Locales which are accepted by the user
405: * as defined on the Browser language preferrences.
406: * @return The primary locale currently in use.
407: */
408: public Locale getPrimaryLocale() {
409: return (locales == null || locales.size() == 0) ? super
410: .getLocale() : (Locale) locales.iterator().next();
411: }
412:
413: // ---- ApplicationTool implementation ----------------------------------
414:
415: /**
416: * Initialize the tool. Within the turbine pull service this tool is
417: * initialized with a RunData. However, the tool can also be initialized
418: * with a Locale.
419: */
420: public void init(Object obj) {
421: super .init(obj);
422: if (obj instanceof RunData) {
423: data = (RunData) obj;
424: locales = getPreferredLocales();
425: } else if (obj instanceof Locale) {
426: locales = new ArrayList();
427: locales.add(obj);
428: locales.add(DEFAULT_LOCALE);
429: locales.add(null);
430: }
431: }
432:
433: /**
434: * Reset this instance to initial values.
435: * Probably needed for reuse of ScarabLocalizationTool Instances.
436: */
437: public void refresh() {
438: super .refresh();
439: data = null;
440: bundlePrefix = null;
441: oldBundlePrefix = null;
442: locales = null;
443: setFilterEnabled(true);
444: }
445:
446: // ===========================
447: // Private utility methods ...
448: // ===========================
449:
450: /**
451: * Utility method: Get a Collection of possible locales
452: * to be used as specified in the Browser settings. Adds
453: * the DEFAULT_LOCALE as last resort to the list.
454: * Additionally adds a final null to the list. So be prepared
455: * to see a null pointer when you iterate through the list.
456: * @return
457: */
458: private List getPreferredLocales() {
459: List result = new ArrayList(3);
460: String localeAsString = getBrowserLocalesAsString();
461: LocaleTokenizer localeTokenizer = new LocaleTokenizer(
462: localeAsString);
463: while (localeTokenizer.hasNext()) {
464: Locale browserLocale = (Locale) localeTokenizer.next();
465: Locale finalLocale = getFinalLocaleFor(browserLocale);
466: if (finalLocale != null) {
467: result.add(finalLocale);
468: }
469: }
470: result.add(DEFAULT_LOCALE);
471: result.add(null);
472: return result;
473: }
474:
475: /**
476: * Contains a map of Locales which support given
477: * browserLocales.
478: */
479: static private Map supportedLocaleMap = new HashMap();
480: /**
481: * Contains a map of Locales which do NOT support given
482: * browserLocales.
483: */
484: static private Map unsupportedLocaleMap = new HashMap();
485:
486: /**
487: * Return the locale, which will be used to resolve
488: * keys of the given browserLocale. This method returns
489: * null, when Scarab does not directly support the
490: * browserLocale.
491: * @param browserLocale
492: * @return
493: */
494: private Locale getFinalLocaleFor(Locale browserLocale) {
495: Locale result = (Locale) supportedLocaleMap.get(browserLocale);
496: if (result == null) {
497: if (unsupportedLocaleMap.get(browserLocale) == null) {
498: ResourceBundle bundle;
499: try {
500: bundle = ResourceBundle.getBundle(getBundleName(),
501: browserLocale);
502: } catch (Exception e) {
503: // [HD] This should not happen, but it does happen;
504: // The problem raises when the system locale is not
505: // supported by Scarab. This was reported on Windows
506: // systems. This needs to be further investigated.
507: // setting bundle to null here enforces usage of the
508: // default ResourceBundle (en-US)
509: bundle = null;
510: }
511:
512: if (bundle != null) {
513: Locale finalLocale = bundle.getLocale();
514: String initialLanguage = browserLocale
515: .getLanguage();
516: String finalLanguage = finalLocale.getLanguage();
517: if (initialLanguage.equals(finalLanguage)) {
518: result = finalLocale;
519: supportedLocaleMap.put(browserLocale,
520: finalLocale);
521: } else {
522: unsupportedLocaleMap.put(browserLocale,
523: finalLocale);
524: }
525: } else {
526: Log.get().error(
527: "ScarabLocalizationTool: ResourceBundle '"
528: + getBundleName()
529: + "' -> not resolved for Locale '"
530: + browserLocale + "'.");
531: }
532: }
533: }
534: return result;
535: }
536:
537: /**
538: * Utility method: Get the content of the Browser localizationj settings.
539: * Return an empty String when no Browser settings are defined.
540: * @param acceptLanguage
541: * @param request
542: */
543: private String getBrowserLocalesAsString() {
544: String acceptLanguage = LocalizationService.ACCEPT_LANGUAGE;
545: HttpServletRequest request = data.getRequest();
546: String browserLocaleAsString = request
547: .getHeader(acceptLanguage);
548: if (browserLocaleAsString == null) {
549: browserLocaleAsString = "";
550: }
551: return browserLocaleAsString;
552: }
553:
554: /**
555: * Utility method: Resolve a given key using the given Locale.
556: * If the key can not be resolved, return null
557: * @param key
558: * @param locale
559: * @return
560: */
561: private String resolveKey(String key, Locale locale) {
562: String value;
563: try {
564: value = Localization
565: .getString(getBundleName(), locale, key);
566: } catch (MissingResourceException noKey) {
567: // No need for logging (already done in base class).
568: value = null;
569: }
570: return value;
571: }
572:
573: /**
574: * Utility method: Resolve a given key using the given Locale and apply
575: * the resource formatter. If the key can not be resolved,
576: * return null
577: * @param key
578: * @param args
579: * @param locale
580: * @return
581: */
582: private String formatKey(String key, Object[] args, Locale locale) {
583: String value;
584: try {
585: value = Localization.format(getBundleName(), locale, key,
586: args);
587: } catch (MissingResourceException noKey) {
588: value = null;
589: }
590: return value;
591: }
592:
593: /**
594: * Utility method: Resolve $variables placed within the args.
595: * Used before actually calling the resourceBundle formatter.
596: * @param args
597: * @return a cloned args list, or args when
598: * filtering is disabled. If args is null, also
599: * return null.
600: */
601: private Object[] resolveArgumentTemplates(Object[] args) {
602: // we are going to allow html text within resource bundles. This
603: // avoids problems in translations when links or other html tags
604: // would result in an unnatural breakup of the text. We need
605: // to apply the filtering here on the arguments which might contain
606: // user entered data, if we are going to skip the filtering later.
607:
608: Object[] result;
609: if (isFilterEnabled() && args != null && args.length > 0) {
610: result = new Object[args.length];
611: for (int i = 0; i < args.length; i++) {
612: Object obj = args[i];
613: // we don't filter Number, because these are sometimes passed
614: // to message formatter in order to make a choice. Converting
615: // the number to a String will cause error
616: if (obj != null) {
617: if (!((obj instanceof SkipFiltering) || (obj instanceof Number))) {
618: obj = ReferenceInsertionFilter.filter(obj
619: .toString());
620: }
621: }
622: result[i] = obj;
623: }
624: } else {
625: result = args;
626: }
627: return result;
628: }
629:
630: /**
631: * Utility method: create a Pseudovalue when the key
632: * has no resolution at all.
633: * @param key
634: * @return
635: */
636: private String createMissingResourceValue(String key) {
637: String value;
638: value = "ERROR! Missing resource (" + key + ")("
639: + Locale.getDefault() + ")";
640: Log.get().error(
641: "ScarabLocalizationTool: ERROR! Missing resource: "
642: + key);
643: return value;
644: }
645:
646: /**
647: * Utility method: create a Pseudovalue when the key
648: * can not be used as resource key.
649: * @param key
650: * @param e
651: * @return
652: */
653: private String createBadResourceValue(String key, Exception e) {
654: String value;
655: value = "ERROR! Bad resource (" + key + ")";
656: Log.get().error(
657: "ScarabLocalizationTool: ERROR! Bad resource: " + key
658: + ". See log for details.", e);
659: return value;
660: }
661:
662: /**
663: * Extract a message from an exception. This method checks, if
664: * the exception is Localizable. If so, we now can retrieve the localized exception message.
665: * Otherwise we retrieve the standard message via e.getLocalizedMessage().
666: * @param e
667: * @return
668: * throws NullPointerException if t is <code>null</code>
669: */
670: public String getMessage(Throwable t) {
671: String result;
672: if (t instanceof Localizable) {
673: result = ((Localizable) t).getMessage(this );
674: } else {
675: // [HD] note we reuse getLocalizedMessage() in case the exception
676: // coming from a third party library is also localized.
677: // [JEROME] After rethinking this, I am not sure that this else {}
678: // would work. The intent is nice but the implementation perhaps
679: // naive. As I said in the Localizable javadoc, implementation of
680: // getLocalizedMessage() probably requires the implementation of
681: // an IoC pattern. That means somebody would have to say to the
682: // third party library which locale to use for the localization.
683: // Perhaps register it to the instance, or to a global Localizer,
684: // or anything that the getLocalizedMessage() implementation
685: // would use to properly localize. Just calling getLocalizedMessage()
686: // wouldn't work without this prior registration, which might be
687: // library dependent.
688: result = t.getLocalizedMessage();
689: }
690: return result;
691: }
692:
693: }
|