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 }