001 /* ========================================================================
002 * JCommon : a free general purpose class library for the Java(tm) platform
003 * ========================================================================
004 *
005 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
006 *
007 * Project Info: http://www.jfree.org/jcommon/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * ---------------------
028 * ReadOnlyIterator.java
029 * ---------------------
030 * (C)opyright 2003, 2004, by Thomas Morgner and Contributors.
031 *
032 * Original Author: Thomas Morgner;
033 * Contributor(s): -;
034 *
035 * $Id: ResourceBundleSupport.java,v 1.11 2008/09/10 09:24:41 mungady Exp $
036 *
037 * Changes
038 * -------------------------
039 */
040 package org.jfree.util;
041
042 import java.awt.Image;
043 import java.awt.Toolkit;
044 import java.awt.event.InputEvent;
045 import java.awt.event.KeyEvent;
046 import java.awt.image.BufferedImage;
047 import java.lang.reflect.Field;
048 import java.net.URL;
049 import java.text.MessageFormat;
050 import java.util.Arrays;
051 import java.util.Locale;
052 import java.util.MissingResourceException;
053 import java.util.ResourceBundle;
054 import java.util.TreeMap;
055 import java.util.TreeSet;
056 import javax.swing.Icon;
057 import javax.swing.ImageIcon;
058 import javax.swing.JMenu;
059 import javax.swing.KeyStroke;
060
061 /**
062 * An utility class to ease up using property-file resource bundles.
063 * <p/>
064 * The class support references within the resource bundle set to minimize the
065 * occurence of duplicate keys. References are given in the format:
066 * <pre>
067 * a.key.name=@referenced.key
068 * </pre>
069 * <p/>
070 * A lookup to a key in an other resource bundle should be written by
071 * <pre>
072 * a.key.name=@@resourcebundle_name@referenced.key
073 * </pre>
074 *
075 * @author Thomas Morgner
076 */
077 public class ResourceBundleSupport
078 {
079 /**
080 * The resource bundle that will be used for local lookups.
081 */
082 private ResourceBundle resources;
083
084 /**
085 * A cache for string values, as looking up the cache is faster than looking
086 * up the value in the bundle.
087 */
088 private TreeMap cache;
089 /**
090 * The current lookup path when performing non local lookups. This prevents
091 * infinite loops during such lookups.
092 */
093 private TreeSet lookupPath;
094
095 /**
096 * The name of the local resource bundle.
097 */
098 private String resourceBase;
099
100 /**
101 * The locale for this bundle.
102 */
103 private Locale locale;
104
105 /**
106 * Creates a new instance.
107 *
108 * @param locale the locale.
109 * @param baseName the base name of the resource bundle, a fully qualified
110 * class name
111 */
112 public ResourceBundleSupport(final Locale locale, final String baseName)
113 {
114 this(locale, ResourceBundle.getBundle(baseName, locale), baseName);
115 }
116
117 /**
118 * Creates a new instance.
119 *
120 * @param locale the locale for which this resource bundle is
121 * created.
122 * @param resourceBundle the resourcebundle
123 * @param baseName the base name of the resource bundle, a fully
124 * qualified class name
125 */
126 protected ResourceBundleSupport(final Locale locale,
127 final ResourceBundle resourceBundle,
128 final String baseName)
129 {
130 if (locale == null)
131 {
132 throw new NullPointerException("Locale must not be null");
133 }
134 if (resourceBundle == null)
135 {
136 throw new NullPointerException("Resources must not be null");
137 }
138 if (baseName == null)
139 {
140 throw new NullPointerException("BaseName must not be null");
141 }
142 this.locale = locale;
143 this.resources = resourceBundle;
144 this.resourceBase = baseName;
145 this.cache = new TreeMap();
146 this.lookupPath = new TreeSet();
147 }
148
149 /**
150 * Creates a new instance.
151 *
152 * @param locale the locale for which the resource bundle is
153 * created.
154 * @param resourceBundle the resourcebundle
155 */
156 public ResourceBundleSupport(final Locale locale,
157 final ResourceBundle resourceBundle)
158 {
159 this(locale, resourceBundle, resourceBundle.toString());
160 }
161
162 /**
163 * Creates a new instance.
164 *
165 * @param baseName the base name of the resource bundle, a fully qualified
166 * class name
167 */
168 public ResourceBundleSupport(final String baseName)
169 {
170 this(Locale.getDefault(), ResourceBundle.getBundle(baseName), baseName);
171 }
172
173 /**
174 * Creates a new instance.
175 *
176 * @param resourceBundle the resourcebundle
177 * @param baseName the base name of the resource bundle, a fully
178 * qualified class name
179 */
180 protected ResourceBundleSupport(final ResourceBundle resourceBundle,
181 final String baseName)
182 {
183 this(Locale.getDefault(), resourceBundle, baseName);
184 }
185
186 /**
187 * Creates a new instance.
188 *
189 * @param resourceBundle the resourcebundle
190 */
191 public ResourceBundleSupport(final ResourceBundle resourceBundle)
192 {
193 this(Locale.getDefault(), resourceBundle, resourceBundle.toString());
194 }
195
196 /**
197 * The base name of the resource bundle.
198 *
199 * @return the resource bundle's name.
200 */
201 protected final String getResourceBase()
202 {
203 return this.resourceBase;
204 }
205
206 /**
207 * Gets a string for the given key from this resource bundle or one of its
208 * parents. If the key is a link, the link is resolved and the referenced
209 * string is returned instead.
210 *
211 * @param key the key for the desired string
212 * @return the string for the given key
213 * @throws NullPointerException if <code>key</code> is <code>null</code>
214 * @throws MissingResourceException if no object for the given key can be
215 * found
216 * @throws ClassCastException if the object found for the given key is
217 * not a string
218 */
219 public synchronized String getString(final String key)
220 {
221 final String retval = (String) this.cache.get(key);
222 if (retval != null)
223 {
224 return retval;
225 }
226 this.lookupPath.clear();
227 return internalGetString(key);
228 }
229
230 /**
231 * Performs the lookup for the given key. If the key points to a link the
232 * link is resolved and that key is looked up instead.
233 *
234 * @param key the key for the string
235 * @return the string for the given key
236 */
237 protected String internalGetString(final String key)
238 {
239 if (this.lookupPath.contains(key))
240 {
241 throw new MissingResourceException
242 ("InfiniteLoop in resource lookup",
243 getResourceBase(), this.lookupPath.toString());
244 }
245 final String fromResBundle = this.resources.getString(key);
246 if (fromResBundle.startsWith("@@"))
247 {
248 // global forward ...
249 final int idx = fromResBundle.indexOf('@', 2);
250 if (idx == -1)
251 {
252 throw new MissingResourceException
253 ("Invalid format for global lookup key.", getResourceBase(), key);
254 }
255 try
256 {
257 final ResourceBundle res = ResourceBundle.getBundle
258 (fromResBundle.substring(2, idx));
259 return res.getString(fromResBundle.substring(idx + 1));
260 }
261 catch (Exception e)
262 {
263 Log.error("Error during global lookup", e);
264 throw new MissingResourceException
265 ("Error during global lookup", getResourceBase(), key);
266 }
267 }
268 else if (fromResBundle.startsWith("@"))
269 {
270 // local forward ...
271 final String newKey = fromResBundle.substring(1);
272 this.lookupPath.add(key);
273 final String retval = internalGetString(newKey);
274
275 this.cache.put(key, retval);
276 return retval;
277 }
278 else
279 {
280 this.cache.put(key, fromResBundle);
281 return fromResBundle;
282 }
283 }
284
285 /**
286 * Returns an scaled icon suitable for buttons or menus.
287 *
288 * @param key the name of the resource bundle key
289 * @param large true, if the image should be scaled to 24x24, or false for
290 * 16x16
291 * @return the icon.
292 */
293 public Icon getIcon(final String key, final boolean large)
294 {
295 final String name = getString(key);
296 return createIcon(name, true, large);
297 }
298
299 /**
300 * Returns an unscaled icon.
301 *
302 * @param key the name of the resource bundle key
303 * @return the icon.
304 */
305 public Icon getIcon(final String key)
306 {
307 final String name = getString(key);
308 return createIcon(name, false, false);
309 }
310
311 /**
312 * Returns the mnemonic stored at the given resourcebundle key. The mnemonic
313 * should be either the symbolic name of one of the KeyEvent.VK_* constants
314 * (without the 'VK_') or the character for that key.
315 * <p/>
316 * For the enter key, the resource bundle would therefore either contain
317 * "ENTER" or "\n".
318 * <pre>
319 * a.resourcebundle.key=ENTER
320 * an.other.resourcebundle.key=\n
321 * </pre>
322 *
323 * @param key the resourcebundle key
324 * @return the mnemonic
325 */
326 public Integer getMnemonic(final String key)
327 {
328 final String name = getString(key);
329 return createMnemonic(name);
330 }
331
332 /**
333 * Returns an optional mnemonic.
334 *
335 * @param key the key.
336 *
337 * @return The mnemonic.
338 */
339 public Integer getOptionalMnemonic(final String key)
340 {
341 final String name = getString(key);
342 if (name != null && name.length() > 0)
343 {
344 return createMnemonic(name);
345 }
346 return null;
347 }
348
349 /**
350 * Returns the keystroke stored at the given resourcebundle key.
351 * <p/>
352 * The keystroke will be composed of a simple key press and the plattform's
353 * MenuKeyMask.
354 * <p/>
355 * The keystrokes character key should be either the symbolic name of one of
356 * the KeyEvent.VK_* constants or the character for that key.
357 * <p/>
358 * For the 'A' key, the resource bundle would therefore either contain
359 * "VK_A" or "a".
360 * <pre>
361 * a.resourcebundle.key=VK_A
362 * an.other.resourcebundle.key=a
363 * </pre>
364 *
365 * @param key the resourcebundle key
366 * @return the mnemonic
367 * @see Toolkit#getMenuShortcutKeyMask()
368 */
369 public KeyStroke getKeyStroke(final String key)
370 {
371 return getKeyStroke(key, getMenuKeyMask());
372 }
373
374 /**
375 * Returns an optional key stroke.
376 *
377 * @param key the key.
378 *
379 * @return The key stroke.
380 */
381 public KeyStroke getOptionalKeyStroke(final String key)
382 {
383 return getOptionalKeyStroke(key, getMenuKeyMask());
384 }
385
386 /**
387 * Returns the keystroke stored at the given resourcebundle key.
388 * <p/>
389 * The keystroke will be composed of a simple key press and the given
390 * KeyMask. If the KeyMask is zero, a plain Keystroke is returned.
391 * <p/>
392 * The keystrokes character key should be either the symbolic name of one of
393 * the KeyEvent.VK_* constants or the character for that key.
394 * <p/>
395 * For the 'A' key, the resource bundle would therefore either contain
396 * "VK_A" or "a".
397 * <pre>
398 * a.resourcebundle.key=VK_A
399 * an.other.resourcebundle.key=a
400 * </pre>
401 *
402 * @param key the resourcebundle key.
403 * @param mask the mask.
404 *
405 * @return the mnemonic
406 * @see Toolkit#getMenuShortcutKeyMask()
407 */
408 public KeyStroke getKeyStroke(final String key, final int mask)
409 {
410 final String name = getString(key);
411 return KeyStroke.getKeyStroke(createMnemonic(name).intValue(), mask);
412 }
413
414 /**
415 * Returns an optional key stroke.
416 *
417 * @param key the key.
418 * @param mask the mask.
419 *
420 * @return The key stroke.
421 */
422 public KeyStroke getOptionalKeyStroke(final String key, final int mask)
423 {
424 final String name = getString(key);
425
426 if (name != null && name.length() > 0)
427 {
428 return KeyStroke.getKeyStroke(createMnemonic(name).intValue(), mask);
429 }
430 return null;
431 }
432
433 /**
434 * Returns a JMenu created from a resource bundle definition.
435 * <p/>
436 * The menu definition consists of two keys, the name of the menu and the
437 * mnemonic for that menu. Both keys share a common prefix, which is
438 * extended by ".name" for the name of the menu and ".mnemonic" for the
439 * mnemonic.
440 * <p/>
441 * <pre>
442 * # define the file menu
443 * menu.file.name=File
444 * menu.file.mnemonic=F
445 * </pre>
446 * The menu definition above can be used to create the menu by calling
447 * <code>createMenu ("menu.file")</code>.
448 *
449 * @param keyPrefix the common prefix for that menu
450 * @return the created menu
451 */
452 public JMenu createMenu(final String keyPrefix)
453 {
454 final JMenu retval = new JMenu();
455 retval.setText(getString(keyPrefix + ".name"));
456 retval.setMnemonic(getMnemonic(keyPrefix + ".mnemonic").intValue());
457 return retval;
458 }
459
460 /**
461 * Returns a URL pointing to a resource located in the classpath. The
462 * resource is looked up using the given key.
463 * <p/>
464 * Example: The load a file named 'logo.gif' which is stored in a java
465 * package named 'org.jfree.resources':
466 * <pre>
467 * mainmenu.logo=org/jfree/resources/logo.gif
468 * </pre>
469 * The URL for that file can be queried with: <code>getResource("mainmenu.logo");</code>.
470 *
471 * @param key the key for the resource
472 * @return the resource URL
473 */
474 public URL getResourceURL(final String key)
475 {
476 final String name = getString(key);
477 final URL in = ObjectUtilities.getResource(name, ResourceBundleSupport.class);
478 if (in == null)
479 {
480 Log.warn("Unable to find file in the class path: " + name + "; key=" + key);
481 }
482 return in;
483 }
484
485
486 /**
487 * Attempts to load an image from classpath. If this fails, an empty image
488 * icon is returned.
489 *
490 * @param resourceName the name of the image. The name should be a global
491 * resource name.
492 * @param scale true, if the image should be scaled, false otherwise
493 * @param large true, if the image should be scaled to 24x24, or
494 * false for 16x16
495 * @return the image icon.
496 */
497 private ImageIcon createIcon(final String resourceName, final boolean scale,
498 final boolean large)
499 {
500 final URL in = ObjectUtilities.getResource(resourceName, ResourceBundleSupport.class);
501 ;
502 if (in == null)
503 {
504 Log.warn("Unable to find file in the class path: " + resourceName);
505 return new ImageIcon(createTransparentImage(1, 1));
506 }
507 final Image img = Toolkit.getDefaultToolkit().createImage(in);
508 if (img == null)
509 {
510 Log.warn("Unable to instantiate the image: " + resourceName);
511 return new ImageIcon(createTransparentImage(1, 1));
512 }
513 if (scale)
514 {
515 if (large)
516 {
517 return new ImageIcon(img.getScaledInstance(24, 24, Image.SCALE_SMOOTH));
518 }
519 return new ImageIcon(img.getScaledInstance(16, 16, Image.SCALE_SMOOTH));
520 }
521 return new ImageIcon(img);
522 }
523
524 /**
525 * Creates the Mnemonic from the given String. The String consists of the
526 * name of the VK constants of the class KeyEvent without VK_*.
527 *
528 * @param keyString the string
529 * @return the mnemonic as integer
530 */
531 private Integer createMnemonic(final String keyString)
532 {
533 if (keyString == null)
534 {
535 throw new NullPointerException("Key is null.");
536 }
537 if (keyString.length() == 0)
538 {
539 throw new IllegalArgumentException("Key is empty.");
540 }
541 int character = keyString.charAt(0);
542 if (keyString.startsWith("VK_"))
543 {
544 try
545 {
546 final Field f = KeyEvent.class.getField(keyString);
547 final Integer keyCode = (Integer) f.get(null);
548 character = keyCode.intValue();
549 }
550 catch (Exception nsfe)
551 {
552 // ignore the exception ...
553 }
554 }
555 return new Integer(character);
556 }
557
558 /**
559 * Returns the plattforms default menu shortcut keymask.
560 *
561 * @return the default key mask.
562 */
563 private int getMenuKeyMask()
564 {
565 try
566 {
567 return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
568 }
569 catch (UnsupportedOperationException he)
570 {
571 // headless exception extends UnsupportedOperation exception,
572 // but the HeadlessException is not defined in older JDKs...
573 return InputEvent.CTRL_MASK;
574 }
575 }
576
577 /**
578 * Creates a transparent image. These can be used for aligning menu items.
579 *
580 * @param width the width.
581 * @param height the height.
582 * @return the created transparent image.
583 */
584 private BufferedImage createTransparentImage(final int width,
585 final int height)
586 {
587 final BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
588 final int[] data = img.getRGB(0, 0, width, height, null, 0, width);
589 Arrays.fill(data, 0x00000000);
590 img.setRGB(0, 0, width, height, data, 0, width);
591 return img;
592 }
593
594 /**
595 * Creates a transparent icon. The Icon can be used for aligning menu
596 * items.
597 *
598 * @param width the width of the new icon
599 * @param height the height of the new icon
600 * @return the created transparent icon.
601 */
602 public Icon createTransparentIcon(final int width, final int height)
603 {
604 return new ImageIcon(createTransparentImage(width, height));
605 }
606
607 /**
608 * Formats the message stored in the resource bundle (using a
609 * MessageFormat).
610 *
611 * @param key the resourcebundle key
612 * @param parameter the parameter for the message
613 * @return the formated string
614 */
615 public String formatMessage(final String key, final Object parameter)
616 {
617 return formatMessage(key, new Object[]{parameter});
618 }
619
620 /**
621 * Formats the message stored in the resource bundle (using a
622 * MessageFormat).
623 *
624 * @param key the resourcebundle key
625 * @param par1 the first parameter for the message
626 * @param par2 the second parameter for the message
627 * @return the formated string
628 */
629 public String formatMessage(final String key,
630 final Object par1,
631 final Object par2)
632 {
633 return formatMessage(key, new Object[]{par1, par2});
634 }
635
636 /**
637 * Formats the message stored in the resource bundle (using a
638 * MessageFormat).
639 *
640 * @param key the resourcebundle key
641 * @param parameters the parameter collection for the message
642 * @return the formated string
643 */
644 public String formatMessage(final String key, final Object[] parameters)
645 {
646 final MessageFormat format = new MessageFormat(getString(key));
647 format.setLocale(getLocale());
648 return format.format(parameters);
649 }
650
651 /**
652 * Returns the current locale for this resource bundle.
653 *
654 * @return the locale.
655 */
656 public Locale getLocale()
657 {
658 return this.locale;
659 }
660 }