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