/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.libraries.base.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import javax.swing.*; import java.awt.*; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.image.BufferedImage; import java.lang.reflect.Field; import java.net.URL; import java.text.MessageFormat; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.StringTokenizer; /** * An utility class to ease up using property-file resource bundles. * <p/> * The class support references within the resource bundle set to minimize the occurence of duplicate keys. References * are given in the format: * <pre> * a.key.name=@referenced.key * </pre> * <p/> * A lookup to a key in an other resource bundle should be written by * <pre> * a.key.name=@@resourcebundle_name@referenced.key * </pre> * * @author Thomas Morgner * @noinspection HardCodedStringLiteral */ public class ResourceBundleSupport { /** * A logger for debug-messages. */ private static final Log logger = LogFactory.getLog( ResourceBundleSupport.class ); /** * The resource bundle that will be used for local lookups. */ private ResourceBundle resources; /** * A cache for string values, as looking up the cache is faster than looking up the value in the bundle. */ private HashMap<String, String> cache; /** * The current lookup path when performing non local lookups. This prevents infinite loops during such lookups. */ private HashSet<String> lookupPath; /** * The name of the local resource bundle. This property is only used for debugging and logging. */ private String resourceBase; /** * The locale for this bundle. */ private Locale locale; private ClassLoader sourceClassLoader; private static final Integer INVALID_MNEMONIC = new Integer( 0 ); /** * Creates a new instance. * * @param locale the locale that should be used to load the resource-bundle. * @param baseName the base name of the resource bundle, a fully qualified class name * @param classLoader the class-loader from where to load resources. */ public ResourceBundleSupport( final Locale locale, final String baseName, final ClassLoader classLoader ) { this( locale, ResourceBundle.getBundle( baseName, locale, classLoader ), baseName, classLoader ); } /** * Creates a new instance. * * @param locale the locale for which this resource bundle is created. * @param resourceBundle the resourcebundle * @param baseName the base name of the resource bundle, a fully qualified class name * @param classLoader the class-loader from where to load resources. */ public ResourceBundleSupport( final Locale locale, final ResourceBundle resourceBundle, final String baseName, final ClassLoader classLoader ) { if ( locale == null ) { throw new NullPointerException( "Locale must not be null" ); } if ( resourceBundle == null ) { throw new NullPointerException( "Resources must not be null" ); } if ( baseName == null ) { throw new NullPointerException( "BaseName must not be null" ); } if ( classLoader == null ) { throw new NullPointerException( "ClassLoader must not be null" ); } this.sourceClassLoader = classLoader; this.locale = locale; this.resources = resourceBundle; this.resourceBase = baseName; this.cache = new HashMap<String, String>(); this.lookupPath = new HashSet<String>(); } /** * Creates a new instance. * * @param locale the locale for which the resource bundle is created. * @param resourceBundle the resourcebundle * @param classLoader the class-loader from where to load resources. */ public ResourceBundleSupport( final Locale locale, final ResourceBundle resourceBundle, final ClassLoader classLoader ) { this( locale, resourceBundle, resourceBundle.toString(), classLoader ); } /** * The base name of the resource bundle. * * @return the resource bundle's name. */ protected final String getResourceBase() { return this.resourceBase; } /** * Gets a string for the given key from this resource bundle or one of its parents. If the key is a link, the link is * resolved and the referenced string is returned instead. * * @param key the key for the desired string * @return the string for the given key * @throws NullPointerException if <code>key</code> is <code>null</code> * @throws java.util.MissingResourceException if no object for the given key can be found * @throws ClassCastException if the object found for the given key is not a string */ public synchronized String strictString( final String key ) { if ( key == null ) { throw new NullPointerException(); } final String retval = this.cache.get( key ); if ( retval != null ) { return retval; } this.lookupPath.clear(); return internalGetString( key ); } /** * Performs the lookup for the given key. If the key points to a link the link is resolved and that key is looked up * instead. * * @param key the key for the string * @return the string for the given key */ protected String internalGetString( final String key ) { if ( key == null ) { throw new NullPointerException(); } if ( this.lookupPath.contains( key ) ) { throw new MissingResourceException ( "InfiniteLoop in resource lookup", getResourceBase(), this.lookupPath.toString() ); } final String fromResBundle = this.resources.getString( key ); if ( fromResBundle.length() > 0 && fromResBundle.charAt( 0 ) == '@' ) { if ( fromResBundle.length() > 1 && fromResBundle.charAt( 1 ) == '@' ) { // global forward ... final int idx = fromResBundle.indexOf( '@', 2 ); if ( idx == -1 ) { throw new MissingResourceException ( "Invalid format for global lookup key.", getResourceBase(), key ); } try { final ResourceBundle res = ResourceBundle.getBundle ( fromResBundle.substring( 2, idx ), locale, sourceClassLoader ); return res.getString( fromResBundle.substring( idx + 1 ) ); } catch ( Exception e ) { logger.error( "Error during global lookup", e ); throw new MissingResourceException( "Error during global lookup", getResourceBase(), key ); } } else { // local forward ... final String newKey = fromResBundle.substring( 1 ); this.lookupPath.add( key ); final String retval = internalGetString( newKey ); this.cache.put( key, retval ); return retval; } } else { this.cache.put( key, fromResBundle ); return fromResBundle; } } /** * Returns an scaled icon suitable for buttons or menus. * * @param key the name of the resource bundle key * @param large true, if the image should be scaled to 24x24, or false for 16x16 * @return the icon. */ public Icon getIcon( final String key, final boolean large ) { if ( key == null ) { throw new NullPointerException(); } final String name = strictString( key ); return createIcon( name, true, large ); } /** * Returns an unscaled icon. * * @param key the name of the resource bundle key * @return the icon. */ public Icon getIcon( final String key ) { if ( key == null ) { throw new NullPointerException(); } final String name = strictString( key ); return createIcon( name, false, false ); } /** * Returns the mnemonic stored at the given resourcebundle key. The mnemonic should be either the symbolic name of one * of the KeyEvent.VK_* constants (without the 'VK_') or the character for that key. * <p/> * For the enter key, the resource bundle would therefore either contain "ENTER" or "\n". * <pre> * a.resourcebundle.key=ENTER * an.other.resourcebundle.key=\n * </pre> * * @param key the resourcebundle key * @return the mnemonic */ public Integer getMnemonic( final String key ) { if ( key == null ) { throw new NullPointerException(); } final String name = strictString( key ); return createMnemonic( name ); } /** * Returns the mnemonic stored at the given resourcebundle key. The mnemonic should be either the symbolic name of one * of the KeyEvent.VK_* constants (without the 'VK_') or the character for that key. * <p/> * For the enter key, the resource bundle would therefore either contain "ENTER" or "\n". * <pre> * a.resourcebundle.key=ENTER * an.other.resourcebundle.key=\n * </pre> * * @param key the resourcebundle key * @return the mnemonic or null, if the mnemonic is not defined. */ public Integer getOptionalMnemonic( final String key ) { if ( key == null ) { throw new NullPointerException(); } final String name = getOptionalString( key ); if ( name != null && name.length() > 0 ) { return createMnemonic( name ); } return INVALID_MNEMONIC; } /** * Returns the keystroke stored at the given resourcebundle key. * <p/> * The keystroke will be composed of a simple key press and the plattform's MenuKeyMask. * <p/> * The keystrokes character key should be either the symbolic name of one of the KeyEvent.VK_* constants or the * character for that key. * <p/> * For the 'A' key, the resource bundle would therefore either contain "VK_A" or "a". * <pre> * a.resourcebundle.key=VK_A * an.other.resourcebundle.key=a * </pre> * * @param key the resourcebundle key * @return the keystroke * @see java.awt.Toolkit#getMenuShortcutKeyMask() */ public KeyStroke getKeyStroke( final String key ) { String name = strictString( key ); if ( StringUtils.isEmpty( name ) ) { return null; } boolean explicitNone = false; int mask = 0; final StringTokenizer strtok = new StringTokenizer( name ); while ( strtok.hasMoreTokens() ) { final String token = strtok.nextToken(); if ( "shift".equals( token ) ) { mask |= KeyEvent.SHIFT_MASK; } else if ( "alt".equals( token ) ) { mask |= KeyEvent.ALT_MASK; } else if ( "ctrl".equals( token ) ) { mask |= KeyEvent.CTRL_MASK; } else if ( "meta".equals( token ) ) { mask |= KeyEvent.META_MASK; } else if ( "menu".equals( token ) ) { mask |= getMenuKeyMask(); } else if ( "none".equals( token ) ) { explicitNone = true; } else { name = token; } } if ( explicitNone == true ) { mask = 0; } else if ( mask == 0 ) { mask = getMenuKeyMask(); } //noinspection MagicConstant return KeyStroke.getKeyStroke( createMnemonic( name ).intValue(), mask ); } /** * Returns the keystroke stored at the given resourcebundle key. * <p/> * The keystroke will be composed of a simple key press and a keystroke mask pattern. The pattern should be specified * via the words "shift", "alt", "ctrl", "meta" or "menu". Menu should be used to reference the platform specific menu * shortcut. For the sake of safety, menu should only be combined with "shift" and/or "alt" for menu keystrokes. * <p/> * The keystrokes character key should be either the symbolic name of one of the KeyEvent.VK_* constants or the * character for that key. * <p/> * For the 'A' key, the resource bundle would therefore either contain "VK_A" or "a". * <pre> * a.resourcebundle.key=VK_A * an.other.resourcebundle.key=a * </pre> * * @param key the resourcebundle key * @return the keystroke * @see java.awt.Toolkit#getMenuShortcutKeyMask() */ public KeyStroke getOptionalKeyStroke( final String key ) { try { String name = getOptionalString( key ); if ( StringUtils.isEmpty( name ) ) { return null; } boolean noneSelected = false; int mask = 0; final StringTokenizer strtok = new StringTokenizer( name ); while ( strtok.hasMoreTokens() ) { final String token = strtok.nextToken(); if ( "shift".equals( token ) ) { mask |= KeyEvent.SHIFT_MASK; } else if ( "alt".equals( token ) ) { mask |= KeyEvent.ALT_MASK; } else if ( "ctrl".equals( token ) ) { mask |= KeyEvent.CTRL_MASK; } else if ( "meta".equals( token ) ) { mask |= KeyEvent.META_MASK; } else if ( "menu".equals( token ) ) { mask |= getMenuKeyMask(); } else if ( "none".equals( token ) ) { noneSelected = true; } else { name = token; } } if ( noneSelected ) { mask = 0; } else if ( mask == 0 ) { mask = getMenuKeyMask(); } //noinspection MagicConstant return KeyStroke.getKeyStroke( createMnemonic( name ).intValue(), mask ); } catch ( MissingResourceException mre ) { return null; } } /** * Returns the keystroke stored at the given resourcebundle key. * <p/> * The keystroke will be composed of a simple key press and the given KeyMask. If the KeyMask is zero, a plain * Keystroke is returned. * <p/> * The keystrokes character key should be either the symbolic name of one of the KeyEvent.VK_* constants or the * character for that key. * <p/> * For the 'A' key, the resource bundle would therefore either contain "VK_A" or "a". * <pre> * a.resourcebundle.key=VK_A * an.other.resourcebundle.key=a * </pre> * * @param key the resourcebundle key * @param mask the key-moifier mask to be used to create the keystroke. * @return the keystroke that has been generated. * @see java.awt.Toolkit#getMenuShortcutKeyMask() */ public KeyStroke getKeyStroke( final String key, final int mask ) { if ( key == null ) { throw new NullPointerException(); } final String name = strictString( key ); //noinspection MagicConstant return KeyStroke.getKeyStroke( createMnemonic( name ).intValue(), mask ); } /** * Returns the keystroke stored at the given resourcebundle key. * <p/> * The keystroke will be composed of a simple key press and the given KeyMask. If the KeyMask is zero, a plain * Keystroke is returned. * <p/> * The keystrokes character key should be either the symbolic name of one of the KeyEvent.VK_* constants or the * character for that key. * <p/> * For the 'A' key, the resource bundle would therefore either contain "VK_A" or "a". * <pre> * a.resourcebundle.key=VK_A * an.other.resourcebundle.key=a * </pre> * * @param key the resourcebundle key * @param mask the key-moifier mask to be used to create the keystroke. * @return the keystroke or null if the key is not defined. * @see java.awt.Toolkit#getMenuShortcutKeyMask() */ public KeyStroke getOptionalKeyStroke( final String key, final int mask ) { if ( key == null ) { throw new NullPointerException(); } final String name = getOptionalString( key ); if ( name != null && name.length() > 0 ) { //noinspection MagicConstant return KeyStroke.getKeyStroke( createMnemonic( name ).intValue(), mask ); } return null; } /** * Returns a JMenu created from a resource bundle definition. * <p/> * The menu definition consists of two keys, the name of the menu and the mnemonic for that menu. Both keys share a * common prefix, which is extended by ".name" for the name of the menu and ".mnemonic" for the mnemonic. * <p/> * <pre> * # define the file menu * menu.file.name=File * menu.file.mnemonic=F * </pre> * The menu definition above can be used to create the menu by calling <code>createMenu ("menu.file")</code>. * * @param keyPrefix the common prefix for that menu * @return the created menu */ public JMenu createMenu( final String keyPrefix ) { if ( keyPrefix == null ) { throw new NullPointerException(); } final JMenu retval = new JMenu(); retval.setText( strictString( keyPrefix + ".name" ) ); final Integer mnemonic = getOptionalMnemonic( keyPrefix + ".mnemonic" ); if ( mnemonic != null ) { retval.setMnemonic( mnemonic.intValue() ); } return retval; } /** * Returns a URL pointing to a resource located in the classpath. The resource is looked up using the given key. * <p/> * Example: The load a file named 'logo.gif' which is stored in a java package named 'org.jfree.resources': * <pre> * mainmenu.logo=org/jfree/resources/logo.gif * </pre> * The URL for that file can be queried with: <code>getResource("mainmenu.logo");</code>. * * @param key the key for the resource * @return the resource URL */ public URL getResourceURL( final String key ) { if ( key == null ) { throw new NullPointerException(); } final String name = strictString( key ); final URL in = sourceClassLoader.getResource( name ); if ( in == null ) { logger.warn( "Unable to find file in the class path: " + name + "; key=" + key ); } return in; } /** * Attempts to load an image from classpath. If this fails, an empty image icon is returned. * * @param resourceName the name of the image. The name should be a global resource name. * @param scale true, if the image should be scaled, false otherwise * @param large true, if the image should be scaled to 24x24, or false for 16x16 * @return the image icon. */ private ImageIcon createIcon( final String resourceName, final boolean scale, final boolean large ) { final URL in = sourceClassLoader.getResource( resourceName ); if ( in == null ) { logger.warn( "Unable to find file in the class path: " + resourceName ); return new ImageIcon( createTransparentImage( 1, 1 ) ); } final Image img = Toolkit.getDefaultToolkit().createImage( in ); if ( img == null ) { logger.warn( "Unable to instantiate the image: " + resourceName ); return new ImageIcon( createTransparentImage( 1, 1 ) ); } if ( scale ) { if ( large ) { return new ImageIcon( img.getScaledInstance( 24, 24, Image.SCALE_SMOOTH ) ); } return new ImageIcon( img.getScaledInstance( 16, 16, Image.SCALE_SMOOTH ) ); } return new ImageIcon( img ); } /** * Creates the Mnemonic from the given String. The String consists of the name of the VK constants of the class * KeyEvent without VK_*. * * @param keyString the string * @return the mnemonic as integer */ private Integer createMnemonic( final String keyString ) { if ( keyString == null ) { throw new NullPointerException( "Key is null." ); } if ( keyString.length() == 0 ) { throw new IllegalArgumentException( "Key is empty." ); } int character = keyString.charAt( 0 ); if ( keyString.startsWith( "VK_" ) ) // NON-NLS { try { final Field f = KeyEvent.class.getField( keyString ); final Integer keyCode = (Integer) f.get( null ); character = keyCode.intValue(); } catch ( Exception nsfe ) { // ignore the exception ... } } return Integer.valueOf( character ); } /** * Returns the plattforms default menu shortcut keymask. * * @return the default key mask. */ private int getMenuKeyMask() { try { return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); } catch ( UnsupportedOperationException he ) { // headless exception extends UnsupportedOperation exception, // but the HeadlessException is not defined in older JDKs... return InputEvent.CTRL_MASK; } } /** * Creates a transparent image. These can be used for aligning menu items. * * @param width the width. * @param height the height. * @return the created transparent image. */ private BufferedImage createTransparentImage( final int width, final int height ) { final BufferedImage img = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB ); final int[] data = img.getRGB( 0, 0, width, height, null, 0, width ); img.setRGB( 0, 0, width, height, data, 0, width ); return img; } /** * Formats the message stored in the resource bundle (using a MessageFormat). * * @param key the resourcebundle key * @param parameter the parameter for the message * @return the formated string */ public String formatMessage( final String key, final Object parameter ) { return formatMessage( key, new Object[] { parameter } ); } /** * Formats the message stored in the resource bundle (using a MessageFormat). * * @param key the resourcebundle key * @param par1 the first parameter for the message * @param par2 the second parameter for the message * @return the formated string */ public String formatMessage( final String key, final Object par1, final Object par2 ) { return formatMessage( key, new Object[] { par1, par2 } ); } /** * Formats the message stored in the resource bundle (using a MessageFormat). * * @param key the resourcebundle key * @param parameters the parameter collection for the message * @return the formated string */ public String formatMessage( final String key, final Object[] parameters ) { final MessageFormat format = new MessageFormat( strictString( key ) ); format.setLocale( getLocale() ); return format.format( parameters ); } public String getString( final String key, final Object[] parameters ) { try { return formatMessage( key, parameters ); } catch ( MissingResourceException mre ) { logger.warn( "ResourceBundleSupport#getString(,,)", mre ); return '!' + key + '!'; } } public String getString( final String key ) { try { return strictString( key ); } catch ( MissingResourceException mre ) { logger.warn( "ResourceBundleSupport#getString(,,)", mre ); return '!' + key + '!'; } } public String getOptionalString( final String key ) { try { return strictString( key ); } catch ( Exception e ) { logger.trace( "Optional String is undefined", e ); // ignore it return null; } } public String getString( final String key, final Object par1 ) { try { return formatMessage( key, par1 ); } catch ( MissingResourceException mre ) { logger.warn( "ResourceBundleSupport#getString(,,)", mre ); return '!' + key + '!'; } } public String getString( final String key, final Object par1, final Object par2 ) { try { return formatMessage( key, par1, par2 ); } catch ( MissingResourceException mre ) { logger.warn( "ResourceBundleSupport#getString(,,)", mre ); return '!' + key + '!'; } } /** * Returns the current locale for this resource bundle. * * @return the locale. */ public Locale getLocale() { return locale; } }