Source for org.jfree.util.ResourceBundleSupport

   1: /* ========================================================================
   2:  * JCommon : a free general purpose class library for the Java(tm) platform
   3:  * ========================================================================
   4:  *
   5:  * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
   6:  * 
   7:  * Project Info:  http://www.jfree.org/jcommon/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  * 
  27:  * ---------------------
  28:  * ReadOnlyIterator.java
  29:  * ---------------------
  30:  * (C)opyright 2003, 2004, by Thomas Morgner and Contributors.
  31:  *
  32:  * Original Author:  Thomas Morgner;
  33:  * Contributor(s):   -;
  34:  *
  35:  * $Id: ResourceBundleSupport.java,v 1.9 2006/09/26 11:22:10 taqua Exp $
  36:  *
  37:  * Changes
  38:  * -------------------------
  39:  */
  40: package org.jfree.util;
  41: 
  42: import java.awt.Image;
  43: import java.awt.Toolkit;
  44: import java.awt.event.InputEvent;
  45: import java.awt.event.KeyEvent;
  46: import java.awt.image.BufferedImage;
  47: import java.lang.reflect.Field;
  48: import java.net.URL;
  49: import java.text.MessageFormat;
  50: import java.util.Arrays;
  51: import java.util.Locale;
  52: import java.util.MissingResourceException;
  53: import java.util.ResourceBundle;
  54: import java.util.TreeMap;
  55: import java.util.TreeSet;
  56: import javax.swing.Icon;
  57: import javax.swing.ImageIcon;
  58: import javax.swing.JMenu;
  59: import javax.swing.KeyStroke;
  60: 
  61: /**
  62:  * An utility class to ease up using property-file resource bundles.
  63:  * <p/>
  64:  * The class support references within the resource bundle set to minimize the occurence
  65:  * of duplicate keys. References are given in the format:
  66:  * <pre>
  67:  * a.key.name=@referenced.key
  68:  * </pre>
  69:  * <p/>
  70:  * A lookup to a key in an other resource bundle should be written by
  71:  * <pre>
  72:  * a.key.name=@@resourcebundle_name@referenced.key
  73:  * </pre>
  74:  *
  75:  * @author Thomas Morgner
  76:  */
  77: public class ResourceBundleSupport {
  78:     /**
  79:      * The resource bundle that will be used for local lookups.
  80:      */
  81:     private ResourceBundle resources;
  82: 
  83:     /**
  84:      * A cache for string values, as looking up the cache is faster than looking up the
  85:      * value in the bundle.
  86:      */
  87:     private TreeMap cache;
  88:     /**
  89:      * The current lookup path when performing non local lookups. This prevents infinite
  90:      * loops during such lookups.
  91:      */
  92:     private TreeSet lookupPath;
  93: 
  94:     /**
  95:      * The name of the local resource bundle.
  96:      */
  97:     private String resourceBase;
  98: 
  99:     /** The locale for this bundle. */
 100:     private Locale locale;
 101: 
 102:     /**
 103:      * Creates a new instance.
 104:      *
 105:      * @param baseName the base name of the resource bundle, a fully qualified class name
 106:      */
 107:     public ResourceBundleSupport(final Locale locale, final String baseName) {
 108:         this(locale, ResourceBundle.getBundle(baseName, locale), baseName);
 109:     }
 110: 
 111:     /**
 112:      * Creates a new instance.
 113:      *
 114:      * @param locale         the locale for which this resource bundle is created.
 115:      * @param resourceBundle the resourcebundle
 116:      * @param baseName       the base name of the resource bundle, a fully qualified class name
 117:      */
 118:     protected ResourceBundleSupport(final Locale locale,
 119:                                     final ResourceBundle resourceBundle,
 120:                                     final String baseName) {
 121:         if (locale == null) {
 122:             throw new NullPointerException("Locale must not be null");
 123:         }
 124:         if (resourceBundle == null) {
 125:             throw new NullPointerException("Resources must not be null");
 126:         }
 127:         if (baseName == null) {
 128:             throw new NullPointerException("BaseName must not be null");
 129:         }
 130:         this.locale = locale;
 131:         this.resources = resourceBundle;
 132:         this.resourceBase = baseName;
 133:         this.cache = new TreeMap();
 134:         this.lookupPath = new TreeSet();
 135:     }
 136: 
 137:     /**
 138:      * Creates a new instance.
 139:      *
 140:      * @param locale         the locale for which the resource bundle is created.
 141:      * @param resourceBundle the resourcebundle
 142:      */
 143:     public ResourceBundleSupport(final Locale locale, final ResourceBundle resourceBundle) {
 144:         this(locale, resourceBundle, resourceBundle.toString());
 145:     }
 146: 
 147:     /**
 148:      * Creates a new instance.
 149:      *
 150:      * @param baseName the base name of the resource bundle, a fully qualified class name
 151:      */
 152:     public ResourceBundleSupport(final String baseName) {
 153:         this(Locale.getDefault(), ResourceBundle.getBundle(baseName), baseName);
 154:     }
 155: 
 156:     /**
 157:      * Creates a new instance.
 158:      *
 159:      * @param resourceBundle the resourcebundle
 160:      * @param baseName       the base name of the resource bundle, a fully qualified class name
 161:      */
 162:     protected ResourceBundleSupport(final ResourceBundle resourceBundle,
 163:                                     final String baseName) {
 164:         this(Locale.getDefault(), resourceBundle, baseName);
 165:     }
 166: 
 167:     /**
 168:      * Creates a new instance.
 169:      *
 170:      * @param resourceBundle the resourcebundle
 171:      */
 172:     public ResourceBundleSupport(final ResourceBundle resourceBundle) {
 173:         this(Locale.getDefault(), resourceBundle, resourceBundle.toString());
 174:     }
 175: 
 176:     /**
 177:      * The base name of the resource bundle.
 178:      *
 179:      * @return the resource bundle's name.
 180:      */
 181:     protected final String getResourceBase() {
 182:         return this.resourceBase;
 183:     }
 184: 
 185:     /**
 186:      * Gets a string for the given key from this resource bundle or one of its parents. If
 187:      * the key is a link, the link is resolved and the referenced string is returned
 188:      * instead.
 189:      *
 190:      * @param key the key for the desired string
 191:      * @return the string for the given key
 192:      * @throws NullPointerException     if <code>key</code> is <code>null</code>
 193:      * @throws MissingResourceException if no object for the given key can be found
 194:      * @throws ClassCastException       if the object found for the given key is not a
 195:      *                                  string
 196:      */
 197:     public synchronized String getString(final String key) {
 198:         final String retval = (String) this.cache.get(key);
 199:         if (retval != null) {
 200:             return retval;
 201:         }
 202:         this.lookupPath.clear();
 203:         return internalGetString(key);
 204:     }
 205: 
 206:     /**
 207:      * Performs the lookup for the given key. If the key points to a link the link is
 208:      * resolved and that key is looked up instead.
 209:      *
 210:      * @param key the key for the string
 211:      * @return the string for the given key
 212:      */
 213:     protected String internalGetString(final String key) {
 214:         if (this.lookupPath.contains(key)) {
 215:             throw new MissingResourceException
 216:                 ("InfiniteLoop in resource lookup",
 217:                     getResourceBase(), this.lookupPath.toString());
 218:         }
 219:         final String fromResBundle = this.resources.getString(key);
 220:         if (fromResBundle.startsWith("@@")) {
 221:             // global forward ...
 222:             final int idx = fromResBundle.indexOf('@', 2);
 223:             if (idx == -1) {
 224:                 throw new MissingResourceException
 225:                     ("Invalid format for global lookup key.", getResourceBase(), key);
 226:             }
 227:             try {
 228:                 final ResourceBundle res = ResourceBundle.getBundle
 229:                     (fromResBundle.substring(2, idx));
 230:                 return res.getString(fromResBundle.substring(idx + 1));
 231:             }
 232:             catch (Exception e) {
 233:                 Log.error("Error during global lookup", e);
 234:                 throw new MissingResourceException
 235:                     ("Error during global lookup", getResourceBase(), key);
 236:             }
 237:         }
 238:         else if (fromResBundle.startsWith("@")) {
 239:             // local forward ...
 240:             final String newKey = fromResBundle.substring(1);
 241:             this.lookupPath.add(key);
 242:             final String retval = internalGetString(newKey);
 243: 
 244:             this.cache.put(key, retval);
 245:             return retval;
 246:         }
 247:         else {
 248:             this.cache.put(key, fromResBundle);
 249:             return fromResBundle;
 250:         }
 251:     }
 252: 
 253:     /**
 254:      * Returns an scaled icon suitable for buttons or menus.
 255:      *
 256:      * @param key   the name of the resource bundle key
 257:      * @param large true, if the image should be scaled to 24x24, or false for 16x16
 258:      * @return the icon.
 259:      */
 260:     public Icon getIcon(final String key, final boolean large) {
 261:         final String name = getString(key);
 262:         return createIcon(name, true, large);
 263:     }
 264: 
 265:     /**
 266:      * Returns an unscaled icon.
 267:      *
 268:      * @param key the name of the resource bundle key
 269:      * @return the icon.
 270:      */
 271:     public Icon getIcon(final String key) {
 272:         final String name = getString(key);
 273:         return createIcon(name, false, false);
 274:     }
 275: 
 276:     /**
 277:      * Returns the mnemonic stored at the given resourcebundle key. The mnemonic should be
 278:      * either the symbolic name of one of the KeyEvent.VK_* constants (without the 'VK_') or
 279:      * the character for that key.
 280:      * <p/>
 281:      * For the enter key, the resource bundle would therefore either contain "ENTER" or
 282:      * "\n".
 283:      * <pre>
 284:      * a.resourcebundle.key=ENTER
 285:      * an.other.resourcebundle.key=\n
 286:      * </pre>
 287:      *
 288:      * @param key the resourcebundle key
 289:      * @return the mnemonic
 290:      */
 291:     public Integer getMnemonic(final String key) {
 292:         final String name = getString(key);
 293:         return createMnemonic(name);
 294:     }
 295: 
 296:     /**
 297:      * Returns the keystroke stored at the given resourcebundle key.
 298:      * <p/>
 299:      * The keystroke will be composed of a simple key press and the plattform's
 300:      * MenuKeyMask.
 301:      * <p/>
 302:      * The keystrokes character key should be either the symbolic name of one of the
 303:      * KeyEvent.VK_* constants or the character for that key.
 304:      * <p/>
 305:      * For the 'A' key, the resource bundle would therefore either contain "VK_A" or
 306:      * "a".
 307:      * <pre>
 308:      * a.resourcebundle.key=VK_A
 309:      * an.other.resourcebundle.key=a
 310:      * </pre>
 311:      *
 312:      * @param key the resourcebundle key
 313:      * @return the mnemonic
 314:      * @see Toolkit#getMenuShortcutKeyMask()
 315:      */
 316:     public KeyStroke getKeyStroke(final String key) {
 317:         return getKeyStroke(key, getMenuKeyMask());
 318:     }
 319: 
 320:     /**
 321:      * Returns the keystroke stored at the given resourcebundle key.
 322:      * <p/>
 323:      * The keystroke will be composed of a simple key press and the given
 324:      * KeyMask. If the KeyMask is zero, a plain Keystroke is returned.
 325:      * <p/>
 326:      * The keystrokes character key should be either the symbolic name of one of the
 327:      * KeyEvent.VK_* constants or the character for that key.
 328:      * <p/>
 329:      * For the 'A' key, the resource bundle would therefore either contain "VK_A" or
 330:      * "a".
 331:      * <pre>
 332:      * a.resourcebundle.key=VK_A
 333:      * an.other.resourcebundle.key=a
 334:      * </pre>
 335:      *
 336:      * @param key the resourcebundle key
 337:      * @return the mnemonic
 338:      * @see Toolkit#getMenuShortcutKeyMask()
 339:      */
 340:     public KeyStroke getKeyStroke(final String key, final int mask) {
 341:         final String name = getString(key);
 342:         return KeyStroke.getKeyStroke(createMnemonic(name).intValue(), mask);
 343: 
 344:     }
 345: 
 346:     /**
 347:      * Returns a JMenu created from a resource bundle definition.
 348:      * <p/>
 349:      * The menu definition consists of two keys, the name of the menu and the mnemonic for
 350:      * that menu. Both keys share a common prefix, which is extended by ".name" for the name
 351:      * of the menu and ".mnemonic" for the mnemonic.
 352:      * <p/>
 353:      * <pre>
 354:      * # define the file menu
 355:      * menu.file.name=File
 356:      * menu.file.mnemonic=F
 357:      * </pre>
 358:      * The menu definition above can be used to create the menu by calling <code>createMenu
 359:      * ("menu.file")</code>.
 360:      *
 361:      * @param keyPrefix the common prefix for that menu
 362:      * @return the created menu
 363:      */
 364:     public JMenu createMenu(final String keyPrefix) {
 365:         final JMenu retval = new JMenu();
 366:         retval.setText(getString(keyPrefix + ".name"));
 367:         retval.setMnemonic(getMnemonic(keyPrefix + ".mnemonic").intValue());
 368:         return retval;
 369:     }
 370: 
 371:     /**
 372:      * Returns a URL pointing to a resource located in the classpath. The resource is looked
 373:      * up using the given key.
 374:      * <p/>
 375:      * Example: The load a file named 'logo.gif' which is stored in a java package named
 376:      * 'org.jfree.resources':
 377:      * <pre>
 378:      * mainmenu.logo=org/jfree/resources/logo.gif
 379:      * </pre>
 380:      * The URL for that file can be queried with: <code>getResource("mainmenu.logo");</code>.
 381:      *
 382:      * @param key the key for the resource
 383:      * @return the resource URL
 384:      */
 385:     public URL getResourceURL(final String key) {
 386:         final String name = getString(key);
 387:         final URL in = ObjectUtilities.getResource(name, ResourceBundleSupport.class);
 388:         if (in == null) {
 389:             Log.warn("Unable to find file in the class path: " + name + "; key=" + key);
 390:         }
 391:         return in;
 392:     }
 393: 
 394: 
 395:     /**
 396:      * Attempts to load an image from classpath. If this fails, an empty image icon is
 397:      * returned.
 398:      *
 399:      * @param resourceName the name of the image. The name should be a global resource
 400:      *                     name.
 401:      * @param scale        true, if the image should be scaled, false otherwise
 402:      * @param large        true, if the image should be scaled to 24x24, or false for 16x16
 403:      * @return the image icon.
 404:      */
 405:     private ImageIcon createIcon(final String resourceName, final boolean scale,
 406:                                  final boolean large) {
 407:         final URL in = ObjectUtilities.getResource(resourceName, ResourceBundleSupport.class);;
 408:         if (in == null) {
 409:             Log.warn("Unable to find file in the class path: " + resourceName);
 410:             return new ImageIcon(createTransparentImage(1, 1));
 411:         }
 412:         final Image img = Toolkit.getDefaultToolkit().createImage(in);
 413:         if (img == null) {
 414:             Log.warn("Unable to instantiate the image: " + resourceName);
 415:             return new ImageIcon(createTransparentImage(1, 1));
 416:         }
 417:         if (scale) {
 418:             if (large) {
 419:                 return new ImageIcon(img.getScaledInstance(24, 24, Image.SCALE_SMOOTH));
 420:             }
 421:             return new ImageIcon(img.getScaledInstance(16, 16, Image.SCALE_SMOOTH));
 422:         }
 423:         return new ImageIcon(img);
 424:     }
 425: 
 426:     /**
 427:      * Creates the Mnemonic from the given String. The String consists of the name of the VK
 428:      * constants of the class KeyEvent without VK_*.
 429:      *
 430:      * @param keyString the string
 431:      * @return the mnemonic as integer
 432:      */
 433:     private Integer createMnemonic(final String keyString) {
 434:         if (keyString == null) {
 435:             throw new NullPointerException("Key is null.");
 436:         }
 437:         if (keyString.length() == 0) {
 438:             throw new IllegalArgumentException("Key is empty.");
 439:         }
 440:         int character = keyString.charAt(0);
 441:         if (keyString.startsWith("VK_")) {
 442:             try {
 443:                 final Field f = KeyEvent.class.getField(keyString);
 444:                 final Integer keyCode = (Integer) f.get(null);
 445:                 character = keyCode.intValue();
 446:             }
 447:             catch (Exception nsfe) {
 448:                 // ignore the exception ...
 449:             }
 450:         }
 451:         return new Integer(character);
 452:     }
 453: 
 454:     /**
 455:      * Returns the plattforms default menu shortcut keymask.
 456:      *
 457:      * @return the default key mask.
 458:      */
 459:     private int getMenuKeyMask() {
 460:         try {
 461:             return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
 462:         }
 463:         catch (UnsupportedOperationException he) {
 464:             // headless exception extends UnsupportedOperation exception,
 465:             // but the HeadlessException is not defined in older JDKs...
 466:             return InputEvent.CTRL_MASK;
 467:         }
 468:     }
 469: 
 470:     /**
 471:      * Creates a transparent image.  These can be used for aligning menu items.
 472:      *
 473:      * @param width  the width.
 474:      * @param height the height.
 475:      * @return the created transparent image.
 476:      */
 477:     private BufferedImage createTransparentImage(final int width, final int height) {
 478:         final BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
 479:         final int[] data = img.getRGB(0, 0, width, height, null, 0, width);
 480:         Arrays.fill(data, 0x00000000);
 481:         img.setRGB(0, 0, width, height, data, 0, width);
 482:         return img;
 483:     }
 484: 
 485:     /**
 486:      * Creates a transparent icon. The Icon can be used for aligning menu items.
 487:      *
 488:      * @param width  the width of the new icon
 489:      * @param height the height of the new icon
 490:      * @return the created transparent icon.
 491:      */
 492:     public Icon createTransparentIcon(final int width, final int height) {
 493:         return new ImageIcon(createTransparentImage(width, height));
 494:     }
 495: 
 496:     /**
 497:      * Formats the message stored in the resource bundle (using a MessageFormat).
 498:      *
 499:      * @param key       the resourcebundle key
 500:      * @param parameter the parameter for the message
 501:      * @return the formated string
 502:      */
 503:     public String formatMessage(final String key, final Object parameter) {
 504:         return formatMessage(key, new Object[]{parameter});
 505:     }
 506: 
 507:     /**
 508:      * Formats the message stored in the resource bundle (using a MessageFormat).
 509:      *
 510:      * @param key  the resourcebundle key
 511:      * @param par1 the first parameter for the message
 512:      * @param par2 the second parameter for the message
 513:      * @return the formated string
 514:      */
 515:     public String formatMessage(final String key,
 516:                                 final Object par1,
 517:                                 final Object par2) {
 518:         return formatMessage(key, new Object[]{par1, par2});
 519:     }
 520: 
 521:     /**
 522:      * Formats the message stored in the resource bundle (using a MessageFormat).
 523:      *
 524:      * @param key        the resourcebundle key
 525:      * @param parameters the parameter collection for the message
 526:      * @return the formated string
 527:      */
 528:     public String formatMessage(final String key, final Object[] parameters) {
 529:         final MessageFormat format = new MessageFormat(getString(key));
 530:         format.setLocale(getLocale());
 531:         return format.format(parameters);
 532:     }
 533: 
 534:     /**
 535:      * Returns the current locale for this resource bundle.
 536:      * @return the locale.
 537:      */
 538:     public Locale getLocale() {
 539:         return locale;
 540:     }
 541: }