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