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