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    }