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