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    }