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