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-2008, 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-2008, by Thomas Morgner and Contributors.
  31:  *
  32:  * Original Author:  Thomas Morgner;
  33:  * Contributor(s):   David Gilbert (for Object Refinery Limited);
  34:  *
  35:  * $Id: ResourceBundleSupport.java,v 1.12 2008/12/18 09:57:32 mungady Exp $
  36:  *
  37:  * Changes
  38:  * -------
  39:  * 18-Dec-2008 : Use ResourceBundleWrapper - see JFreeChart patch 1607918 by
  40:  *               Jess Thrysoee (DG);
  41:  *
  42:  */
  43: 
  44: package org.jfree.util;
  45: 
  46: import java.awt.Image;
  47: import java.awt.Toolkit;
  48: import java.awt.event.InputEvent;
  49: import java.awt.event.KeyEvent;
  50: import java.awt.image.BufferedImage;
  51: import java.lang.reflect.Field;
  52: import java.net.URL;
  53: import java.text.MessageFormat;
  54: import java.util.Arrays;
  55: import java.util.Locale;
  56: import java.util.MissingResourceException;
  57: import java.util.ResourceBundle;
  58: import java.util.TreeMap;
  59: import java.util.TreeSet;
  60: 
  61: import javax.swing.Icon;
  62: import javax.swing.ImageIcon;
  63: import javax.swing.JMenu;
  64: import javax.swing.KeyStroke;
  65: 
  66: /**
  67:  * An utility class to ease up using property-file resource bundles.
  68:  * <p/>
  69:  * The class support references within the resource bundle set to minimize the
  70:  * occurence of duplicate keys. References are given in the format:
  71:  * <pre>
  72:  * a.key.name=@referenced.key
  73:  * </pre>
  74:  * <p/>
  75:  * A lookup to a key in an other resource bundle should be written by
  76:  * <pre>
  77:  * a.key.name=@@resourcebundle_name@referenced.key
  78:  * </pre>
  79:  *
  80:  * @author Thomas Morgner
  81:  */
  82: public class ResourceBundleSupport
  83: {
  84:   /**
  85:    * The resource bundle that will be used for local lookups.
  86:    */
  87:   private ResourceBundle resources;
  88: 
  89:   /**
  90:    * A cache for string values, as looking up the cache is faster than looking
  91:    * up the value in the bundle.
  92:    */
  93:   private TreeMap cache;
  94:   /**
  95:    * The current lookup path when performing non local lookups. This prevents
  96:    * infinite loops during such lookups.
  97:    */
  98:   private TreeSet lookupPath;
  99: 
 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: }