001: /***************************************************************
002: * This file is part of the [fleXive](R) project.
003: *
004: * Copyright (c) 1999-2007
005: * UCS - unique computing solutions gmbh (http://www.ucs.at)
006: * All rights reserved
007: *
008: * The [fleXive](R) project is free software; you can redistribute
009: * it and/or modify it under the terms of the GNU General Public
010: * License as published by the Free Software Foundation;
011: * either version 2 of the License, or (at your option) any
012: * later version.
013: *
014: * The GNU General Public License can be found at
015: * http://www.gnu.org/copyleft/gpl.html.
016: * A copy is found in the textfile GPL.txt and important notices to the
017: * license from the author are found in LICENSE.txt distributed with
018: * these libraries.
019: *
020: * This library is distributed in the hope that it will be useful,
021: * but WITHOUT ANY WARRANTY; without even the implied warranty of
022: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
023: * GNU General Public License for more details.
024: *
025: * For further information about UCS - unique computing solutions gmbh,
026: * please see the company website: http://www.ucs.at
027: *
028: * For further information about [fleXive](R), please see the
029: * project website: http://www.flexive.org
030: *
031: *
032: * This copyright notice MUST APPEAR in all copies of the file!
033: ***************************************************************/package com.flexive.faces.beans;
034:
035: import com.flexive.faces.FxJsfUtils;
036: import com.flexive.shared.FxContext;
037: import com.flexive.shared.FxFormatUtils;
038: import com.flexive.shared.FxSharedUtils;
039: import com.flexive.shared.value.FxValue;
040: import com.flexive.shared.value.renderer.FxValueRendererFactory;
041: import org.apache.commons.logging.Log;
042: import org.apache.commons.logging.LogFactory;
043:
044: import java.io.IOException;
045: import java.net.URL;
046: import java.net.URLClassLoader;
047: import java.util.*;
048: import java.util.concurrent.ConcurrentHashMap;
049: import java.util.concurrent.ConcurrentMap;
050: import java.util.concurrent.CopyOnWriteArrayList;
051:
052: /**
053: * <p>A generic localization beans for messages displayed in the UI. The MessageBean wraps
054: * one or more {@link ResourceBundle ResourceBundles} that provide localized messages for
055: * web applications or plugins. By providing a localized resource bundle with a fixed name
056: * in your plugin/application JAR file, these messages will be automatically detected during
057: * startup and can be accessed through the message beans.</p>
058: * <p/>
059: * <p>
060: * Include the resource bundle with one of the following base names in the root directory
061: * of a JAR file deployed with the application:
062: * <p/>
063: * <table>
064: * <tr>
065: * <th>{@link #BUNDLE_APPLICATIONS}</th>
066: * <td>for web applications</td>
067: * </tr>
068: * <tr>
069: * <th>{@link #BUNDLE_PLUGINS}</th>
070: * <td>for plugins (or other JAR files providing localized messages)</td>
071: * </tr>
072: * </table>
073: * <p/>
074: * Currently both resource bundle types are treated equally, except that all application resource
075: * bundles are queried before the first plugin resource bundle.
076: * </p>
077: * <p/>
078: * <p><b>Usage:</b> fxMessageBean[key] to get the translation
079: * of the given property name in the user's language.
080: * Parameters in the localized message can be replaced too, by placing EL expressions inside the lookup string:
081: * Using a message declaration of "my.message.key=1+1 is: {0}", the placeholder {0} will be replaced
082: * using the following EL code:
083: * <pre>
084: * fxMessageBean['my.message.key,#{1+1}']
085: * </pre>
086: * </p>
087: *
088: * @author Daniel Lichtenberger (daniel.lichtenberger@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
089: * @version $Rev: 90 $
090: */
091: public class MessageBean extends HashMap {
092: private static final long serialVersionUID = 2176514561683264331L;
093:
094: private static class MessageKey {
095: private final Locale locale;
096: private final String key;
097:
098: private MessageKey(Locale locale, String key) {
099: this .locale = locale;
100: this .key = key;
101: }
102:
103: @Override
104: public boolean equals(Object o) {
105: if (this == o)
106: return true;
107: if (o == null || getClass() != o.getClass())
108: return false;
109:
110: MessageKey that = (MessageKey) o;
111:
112: if (!key.equals(that.key))
113: return false;
114: if (!locale.equals(that.locale))
115: return false;
116:
117: return true;
118: }
119:
120: @Override
121: public int hashCode() {
122: int result;
123: result = locale.hashCode();
124: result = 31 * result + key.hashCode();
125: return result;
126: }
127: }
128:
129: /**
130: * Resource bundle name for plugin packages.
131: */
132: public static final String BUNDLE_PLUGINS = "PluginMessages";
133: /**
134: * Resource bundle name for web applications.
135: */
136: public static final String BUNDLE_APPLICATIONS = "ApplicationMessages";
137:
138: private static final Log LOG = LogFactory.getLog(MessageBean.class);
139: private static final List<BundleReference> resourceBundles = new CopyOnWriteArrayList<BundleReference>();
140: private static final ConcurrentMap<String, ResourceBundle> cachedBundles = new ConcurrentHashMap<String, ResourceBundle>();
141: private static final ConcurrentMap<MessageKey, String> cachedMessages = new ConcurrentHashMap<MessageKey, String>();
142: private static volatile boolean initialized = false;
143:
144: /**
145: * Return the managed instance of the message beans.
146: *
147: * @return the managed instance of the message beans.
148: */
149: public static MessageBean getInstance() {
150: return (MessageBean) FxJsfUtils.getManagedBean("fxMessageBean");
151: }
152:
153: /**
154: * Return the localized message, replacing {0}...{n} with the given args.
155: * < b/>
156: * Parameters may be contained in the key and are comma separated. JSF EL expressions
157: * are evaluated in the current faces context.<br/>
158: * Examples: <br/>
159: * key="xx.yy.myMessage,myParam1,myParam2" <br/>
160: * key="xx.yy.myMessage,#{1+2},#{myBean.value},'some literal string value'"
161: *
162: * @param key the message key (optional with parameters)
163: * @return the formatted message
164: */
165: @Override
166: public Object get(Object key) {
167: // The key may contain parameters, which are comma separated
168: String sKey = String.valueOf(key);
169: String sParams[] = null;
170: try {
171: String[] arr = FxSharedUtils.splitLiterals(sKey);
172: sKey = arr[0];
173: if (arr.length > 1) {
174: sParams = new String[arr.length - 1];
175: System.arraycopy(arr, 1, sParams, 0, sParams.length);
176: for (int i = 1; i < arr.length; i++) {
177: // evaluate parameters
178: Object value;
179: try {
180: value = FxJsfUtils.evalObject(arr[i]);
181: } catch (Exception e) {
182: LOG.warn("Failed to evaluate parameter "
183: + arr[i] + ": " + e.getMessage(), e);
184: value = "";
185: }
186: //noinspection unchecked
187: sParams[i - 1] = value != null ? (value instanceof FxValue ? FxValueRendererFactory
188: .getInstance().format((FxValue) value)
189: : value.toString())
190: : null;
191: }
192: }
193: } catch (Throwable t) {
194: LOG.error("Failed to convert parameters (ignored): "
195: + t.getMessage(), t);
196: }
197:
198: return getMessage(sKey, (Object[]) sParams);
199: }
200:
201: /**
202: * Return the localized message, replacing {0}...{n} with the given args. FxString
203: * objects will be translated in the user's locale automatically.
204: *
205: * @param key message key
206: * @param args optional arguments for
207: * @return the formatted message
208: */
209: public String getMessage(String key, Object... args) {
210: String result;
211: try {
212: result = getResource(key);
213: if (args != null && args.length > 0) {
214: result = FxFormatUtils.formatResource(result, FxContext
215: .get().getLanguage().getId(), args);
216: }
217: return result;
218: } catch (MissingResourceException e) {
219: LOG.warn("Unknown message key: " + key);
220: return "??" + key + "??";
221: }
222: }
223:
224: /**
225: * Returns the resource bundle, which is cached within the request.
226: *
227: * @param key resource key
228: * @return the resource bundle
229: */
230: public String getResource(String key) {
231: final Locale locale = FxContext.get().getLocale();
232: if (!initialized) {
233: initialize();
234: }
235: final MessageKey messageKey = new MessageKey(locale, key);
236: if (cachedMessages.containsKey(messageKey)) {
237: return cachedMessages.get(messageKey);
238: }
239: for (BundleReference bundleReference : resourceBundles) {
240: try {
241: final ResourceBundle bundle = getResources(
242: bundleReference, locale);
243: final String message = bundle.getString(key);
244: cachedMessages.putIfAbsent(messageKey, message);
245: return message;
246: } catch (MissingResourceException e) {
247: // continue with next bundle
248: }
249: }
250: throw new MissingResourceException("Resource not found",
251: "MessageBean", key);
252: }
253:
254: /**
255: * Return the resource bundle in the given locale. Uses caching to speed up
256: * lookups.
257: *
258: * @param bundleReference the bundle reference object
259: * @param locale the requested locale
260: * @return the resource bundle in the requested locale
261: */
262: private ResourceBundle getResources(
263: BundleReference bundleReference, Locale locale) {
264: final String key = bundleReference.getCacheKey(locale);
265: if (cachedBundles.get(key) == null) {
266: cachedBundles.putIfAbsent(key, bundleReference
267: .getBundle(locale));
268: }
269: return cachedBundles.get(key);
270: }
271:
272: /**
273: * Initialize the application resource bundles. Scans the classpath for resource bundles
274: * for a predefined set of names ({@link #BUNDLE_APPLICATIONS} and {@link #BUNDLE_PLUGINS}),
275: * and then adds resource references that use {@link URLClassLoader URLClassLoaders} for loading
276: * the associated resource bundles.
277: */
278: private static synchronized void initialize() {
279: if (initialized) {
280: return;
281: }
282: try {
283: addResources(BUNDLE_APPLICATIONS);
284: addResources(BUNDLE_PLUGINS);
285: } catch (IOException e) {
286: LOG.error("Failed to initialize plugin message resources: "
287: + e.getMessage(), e);
288: } finally {
289: initialized = true;
290: }
291: }
292:
293: /**
294: * Add a resource reference for the given resource base name.
295: *
296: * @param baseName the resource name (e.g. "ApplicationResources")
297: * @throws IOException if an I/O error occured while looking for resources
298: */
299: private static void addResources(String baseName)
300: throws IOException {
301: // scan classpath
302: final Enumeration<URL> resources = Thread.currentThread()
303: .getContextClassLoader().getResources(
304: baseName + ".properties");
305: while (resources.hasMoreElements()) {
306: final URL resourceURL = resources.nextElement();
307: try {
308: // expected format: file:/some/path/to/file.jar!{baseName}.properties
309: final int jarDelim = resourceURL.getPath().lastIndexOf(
310: ".jar!");
311: if (jarDelim == -1) {
312: LOG
313: .warn("Cannot use message resources because they are not stored in a jar file: "
314: + resourceURL.getPath());
315: continue;
316: }
317: if (!resourceURL.getPath().startsWith("file:")) {
318: LOG
319: .warn("Cannot use message resources because they are not served from the file system: "
320: + resourceURL.getPath());
321: continue;
322: }
323:
324: // "file:" and everything after ".jar" gets stripped for the class loader URL
325: final URL jarURL = new URL("file", null, resourceURL
326: .getPath().substring("file:".length(),
327: jarDelim + 4));
328: addResourceBundle(baseName, new URLClassLoader(
329: new URL[] { jarURL }));
330:
331: LOG.info("Added plugin message resources for "
332: + jarURL.getPath());
333: } catch (Exception e) {
334: LOG.error(
335: "Failed to add plugin resources for URL "
336: + resourceURL.getPath() + ": "
337: + e.getMessage(), e);
338: }
339: }
340: }
341:
342: /**
343: * Add a resource bundle with the given name and classloader.
344: *
345: * @param baseName the resource base name
346: * @param loader the class loader to be used
347: */
348: private static void addResourceBundle(String baseName,
349: ClassLoader loader) {
350: resourceBundles.add(new BundleReference(baseName, loader));
351: }
352:
353: /**
354: * A resource bundle reference.
355: */
356: private static class BundleReference {
357: private final String baseName;
358: private final ClassLoader classLoader;
359:
360: /**
361: * Create a new bundle reference.
362: *
363: * @param baseName the fully qualified base name (e.g. "ApplicationResources")
364: * @param classLoader the class loader to be used for loading the resource bundle. If null,
365: * the context class loader will be used.
366: */
367: private BundleReference(String baseName, ClassLoader classLoader) {
368: this .baseName = baseName;
369: this .classLoader = classLoader;
370: }
371:
372: /**
373: * Returns the base name of the resource bundle (e.g. "ApplicationResources").
374: *
375: * @return the base name of the resource bundle (e.g. "ApplicationResources").
376: */
377: public String getBaseName() {
378: return baseName;
379: }
380:
381: /**
382: * Returns the class loader to be used for loading the bundle.
383: *
384: * @return the class loader to be used for loading the bundle.
385: */
386: public ClassLoader getClassLoader() {
387: return classLoader;
388: }
389:
390: /**
391: * Return the resource bundle in the given locale.
392: *
393: * @param locale the requested locale
394: * @return the resource bundle in the given locale.
395: */
396: public ResourceBundle getBundle(Locale locale) {
397: if (this .classLoader == null) {
398: return ResourceBundle.getBundle(baseName, locale);
399: } else {
400: return ResourceBundle.getBundle(baseName, locale,
401: classLoader);
402: }
403: }
404:
405: /**
406: * Return a cache key unique for this resource bundle and locale.
407: *
408: * @param locale the requested locale
409: * @return a cache key unique for this resource bundle and locale.
410: */
411: public String getCacheKey(Locale locale) {
412: final String localeSuffix = locale == null ? "" : "_"
413: + locale.toString();
414: if (this.classLoader == null) {
415: return baseName + localeSuffix;
416: } else {
417: return baseName + this.toString() + localeSuffix;
418: }
419: }
420: }
421:
422: }
|