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