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}