    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    }