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
044package org.jfree.util;
045
046import java.awt.Image;
047import java.awt.Toolkit;
048import java.awt.event.InputEvent;
049import java.awt.event.KeyEvent;
050import java.awt.image.BufferedImage;
051import java.lang.reflect.Field;
052import java.net.URL;
053import java.text.MessageFormat;
054import java.util.Arrays;
055import java.util.Locale;
056import java.util.MissingResourceException;
057import java.util.ResourceBundle;
058import java.util.TreeMap;
059import java.util.TreeSet;
060
061import javax.swing.Icon;
062import javax.swing.ImageIcon;
063import javax.swing.JMenu;
064import 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 */
082public 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}