    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
    044package org.jfree.util;
    045
    046import java.awt.Image;
    047import java.awt.Toolkit;
    048import java.awt.event.InputEvent;
    049import java.awt.event.KeyEvent;
    050import java.awt.image.BufferedImage;
    051import java.lang.reflect.Field;
    052import java.net.URL;
    053import java.text.MessageFormat;
    054import java.util.Arrays;
    055import java.util.Locale;
    056import java.util.MissingResourceException;
    057import java.util.ResourceBundle;
    058import java.util.TreeMap;
    059import java.util.TreeSet;
    060
    061import javax.swing.Icon;
    062import javax.swing.ImageIcon;
    063import javax.swing.JMenu;
    064import 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 */
    082public 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}