    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.8 2005/11/09 22:35:13 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 occurence
    065     * 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         * The resource bundle that will be used for local lookups.
    080         */
    081        private ResourceBundle resources;
    082
    083        /**
    084         * A cache for string values, as looking up the cache is faster than looking up the
    085         * value in the bundle.
    086         */
    087        private TreeMap cache;
    088        /**
    089         * The current lookup path when performing non local lookups. This prevents infinite
    090         * loops during such lookups.
    091         */
    092        private TreeSet lookupPath;
    093
    094        /**
    095         * The name of the local resource bundle.
    096         */
    097        private String resourceBase;
    098
    099        /** The locale for this bundle. */
    100        private Locale locale;
    101
    102        /**
    103         * Creates a new instance.
    104         *
    105         * @param baseName the base name of the resource bundle, a fully qualified class name
    106         */
    107        public ResourceBundleSupport(final Locale locale, final String baseName) {
    108            this(locale, ResourceBundle.getBundle(baseName, locale), baseName);
    109        }
    110
    111        /**
    112         * Creates a new instance.
    113         *
    114         * @param locale         the locale for which this resource bundle is created.
    115         * @param resourceBundle the resourcebundle
    116         * @param baseName       the base name of the resource bundle, a fully qualified class name
    117         */
    118        protected ResourceBundleSupport(final Locale locale,
    119                                        final ResourceBundle resourceBundle,
    120                                        final String baseName) {
    121            if (locale == null) {
    122                throw new NullPointerException("Locale must not be null");
    123            }
    124            if (resourceBundle == null) {
    125                throw new NullPointerException("Resources must not be null");
    126            }
    127            if (baseName == null) {
    128                throw new NullPointerException("BaseName must not be null");
    129            }
    130            this.locale = locale;
    131            this.resources = resourceBundle;
    132            this.resourceBase = baseName;
    133            this.cache = new TreeMap();
    134            this.lookupPath = new TreeSet();
    135        }
    136
    137        /**
    138         * Creates a new instance.
    139         *
    140         * @param locale         the locale for which the resource bundle is created.
    141         * @param resourceBundle the resourcebundle
    142         */
    143        public ResourceBundleSupport(final Locale locale, final ResourceBundle resourceBundle) {
    144            this(locale, resourceBundle, resourceBundle.toString());
    145        }
    146
    147        /**
    148         * Creates a new instance.
    149         *
    150         * @param baseName the base name of the resource bundle, a fully qualified class name
    151         */
    152        public ResourceBundleSupport(final String baseName) {
    153            this(Locale.getDefault(), ResourceBundle.getBundle(baseName), baseName);
    154        }
    155
    156        /**
    157         * Creates a new instance.
    158         *
    159         * @param resourceBundle the resourcebundle
    160         * @param baseName       the base name of the resource bundle, a fully qualified class name
    161         */
    162        protected ResourceBundleSupport(final ResourceBundle resourceBundle,
    163                                        final String baseName) {
    164            this(Locale.getDefault(), resourceBundle, baseName);
    165        }
    166
    167        /**
    168         * Creates a new instance.
    169         *
    170         * @param resourceBundle the resourcebundle
    171         */
    172        public ResourceBundleSupport(final ResourceBundle resourceBundle) {
    173            this(Locale.getDefault(), resourceBundle, resourceBundle.toString());
    174        }
    175
    176        /**
    177         * The base name of the resource bundle.
    178         *
    179         * @return the resource bundle's name.
    180         */
    181        protected final String getResourceBase() {
    182            return this.resourceBase;
    183        }
    184
    185        /**
    186         * Gets a string for the given key from this resource bundle or one of its parents. If
    187         * the key is a link, the link is resolved and the referenced string is returned
    188         * instead.
    189         *
    190         * @param key the key for the desired string
    191         * @return the string for the given key
    192         * @throws NullPointerException     if <code>key</code> is <code>null</code>
    193         * @throws MissingResourceException if no object for the given key can be found
    194         * @throws ClassCastException       if the object found for the given key is not a
    195         *                                  string
    196         */
    197        public synchronized String getString(final String key) {
    198            final String retval = (String) this.cache.get(key);
    199            if (retval != null) {
    200                return retval;
    201            }
    202            this.lookupPath.clear();
    203            return internalGetString(key);
    204        }
    205
    206        /**
    207         * Performs the lookup for the given key. If the key points to a link the link is
    208         * resolved and that key is looked up instead.
    209         *
    210         * @param key the key for the string
    211         * @return the string for the given key
    212         */
    213        protected String internalGetString(final String key) {
    214            if (this.lookupPath.contains(key)) {
    215                throw new MissingResourceException
    216                    ("InfiniteLoop in resource lookup",
    217                        getResourceBase(), this.lookupPath.toString());
    218            }
    219            final String fromResBundle = this.resources.getString(key);
    220            if (fromResBundle.startsWith("@@")) {
    221                // global forward ...
    222                final int idx = fromResBundle.indexOf('@', 2);
    223                if (idx == -1) {
    224                    throw new MissingResourceException
    225                        ("Invalid format for global lookup key.", getResourceBase(), key);
    226                }
    227                try {
    228                    final ResourceBundle res = ResourceBundle.getBundle
    229                        (fromResBundle.substring(2, idx));
    230                    return res.getString(fromResBundle.substring(idx + 1));
    231                }
    232                catch (Exception e) {
    233                    Log.error("Error during global lookup", e);
    234                    throw new MissingResourceException
    235                        ("Error during global lookup", getResourceBase(), key);
    236                }
    237            }
    238            else if (fromResBundle.startsWith("@")) {
    239                // local forward ...
    240                final String newKey = fromResBundle.substring(1);
    241                this.lookupPath.add(key);
    242                final String retval = internalGetString(newKey);
    243
    244                this.cache.put(key, retval);
    245                return retval;
    246            }
    247            else {
    248                this.cache.put(key, fromResBundle);
    249                return fromResBundle;
    250            }
    251        }
    252
    253        /**
    254         * Returns an scaled icon suitable for buttons or menus.
    255         *
    256         * @param key   the name of the resource bundle key
    257         * @param large true, if the image should be scaled to 24x24, or false for 16x16
    258         * @return the icon.
    259         */
    260        public Icon getIcon(final String key, final boolean large) {
    261            final String name = getString(key);
    262            return createIcon(name, true, large);
    263        }
    264
    265        /**
    266         * Returns an unscaled icon.
    267         *
    268         * @param key the name of the resource bundle key
    269         * @return the icon.
    270         */
    271        public Icon getIcon(final String key) {
    272            final String name = getString(key);
    273            return createIcon(name, false, false);
    274        }
    275
    276        /**
    277         * Returns the mnemonic stored at the given resourcebundle key. The mnemonic should be
    278         * either the symbolic name of one of the KeyEvent.VK_* constants (without the 'VK_') or
    279         * the character for that key.
    280         * <p/>
    281         * For the enter key, the resource bundle would therefore either contain "ENTER" or
    282         * "\n".
    283         * <pre>
    284         * a.resourcebundle.key=ENTER
    285         * an.other.resourcebundle.key=\n
    286         * </pre>
    287         *
    288         * @param key the resourcebundle key
    289         * @return the mnemonic
    290         */
    291        public Integer getMnemonic(final String key) {
    292            final String name = getString(key);
    293            return createMnemonic(name);
    294        }
    295
    296        /**
    297         * Returns the keystroke stored at the given resourcebundle key.
    298         * <p/>
    299         * The keystroke will be composed of a simple key press and the plattform's
    300         * MenuKeyMask.
    301         * <p/>
    302         * The keystrokes character key should be either the symbolic name of one of the
    303         * KeyEvent.VK_* constants or the character for that key.
    304         * <p/>
    305         * For the 'A' key, the resource bundle would therefore either contain "VK_A" or
    306         * "a".
    307         * <pre>
    308         * a.resourcebundle.key=VK_A
    309         * an.other.resourcebundle.key=a
    310         * </pre>
    311         *
    312         * @param key the resourcebundle key
    313         * @return the mnemonic
    314         * @see Toolkit#getMenuShortcutKeyMask()
    315         */
    316        public KeyStroke getKeyStroke(final String key) {
    317            return getKeyStroke(key, getMenuKeyMask());
    318        }
    319
    320        /**
    321         * Returns the keystroke stored at the given resourcebundle key.
    322         * <p/>
    323         * The keystroke will be composed of a simple key press and the given
    324         * KeyMask. If the KeyMask is zero, a plain Keystroke is returned.
    325         * <p/>
    326         * The keystrokes character key should be either the symbolic name of one of the
    327         * KeyEvent.VK_* constants or the character for that key.
    328         * <p/>
    329         * For the 'A' key, the resource bundle would therefore either contain "VK_A" or
    330         * "a".
    331         * <pre>
    332         * a.resourcebundle.key=VK_A
    333         * an.other.resourcebundle.key=a
    334         * </pre>
    335         *
    336         * @param key the resourcebundle key
    337         * @return the mnemonic
    338         * @see Toolkit#getMenuShortcutKeyMask()
    339         */
    340        public KeyStroke getKeyStroke(final String key, final int mask) {
    341            final String name = getString(key);
    342            return KeyStroke.getKeyStroke(createMnemonic(name).intValue(), mask);
    343
    344        }
    345
    346        /**
    347         * Returns a JMenu created from a resource bundle definition.
    348         * <p/>
    349         * The menu definition consists of two keys, the name of the menu and the mnemonic for
    350         * that menu. Both keys share a common prefix, which is extended by ".name" for the name
    351         * of the menu and ".mnemonic" for the mnemonic.
    352         * <p/>
    353         * <pre>
    354         * # define the file menu
    355         * menu.file.name=File
    356         * menu.file.mnemonic=F
    357         * </pre>
    358         * The menu definition above can be used to create the menu by calling <code>createMenu
    359         * ("menu.file")</code>.
    360         *
    361         * @param keyPrefix the common prefix for that menu
    362         * @return the created menu
    363         */
    364        public JMenu createMenu(final String keyPrefix) {
    365            final JMenu retval = new JMenu();
    366            retval.setText(getString(keyPrefix + ".name"));
    367            retval.setMnemonic(getMnemonic(keyPrefix + ".mnemonic").intValue());
    368            return retval;
    369        }
    370
    371        /**
    372         * Returns a URL pointing to a resource located in the classpath. The resource is looked
    373         * up using the given key.
    374         * <p/>
    375         * Example: The load a file named 'logo.gif' which is stored in a java package named
    376         * 'org.jfree.resources':
    377         * <pre>
    378         * mainmenu.logo=org/jfree/resources/logo.gif
    379         * </pre>
    380         * The URL for that file can be queried with: <code>getResource("mainmenu.logo");</code>.
    381         *
    382         * @param key the key for the resource
    383         * @return the resource URL
    384         */
    385        public URL getResourceURL(final String key) {
    386            final String name = getString(key);
    387            final URL in = ObjectUtilities.getResource(name, ResourceBundleSupport.class);
    388            if (in == null) {
    389                Log.warn("Unable to find file in the class path: " + name + "; key=" + key);
    390            }
    391            return in;
    392        }
    393
    394
    395        /**
    396         * Attempts to load an image from classpath. If this fails, an empty image icon is
    397         * returned.
    398         *
    399         * @param resourceName the name of the image. The name should be a global resource
    400         *                     name.
    401         * @param scale        true, if the image should be scaled, false otherwise
    402         * @param large        true, if the image should be scaled to 24x24, or false for 16x16
    403         * @return the image icon.
    404         */
    405        private ImageIcon createIcon(final String resourceName, final boolean scale,
    406                                     final boolean large) {
    407            final URL in = ObjectUtilities.getResource(resourceName, ResourceBundleSupport.class);;
    408            if (in == null) {
    409                Log.warn("Unable to find file in the class path: " + resourceName);
    410                return new ImageIcon(createTransparentImage(1, 1));
    411            }
    412            final Image img = Toolkit.getDefaultToolkit().createImage(in);
    413            if (img == null) {
    414                Log.warn("Unable to instantiate the image: " + resourceName);
    415                return new ImageIcon(createTransparentImage(1, 1));
    416            }
    417            if (scale) {
    418                if (large) {
    419                    return new ImageIcon(img.getScaledInstance(24, 24, Image.SCALE_SMOOTH));
    420                }
    421                return new ImageIcon(img.getScaledInstance(16, 16, Image.SCALE_SMOOTH));
    422            }
    423            return new ImageIcon(img);
    424        }
    425
    426        /**
    427         * Creates the Mnemonic from the given String. The String consists of the name of the VK
    428         * constants of the class KeyEvent without VK_*.
    429         *
    430         * @param keyString the string
    431         * @return the mnemonic as integer
    432         */
    433        private Integer createMnemonic(final String keyString) {
    434            if (keyString == null) {
    435                throw new NullPointerException("Key is null.");
    436            }
    437            if (keyString.length() == 0) {
    438                throw new IllegalArgumentException("Key is empty.");
    439            }
    440            int character = keyString.charAt(0);
    441            if (keyString.startsWith("VK_")) {
    442                try {
    443                    final Field f = KeyEvent.class.getField(keyString);
    444                    final Integer keyCode = (Integer) f.get(null);
    445                    character = keyCode.intValue();
    446                }
    447                catch (Exception nsfe) {
    448                    // ignore the exception ...
    449                }
    450            }
    451            return new Integer(character);
    452        }
    453
    454        /**
    455         * Returns the plattforms default menu shortcut keymask.
    456         *
    457         * @return the default key mask.
    458         */
    459        private int getMenuKeyMask() {
    460            try {
    461                return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
    462            }
    463            catch (UnsupportedOperationException he) {
    464                // headless exception extends UnsupportedOperation exception,
    465                // but the HeadlessException is not defined in older JDKs...
    466                return InputEvent.CTRL_MASK;
    467            }
    468        }
    469
    470        /**
    471         * Creates a transparent image.  These can be used for aligning menu items.
    472         *
    473         * @param width  the width.
    474         * @param height the height.
    475         * @return the created transparent image.
    476         */
    477        private BufferedImage createTransparentImage(final int width, final int height) {
    478            final BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    479            final int[] data = img.getRGB(0, 0, width, height, null, 0, width);
    480            Arrays.fill(data, 0x00000000);
    481            img.setRGB(0, 0, width, height, data, 0, width);
    482            return img;
    483        }
    484
    485        /**
    486         * Creates a transparent icon. The Icon can be used for aligning menu items.
    487         *
    488         * @param width  the width of the new icon
    489         * @param height the height of the new icon
    490         * @return the created transparent icon.
    491         */
    492        public Icon createTransparentIcon(final int width, final int height) {
    493            return new ImageIcon(createTransparentImage(width, height));
    494        }
    495
    496        /**
    497         * Formats the message stored in the resource bundle (using a MessageFormat).
    498         *
    499         * @param key       the resourcebundle key
    500         * @param parameter the parameter for the message
    501         * @return the formated string
    502         */
    503        public String formatMessage(final String key, final Object parameter) {
    504            return formatMessage(getString(key), new Object[]{parameter});
    505        }
    506
    507        /**
    508         * Formats the message stored in the resource bundle (using a MessageFormat).
    509         *
    510         * @param key  the resourcebundle key
    511         * @param par1 the first parameter for the message
    512         * @param par2 the second parameter for the message
    513         * @return the formated string
    514         */
    515        public String formatMessage(final String key,
    516                                    final Object par1,
    517                                    final Object par2) {
    518            return formatMessage(getString(key), new Object[]{par1, par2});
    519        }
    520
    521        /**
    522         * Formats the message stored in the resource bundle (using a MessageFormat).
    523         *
    524         * @param key        the resourcebundle key
    525         * @param parameters the parameter collection for the message
    526         * @return the formated string
    527         */
    528        public String formatMessage(final String key, final Object[] parameters) {
    529            final MessageFormat format = new MessageFormat(getString(key));
    530            format.setLocale(getLocale());
    531            return format.format(parameters);
    532        }
    533
    534        /**
    535         * Returns the current locale for this resource bundle.
    536         * @return the locale.
    537         */
    538        public Locale getLocale() {
    539            return locale;
    540        }
    541    }