    001    /* ========================================================================
    002     * JCommon : a free general purpose class library for the Java(tm) platform
    003     * ========================================================================
    004     *
    005     * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
    006     *
    007     * Project Info:  http://www.jfree.org/jcommon/index.html
    008     *
    009     * This library is free software; you can redistribute it and/or modify it
    010     * under the terms of the GNU Lesser General Public License as published by
    011     * the Free Software Foundation; either version 2.1 of the License, or
    012     * (at your option) any later version.
    013     *
    014     * This library is distributed in the hope that it will be useful, but
    015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017     * License for more details.
    018     *
    019     * You should have received a copy of the GNU Lesser General Public
    020     * License along with this library; if not, write to the Free Software
    021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022     * USA.
    023     *
    024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * ---------------------
    028     * ReadOnlyIterator.java
    029     * ---------------------
    030     * (C)opyright 2003, 2004, by Thomas Morgner and Contributors.
    031     *
    032     * Original Author:  Thomas Morgner;
    033     * Contributor(s):   -;
    034     *
    035     * $Id: ResourceBundleSupport.java,v 1.10 2006/12/03 15:33:33 taqua Exp $
    036     *
    037     * Changes
    038     * -------------------------
    039     */
    040    package org.jfree.util;
    041
    042    import java.awt.Image;
    043    import java.awt.Toolkit;
    044    import java.awt.event.InputEvent;
    045    import java.awt.event.KeyEvent;
    046    import java.awt.image.BufferedImage;
    047    import java.lang.reflect.Field;
    048    import java.net.URL;
    049    import java.text.MessageFormat;
    050    import java.util.Arrays;
    051    import java.util.Locale;
    052    import java.util.MissingResourceException;
    053    import java.util.ResourceBundle;
    054    import java.util.TreeMap;
    055    import java.util.TreeSet;
    056    import javax.swing.Icon;
    057    import javax.swing.ImageIcon;
    058    import javax.swing.JMenu;
    059    import javax.swing.KeyStroke;
    060
    061    /**
    062     * An utility class to ease up using property-file resource bundles.
    063     * <p/>
    064     * The class support references within the resource bundle set to minimize the
    065     * occurence of duplicate keys. References are given in the format:
    066     * <pre>
    067     * a.key.name=@referenced.key
    068     * </pre>
    069     * <p/>
    070     * A lookup to a key in an other resource bundle should be written by
    071     * <pre>
    072     * a.key.name=@@resourcebundle_name@referenced.key
    073     * </pre>
    074     *
    075     * @author Thomas Morgner
    076     */
    077    public class ResourceBundleSupport
    078    {
    079      /**
    080       * The resource bundle that will be used for local lookups.
    081       */
    082      private ResourceBundle resources;
    083
    084      /**
    085       * A cache for string values, as looking up the cache is faster than looking
    086       * up the value in the bundle.
    087       */
    088      private TreeMap cache;
    089      /**
    090       * The current lookup path when performing non local lookups. This prevents
    091       * infinite loops during such lookups.
    092       */
    093      private TreeSet lookupPath;
    094
    095      /**
    096       * The name of the local resource bundle.
    097       */
    098      private String resourceBase;
    099
    100      /**
    101       * The locale for this bundle.
    102       */
    103      private Locale locale;
    104
    105      /**
    106       * Creates a new instance.
    107       *
    108       * @param baseName the base name of the resource bundle, a fully qualified
    109       *                 class name
    110       */
    111      public ResourceBundleSupport(final Locale locale, final String baseName)
    112      {
    113        this(locale, ResourceBundle.getBundle(baseName, locale), baseName);
    114      }
    115
    116      /**
    117       * Creates a new instance.
    118       *
    119       * @param locale         the locale for which this resource bundle is
    120       *                       created.
    121       * @param resourceBundle the resourcebundle
    122       * @param baseName       the base name of the resource bundle, a fully
    123       *                       qualified class name
    124       */
    125      protected ResourceBundleSupport(final Locale locale,
    126                                      final ResourceBundle resourceBundle,
    127                                      final String baseName)
    128      {
    129        if (locale == null)
    130        {
    131          throw new NullPointerException("Locale must not be null");
    132        }
    133        if (resourceBundle == null)
    134        {
    135          throw new NullPointerException("Resources must not be null");
    136        }
    137        if (baseName == null)
    138        {
    139          throw new NullPointerException("BaseName must not be null");
    140        }
    141        this.locale = locale;
    142        this.resources = resourceBundle;
    143        this.resourceBase = baseName;
    144        this.cache = new TreeMap();
    145        this.lookupPath = new TreeSet();
    146      }
    147
    148      /**
    149       * Creates a new instance.
    150       *
    151       * @param locale         the locale for which the resource bundle is
    152       *                       created.
    153       * @param resourceBundle the resourcebundle
    154       */
    155      public ResourceBundleSupport(final Locale locale,
    156                                   final ResourceBundle resourceBundle)
    157      {
    158        this(locale, resourceBundle, resourceBundle.toString());
    159      }
    160
    161      /**
    162       * Creates a new instance.
    163       *
    164       * @param baseName the base name of the resource bundle, a fully qualified
    165       *                 class name
    166       */
    167      public ResourceBundleSupport(final String baseName)
    168      {
    169        this(Locale.getDefault(), ResourceBundle.getBundle(baseName), baseName);
    170      }
    171
    172      /**
    173       * Creates a new instance.
    174       *
    175       * @param resourceBundle the resourcebundle
    176       * @param baseName       the base name of the resource bundle, a fully
    177       *                       qualified class name
    178       */
    179      protected ResourceBundleSupport(final ResourceBundle resourceBundle,
    180                                      final String baseName)
    181      {
    182        this(Locale.getDefault(), resourceBundle, baseName);
    183      }
    184
    185      /**
    186       * Creates a new instance.
    187       *
    188       * @param resourceBundle the resourcebundle
    189       */
    190      public ResourceBundleSupport(final ResourceBundle resourceBundle)
    191      {
    192        this(Locale.getDefault(), resourceBundle, resourceBundle.toString());
    193      }
    194
    195      /**
    196       * The base name of the resource bundle.
    197       *
    198       * @return the resource bundle's name.
    199       */
    200      protected final String getResourceBase()
    201      {
    202        return this.resourceBase;
    203      }
    204
    205      /**
    206       * Gets a string for the given key from this resource bundle or one of its
    207       * parents. If the key is a link, the link is resolved and the referenced
    208       * string is returned instead.
    209       *
    210       * @param key the key for the desired string
    211       * @return the string for the given key
    212       * @throws NullPointerException     if <code>key</code> is <code>null</code>
    213       * @throws MissingResourceException if no object for the given key can be
    214       *                                  found
    215       * @throws ClassCastException       if the object found for the given key is
    216       *                                  not a string
    217       */
    218      public synchronized String getString(final String key)
    219      {
    220        final String retval = (String) this.cache.get(key);
    221        if (retval != null)
    222        {
    223          return retval;
    224        }
    225        this.lookupPath.clear();
    226        return internalGetString(key);
    227      }
    228
    229      /**
    230       * Performs the lookup for the given key. If the key points to a link the
    231       * link is resolved and that key is looked up instead.
    232       *
    233       * @param key the key for the string
    234       * @return the string for the given key
    235       */
    236      protected String internalGetString(final String key)
    237      {
    238        if (this.lookupPath.contains(key))
    239        {
    240          throw new MissingResourceException
    241              ("InfiniteLoop in resource lookup",
    242                  getResourceBase(), this.lookupPath.toString());
    243        }
    244        final String fromResBundle = this.resources.getString(key);
    245        if (fromResBundle.startsWith("@@"))
    246        {
    247          // global forward ...
    248          final int idx = fromResBundle.indexOf('@', 2);
    249          if (idx == -1)
    250          {
    251            throw new MissingResourceException
    252                ("Invalid format for global lookup key.", getResourceBase(), key);
    253          }
    254          try
    255          {
    256            final ResourceBundle res = ResourceBundle.getBundle
    257                (fromResBundle.substring(2, idx));
    258            return res.getString(fromResBundle.substring(idx + 1));
    259          }
    260          catch (Exception e)
    261          {
    262            Log.error("Error during global lookup", e);
    263            throw new MissingResourceException
    264                ("Error during global lookup", getResourceBase(), key);
    265          }
    266        }
    267        else if (fromResBundle.startsWith("@"))
    268        {
    269          // local forward ...
    270          final String newKey = fromResBundle.substring(1);
    271          this.lookupPath.add(key);
    272          final String retval = internalGetString(newKey);
    273
    274          this.cache.put(key, retval);
    275          return retval;
    276        }
    277        else
    278        {
    279          this.cache.put(key, fromResBundle);
    280          return fromResBundle;
    281        }
    282      }
    283
    284      /**
    285       * Returns an scaled icon suitable for buttons or menus.
    286       *
    287       * @param key   the name of the resource bundle key
    288       * @param large true, if the image should be scaled to 24x24, or false for
    289       *              16x16
    290       * @return the icon.
    291       */
    292      public Icon getIcon(final String key, final boolean large)
    293      {
    294        final String name = getString(key);
    295        return createIcon(name, true, large);
    296      }
    297
    298      /**
    299       * Returns an unscaled icon.
    300       *
    301       * @param key the name of the resource bundle key
    302       * @return the icon.
    303       */
    304      public Icon getIcon(final String key)
    305      {
    306        final String name = getString(key);
    307        return createIcon(name, false, false);
    308      }
    309
    310      /**
    311       * Returns the mnemonic stored at the given resourcebundle key. The mnemonic
    312       * should be either the symbolic name of one of the KeyEvent.VK_* constants
    313       * (without the 'VK_') or the character for that key.
    314       * <p/>
    315       * For the enter key, the resource bundle would therefore either contain
    316       * "ENTER" or "\n".
    317       * <pre>
    318       * a.resourcebundle.key=ENTER
    319       * an.other.resourcebundle.key=\n
    320       * </pre>
    321       *
    322       * @param key the resourcebundle key
    323       * @return the mnemonic
    324       */
    325      public Integer getMnemonic(final String key)
    326      {
    327        final String name = getString(key);
    328        return createMnemonic(name);
    329      }
    330
    331
    332      public Integer getOptionalMnemonic(final String key)
    333      {
    334        final String name = getString(key);
    335        if (name != null && name.length() > 0)
    336        {
    337          return createMnemonic(name);
    338        }
    339        return null;
    340      }
    341
    342      /**
    343       * Returns the keystroke stored at the given resourcebundle key.
    344       * <p/>
    345       * The keystroke will be composed of a simple key press and the plattform's
    346       * MenuKeyMask.
    347       * <p/>
    348       * The keystrokes character key should be either the symbolic name of one of
    349       * the KeyEvent.VK_* constants or the character for that key.
    350       * <p/>
    351       * For the 'A' key, the resource bundle would therefore either contain
    352       * "VK_A" or "a".
    353       * <pre>
    354       * a.resourcebundle.key=VK_A
    355       * an.other.resourcebundle.key=a
    356       * </pre>
    357       *
    358       * @param key the resourcebundle key
    359       * @return the mnemonic
    360       * @see Toolkit#getMenuShortcutKeyMask()
    361       */
    362      public KeyStroke getKeyStroke(final String key)
    363      {
    364        return getKeyStroke(key, getMenuKeyMask());
    365      }
    366
    367      public KeyStroke getOptionalKeyStroke(final String key)
    368      {
    369        return getOptionalKeyStroke(key, getMenuKeyMask());
    370      }
    371
    372      /**
    373       * Returns the keystroke stored at the given resourcebundle key.
    374       * <p/>
    375       * The keystroke will be composed of a simple key press and the given
    376       * KeyMask. If the KeyMask is zero, a plain Keystroke is returned.
    377       * <p/>
    378       * The keystrokes character key should be either the symbolic name of one of
    379       * the KeyEvent.VK_* constants or the character for that key.
    380       * <p/>
    381       * For the 'A' key, the resource bundle would therefore either contain
    382       * "VK_A" or "a".
    383       * <pre>
    384       * a.resourcebundle.key=VK_A
    385       * an.other.resourcebundle.key=a
    386       * </pre>
    387       *
    388       * @param key the resourcebundle key
    389       * @return the mnemonic
    390       * @see Toolkit#getMenuShortcutKeyMask()
    391       */
    392      public KeyStroke getKeyStroke(final String key, final int mask)
    393      {
    394        final String name = getString(key);
    395        return KeyStroke.getKeyStroke(createMnemonic(name).intValue(), mask);
    396      }
    397
    398      public KeyStroke getOptionalKeyStroke(final String key, final int mask)
    399      {
    400        final String name = getString(key);
    401
    402        if (name != null && name.length() > 0)
    403        {
    404          return KeyStroke.getKeyStroke(createMnemonic(name).intValue(), mask);
    405        }
    406        return null;
    407      }
    408
    409      /**
    410       * Returns a JMenu created from a resource bundle definition.
    411       * <p/>
    412       * The menu definition consists of two keys, the name of the menu and the
    413       * mnemonic for that menu. Both keys share a common prefix, which is
    414       * extended by ".name" for the name of the menu and ".mnemonic" for the
    415       * mnemonic.
    416       * <p/>
    417       * <pre>
    418       * # define the file menu
    419       * menu.file.name=File
    420       * menu.file.mnemonic=F
    421       * </pre>
    422       * The menu definition above can be used to create the menu by calling
    423       * <code>createMenu ("menu.file")</code>.
    424       *
    425       * @param keyPrefix the common prefix for that menu
    426       * @return the created menu
    427       */
    428      public JMenu createMenu(final String keyPrefix)
    429      {
    430        final JMenu retval = new JMenu();
    431        retval.setText(getString(keyPrefix + ".name"));
    432        retval.setMnemonic(getMnemonic(keyPrefix + ".mnemonic").intValue());
    433        return retval;
    434      }
    435
    436      /**
    437       * Returns a URL pointing to a resource located in the classpath. The
    438       * resource is looked up using the given key.
    439       * <p/>
    440       * Example: The load a file named 'logo.gif' which is stored in a java
    441       * package named 'org.jfree.resources':
    442       * <pre>
    443       * mainmenu.logo=org/jfree/resources/logo.gif
    444       * </pre>
    445       * The URL for that file can be queried with: <code>getResource("mainmenu.logo");</code>.
    446       *
    447       * @param key the key for the resource
    448       * @return the resource URL
    449       */
    450      public URL getResourceURL(final String key)
    451      {
    452        final String name = getString(key);
    453        final URL in = ObjectUtilities.getResource(name, ResourceBundleSupport.class);
    454        if (in == null)
    455        {
    456          Log.warn("Unable to find file in the class path: " + name + "; key=" + key);
    457        }
    458        return in;
    459      }
    460
    461
    462      /**
    463       * Attempts to load an image from classpath. If this fails, an empty image
    464       * icon is returned.
    465       *
    466       * @param resourceName the name of the image. The name should be a global
    467       *                     resource name.
    468       * @param scale        true, if the image should be scaled, false otherwise
    469       * @param large        true, if the image should be scaled to 24x24, or
    470       *                     false for 16x16
    471       * @return the image icon.
    472       */
    473      private ImageIcon createIcon(final String resourceName, final boolean scale,
    474                                   final boolean large)
    475      {
    476        final URL in = ObjectUtilities.getResource(resourceName, ResourceBundleSupport.class);
    477        ;
    478        if (in == null)
    479        {
    480          Log.warn("Unable to find file in the class path: " + resourceName);
    481          return new ImageIcon(createTransparentImage(1, 1));
    482        }
    483        final Image img = Toolkit.getDefaultToolkit().createImage(in);
    484        if (img == null)
    485        {
    486          Log.warn("Unable to instantiate the image: " + resourceName);
    487          return new ImageIcon(createTransparentImage(1, 1));
    488        }
    489        if (scale)
    490        {
    491          if (large)
    492          {
    493            return new ImageIcon(img.getScaledInstance(24, 24, Image.SCALE_SMOOTH));
    494          }
    495          return new ImageIcon(img.getScaledInstance(16, 16, Image.SCALE_SMOOTH));
    496        }
    497        return new ImageIcon(img);
    498      }
    499
    500      /**
    501       * Creates the Mnemonic from the given String. The String consists of the
    502       * name of the VK constants of the class KeyEvent without VK_*.
    503       *
    504       * @param keyString the string
    505       * @return the mnemonic as integer
    506       */
    507      private Integer createMnemonic(final String keyString)
    508      {
    509        if (keyString == null)
    510        {
    511          throw new NullPointerException("Key is null.");
    512        }
    513        if (keyString.length() == 0)
    514        {
    515          throw new IllegalArgumentException("Key is empty.");
    516        }
    517        int character = keyString.charAt(0);
    518        if (keyString.startsWith("VK_"))
    519        {
    520          try
    521          {
    522            final Field f = KeyEvent.class.getField(keyString);
    523            final Integer keyCode = (Integer) f.get(null);
    524            character = keyCode.intValue();
    525          }
    526          catch (Exception nsfe)
    527          {
    528            // ignore the exception ...
    529          }
    530        }
    531        return new Integer(character);
    532      }
    533
    534      /**
    535       * Returns the plattforms default menu shortcut keymask.
    536       *
    537       * @return the default key mask.
    538       */
    539      private int getMenuKeyMask()
    540      {
    541        try
    542        {
    543          return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
    544        }
    545        catch (UnsupportedOperationException he)
    546        {
    547          // headless exception extends UnsupportedOperation exception,
    548          // but the HeadlessException is not defined in older JDKs...
    549          return InputEvent.CTRL_MASK;
    550        }
    551      }
    552
    553      /**
    554       * Creates a transparent image.  These can be used for aligning menu items.
    555       *
    556       * @param width  the width.
    557       * @param height the height.
    558       * @return the created transparent image.
    559       */
    560      private BufferedImage createTransparentImage(final int width,
    561                                                   final int height)
    562      {
    563        final BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    564        final int[] data = img.getRGB(0, 0, width, height, null, 0, width);
    565        Arrays.fill(data, 0x00000000);
    566        img.setRGB(0, 0, width, height, data, 0, width);
    567        return img;
    568      }
    569
    570      /**
    571       * Creates a transparent icon. The Icon can be used for aligning menu
    572       * items.
    573       *
    574       * @param width  the width of the new icon
    575       * @param height the height of the new icon
    576       * @return the created transparent icon.
    577       */
    578      public Icon createTransparentIcon(final int width, final int height)
    579      {
    580        return new ImageIcon(createTransparentImage(width, height));
    581      }
    582
    583      /**
    584       * Formats the message stored in the resource bundle (using a
    585       * MessageFormat).
    586       *
    587       * @param key       the resourcebundle key
    588       * @param parameter the parameter for the message
    589       * @return the formated string
    590       */
    591      public String formatMessage(final String key, final Object parameter)
    592      {
    593        return formatMessage(key, new Object[]{parameter});
    594      }
    595
    596      /**
    597       * Formats the message stored in the resource bundle (using a
    598       * MessageFormat).
    599       *
    600       * @param key  the resourcebundle key
    601       * @param par1 the first parameter for the message
    602       * @param par2 the second parameter for the message
    603       * @return the formated string
    604       */
    605      public String formatMessage(final String key,
    606                                  final Object par1,
    607                                  final Object par2)
    608      {
    609        return formatMessage(key, new Object[]{par1, par2});
    610      }
    611
    612      /**
    613       * Formats the message stored in the resource bundle (using a
    614       * MessageFormat).
    615       *
    616       * @param key        the resourcebundle key
    617       * @param parameters the parameter collection for the message
    618       * @return the formated string
    619       */
    620      public String formatMessage(final String key, final Object[] parameters)
    621      {
    622        final MessageFormat format = new MessageFormat(getString(key));
    623        format.setLocale(getLocale());
    624        return format.format(parameters);
    625      }
    626
    627      /**
    628       * Returns the current locale for this resource bundle.
    629       *
    630       * @return the locale.
    631       */
    632      public Locale getLocale()
    633      {
    634        return locale;
    635      }
    636    }