001    /* JAPI - (Yet another (hopefully) useful) Java API
002     *
003     * Copyright (C) 2004-2006 Christian Hujer
004     *
005     * This program is free software; you can redistribute it and/or
006     * modify it under the terms of the GNU General Public License as
007     * published by the Free Software Foundation; either version 2 of the
008     * License, or (at your option) any later version.
009     *
010     * This program is distributed in the hope that it will be useful, but
011     * WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013     * General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with this program; if not, write to the Free Software
017     * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
018     * 02111-1307, USA.
019     */
020    
021    package net.sf.japi.swing;
022    
023    import java.awt.Component;
024    import java.awt.event.ActionEvent;
025    import java.lang.reflect.Field;
026    import java.text.MessageFormat;
027    import java.util.LinkedList;
028    import java.util.List;
029    import java.util.Map;
030    import java.util.MissingResourceException;
031    import java.util.ResourceBundle;
032    import static java.util.ResourceBundle.getBundle;
033    import java.util.WeakHashMap;
034    import java.util.ArrayList;
035    import java.util.prefs.Preferences;
036    import static java.util.prefs.Preferences.userNodeForPackage;
037    import javax.swing.AbstractAction;
038    import javax.swing.Action;
039    import static javax.swing.Action.ACCELERATOR_KEY;
040    import static javax.swing.Action.LONG_DESCRIPTION;
041    import static javax.swing.Action.MNEMONIC_KEY;
042    import static javax.swing.Action.NAME;
043    import static javax.swing.Action.SHORT_DESCRIPTION;
044    import static javax.swing.Action.SMALL_ICON;
045    import javax.swing.ActionMap;
046    import javax.swing.Icon;
047    import javax.swing.JCheckBox;
048    import javax.swing.JLabel;
049    import javax.swing.JMenu;
050    import javax.swing.JMenuBar;
051    import javax.swing.JMenuItem;
052    import javax.swing.JOptionPane;
053    import javax.swing.JToolBar;
054    import static javax.swing.KeyStroke.getKeyStroke;
055    import org.jetbrains.annotations.NonNls;
056    import org.jetbrains.annotations.NotNull;
057    import org.jetbrains.annotations.Nullable;
058    import static net.sf.japi.swing.IconManager.getDefaultIconManager;
059    import static net.sf.japi.swing.ReflectionAction.REFLECTION_MESSAGE_PROVIDER;
060    import static net.sf.japi.swing.ReflectionAction.REFLECTION_TARGET;
061    import static net.sf.japi.swing.ToggleAction.REFLECTION_PROPERTY_NAME;
062    
063    /** Class for creating and initializing {@link Action}s that are localized, user configurable and may invoke their final methods using Reflection;
064     * also handles extremely much useful stuff for i18n/l10n.
065     * It is meant as a general service for creating Action objects that have some comfort for the programmer in several aspects:
066     * <ul>
067     *  <li>Allow zero or more ResourceBundles to be used when creating Actions</li>
068     *  <li>Allow zero or more UserPreferences to be used when creating Actions</li>
069     *  <li>Manage an ActionMap to which created Actions are automatically added</li>
070     * </ul>
071     * You may choose to use one or more ActionFactories, depending on the size of your application.
072     * You may use to spread Action configuration information accross one or more ResourceBundles and one or more Preferences, as you wish.
073     * When looking for values, the Preferences are queried first, in addition order, after that the ResourceBundles, again in addition order, until
074     * a value was found. The behaviour when no value was found is undefined.
075     * <h3>Usage</h3>
076     * The recommended usage is to
077     * <ol>
078     *  <li>
079     *      create and initialize an ActionFactory similar as the following example code, put it somewhere at the start of your program:
080     * <pre>
081     * ActionFactory myActionFactory = ActionFactory.getFactory("MyApplication");
082     * myActionFactory.addBundle("com.mycompany.mypackage.myresource"); // not always required
083     * myActionFactory.addPref(MyClass.class);
084     * </pre>
085     *  </li>
086     *  <li>
087     *      then use the ActionFactory from anywhere within the application like this:
088     * <pre>
089     * ActionFactory myActionFactory = ActionFactory.getFactory("MyApplication");
090     * Action myAction = myActionFactory.createAction("load", this);
091     * </pre>
092     *  </li>
093     * </ol>
094     * <p>
095     *  All actions created or initialized by an instance of this class are optionally put into that instance's {@link ActionMap}.
096     *  If they are stored, you can use that map for later retrieval.
097     * </p>
098     * <h4>Usage Notes: Factory Name</h4>
099     * <ul>
100     *  <li>
101     *      The factory name is used to try to load a resource bundle when a bundle is created.
102     *      The factory name is used as package name for the bundle, the bundle name itself is "action".
103     *      Example: When calling <code>ActionFactory.getFactory("net.sf.japi.swing");</code> for the first time, it is tried to load a
104     *      {@link ResourceBundle} named <code>net.sf.japi.swing.actions</code> for that <code>ActionFactory</code>.
105     *      This automatism has been implemented to save you from the need of initializing an ActionFactory before use.
106     *  </li>
107     * </ul>
108     * <h4>Usage Notes: Action Key / Action Name</h4>
109     * The key you supply as first argument of {@link #createAction(boolean, String, Object)} determines several things:
110     * <ul>
111     *  <li>The base name for the different keys in the preferences / resource bundle and other known Action Keys:
112     *      <table border="1">
113     *          <tr><th>What</th><th>Preferences / Bundle key</th><th>Action key if stored in an action</th></tr>
114     *          <tr><td>An (somewhat unique) ID</td><td>(<var>basename</var> itself)</td><td>{@link #ACTION_ID}</td></tr>
115     *          <tr><td>The icon</td><td><code><var>basename</var> + ".icon"</code></td><td>{@link Action#SMALL_ICON}</td></tr>
116     *          <tr><td>The tooltip help</td><td><code><var>basename</var> + ".shortdescription"</code></td><td>{@link Action#SHORT_DESCRIPTION}</td></tr>
117     *          <tr><td>The long help</td><td><code><var>basename</var> + ".longdescription"</code></td><td>{@link Action#LONG_DESCRIPTION}</td></tr>
118     *          <tr><td>The text label</td><td><code><var>basename</var> + ".text"</code></td><td>{@link Action#NAME}</td></tr>
119     *          <tr><td>The keyboard accelerator</td><td><code><var>basename</var> + ".accel"</code></td><td>{@link Action#ACCELERATOR_KEY}</td></tr>
120     *          <tr><td>The alternate keyboard accelerator</td><td><code><var>basename</var> + ".accel2"</code></td><td>{@link #ACCELERATOR_KEY_2}</td></tr>
121     *          <tr><td>The mnemonic</td><td><code><var>basename</var> + ".mnemonic"</code></td><td>{@link Action#MNEMONIC_KEY}</td></tr>
122     *          <tr><td>The method name</td><td></td><td>{@link ReflectionAction#REFLECTION_METHOD_NAME}</td></tr>
123     *          <tr><td>The method</td><td></td><td>{@link ReflectionAction#REFLECTION_METHOD}</td></tr>
124     *          <tr><td>The boolean property name</td><td></td><td>{@link ToggleAction#REFLECTION_PROPERTY_NAME}</td></tr>
125     *          <tr><td>The target instance</td><td></td><td>{@link ReflectionAction#REFLECTION_TARGET}</td></tr>
126     *          <tr><td>Exception handler dialogs</td><td><code><var>basename</var> + ".exception." + <var>exception class name</var> + ...</code><br/>The message can be formatted with 1 parameter that will be the localized message of the thrown exception.</td><td>n/a</td></tr>
127     *      </table>
128     *  </li>
129     * </ul>
130     * <p>Some methods are not related to actions, yet take base keys:</p>
131     * <ul>
132     *  <li>The methods for dialogs, e.g. {@link #showMessageDialog(Component, String, Object...)}:
133     *      <table border="1">
134     *          <tr><th>What</th><th>Preferences / Bundle key</th></tr>
135     *          <tr><td>Dialog title</td><td><code><var>basename</var> + ".title"</code></td></tr>
136     *          <tr><td>Dialog message</td><td><code><var>basename</var> + ".message"</code></td></tr>
137     *          <tr><td>Dialog messagetype </td><td><code><var>basename</var> + ".messagetype"</code><br/>The message type should be one of the message types defined in {@link JOptionPane}, e.g. {@link JOptionPane#PLAIN_MESSAGE}.</td></tr>
138     *      </table>
139     *  </li>
140     * </ul>
141     * <h4>Final Notes</h4>
142     * <ul>
143     *  <li>
144     *      If by having read all this you think it might often be a good idea to use a package name as a factory name: this is completely right and the
145     *      most common way of using an ActionFactory.
146     *  </li>
147     *  <li>
148     *      If you think you're too lazy to hold your own ActionFactory reference and instead more often call {@link #getFactory(String)}, just go ahead
149     *      and do so.
150     *      Looking up created ActionFactories is extremely fast, and of course they are initialized exactly once, not more.
151     *  </li>
152     * </ul>
153     * @see AbstractAction
154     * @see Action
155     * @see Preferences
156     * @see ResourceBundle
157     * @todo think about toolbar interaction
158     * @todo think about toolbar configuration
159     * @todo eventually rename this ActionBuilder and provide an ActionBuilderFactory.
160     * @author <a href="mailto:chris@riedquat.de">Christian Hujer</a>
161     */
162    public final class ActionFactory {
163    
164        /** The key used for storing a somewhat unique id for this Action. */
165        @NotNull public static final String ACTION_ID = "ActionID";
166    
167        /** The key used for storing an alternative accelerator for this Action.
168         * Currently unused.
169         */
170        @NotNull public static final String ACCELERATOR_KEY_2 = "AcceleratorKey2";
171    
172        /** The ActionFactories. */
173        @NotNull private static final Map<String, ActionFactory> FACTORIES = new WeakHashMap<String, ActionFactory>();
174    
175        /** The parent ActionFactories. */
176        @NotNull private final List<ActionFactory> parents = new LinkedList<ActionFactory>();
177    
178        /** The ResourceBundles to look for.
179         * Type: ResourceBundle
180         */
181        @NotNull private final List<ResourceBundle> bundles = new LinkedList<ResourceBundle>();
182    
183        /** The Preferences to look for.
184         * Type: Preferences
185         */
186        @NotNull private final List<Preferences> prefs = new LinkedList<Preferences>();
187    
188        /** The ActionMap to which created Actions are automatically added. */
189        @NotNull private final ActionMap actionMap = new NamedActionMap();
190    
191        private List<ActionProvider> actionProviders = new ArrayList<ActionProvider>();
192    
193        /** Get an ActionFactory.
194         * If there is no ActionFactory with name <var>key</var>, a new ActionFactory is created and stored.
195         * Future invocations of this method will constantly return that ActionFactory unless the key is garbage collected.
196         * If you must prevent the key from being garbage collected (and with it the ActionFactory), you may internalize the key ({@link String#intern()}).
197         * A good name for a key is the application or package name.
198         * The <code><var>key</var></code> may be a package name, in which case it is tried to load a {@link ResourceBundle} named "action" from that
199         * package and add it ({@link #addBundle(ResourceBundle)}); nothing special happens if that fails.
200         * @param key name of ActionFactory (which even may be <code>null</code> if you are too lazy to invent a key)
201         * @return ActionFactory for given key. The factory is created in case it didn't already exist.
202         */
203        @NotNull public static ActionFactory getFactory(@Nullable final String key) {
204            ActionFactory factory = FACTORIES.get(key);
205            if (factory == null) {
206                factory = new ActionFactory();
207                FACTORIES.put(key, factory);
208                try {
209                    factory.addBundle(key + ".action");
210                } catch (final MissingResourceException e) {
211                    /* ignore */
212                }
213                // eventually initialize factory here
214            }
215            return factory;
216        }
217    
218        /** Add a ResourceBundle to the list of used bundles.
219         * @param baseName the base name of the resource bundle, a fully qualified class name
220         * @see ResourceBundle#getBundle(String)
221         */
222        public void addBundle(@NotNull final String baseName) {
223            //noinspection ConstantConditions
224            if (baseName == null) {
225                throw new NullPointerException("null bundle name not allowed");
226            }
227            @NotNull final ResourceBundle newBundle = getBundle(baseName);
228            addBundle(newBundle);
229            @Nullable final String additionalBundles = newBundle.getString("ActionFactory.additionalBundles");
230            if (additionalBundles != null) {
231                for (final String additionalBundle : additionalBundles.split("\\s+")) {
232                    addBundle(additionalBundle);
233                }
234            }
235        }
236    
237        /** Method to find the JMenuItem for a specific Action.
238         * @param menuBar JMenuBar to search
239         * @param action Action to find JMenuItem for
240         * @return JMenuItem for action or <code>null</code> if none found
241         * @throws NullPointerException if <var>action</var> or <var>menuBar</var> is <code>null</code>
242         */
243        @Nullable public static JMenuItem find(@NotNull final JMenuBar menuBar, @NotNull final Action action) {
244            //noinspection ConstantConditions
245            if (menuBar == null) {
246                throw new NullPointerException("null JMenuBar not allowed");
247            }
248            //noinspection ConstantConditions
249            if (action == null) {
250                throw new NullPointerException("null Action not allowed");
251            }
252            for (int i = 0; i < menuBar.getMenuCount(); i++) {
253                final JMenu menu = menuBar.getMenu(i);
254                if (menu.getAction() == action) {
255                    return menu;
256                } else {
257                    final JMenuItem ret = find(menu, action);
258                    if (ret != null) {
259                        return ret;
260                    }
261                }
262            }
263            return null;
264        }
265    
266        /** Method to find the JMenuItem for a specific Action.
267         * @param menu JMenu to search
268         * @param action Action to find JMenuItem for
269         * @return JMenuItem for action or <code>null</code> if none found
270         * @throws NullPointerException if <var>menu</var> or <var>action</var> is <code>null</code>
271         */
272        @Nullable public static JMenuItem find(@NotNull final JMenu menu, @NotNull final Action action) {
273            for (int i = 0; i < menu.getItemCount(); i++) {
274                final JMenuItem item = menu.getItem(i);
275                if (item == null) {
276                    // Ignore Separators
277                } else if (item.getAction() == action) {
278                    return item;
279                } else if (item instanceof JMenu) {
280                    final JMenuItem ret = find((JMenu) item, action);
281                    if (ret != null) {
282                        return ret;
283                    }
284                }
285            }
286            return null;
287        }
288    
289        /** Create an ActionFactory.
290         * This constructor is private to force users to use the method {@link #getFactory(String)} for recycling ActionFactories and profit of easy
291         * access to the same ActionFactory from within the whole application without passing around ActionFactory references.
292         */
293        private ActionFactory() {
294        }
295    
296        /** Get the ActionMap.
297         * @return ActionMap
298         */
299        @NotNull public ActionMap getActionMap() {
300            return actionMap;
301        }
302    
303        /** Add a ResourceBundle to the list of used bundles.
304         * @param bundle ResourceBundle to add
305         * @throws NullPointerException if <code>bundle == null</code>
306         */
307        public void addBundle(@NotNull final ResourceBundle bundle) throws NullPointerException {
308            //noinspection ConstantConditions
309            if (bundle == null) {
310                throw new NullPointerException("null ResourceBundle not allowed");
311            }
312            if (!bundles.contains(bundle)) {
313                // insert first because new bundles override old bundles
314                bundles.add(0, bundle);
315            }
316        }
317    
318        /** Add a parent to the list of used parents.
319         * @param parent Parent to use if lookups failed in this ActionFactory
320         * WARNING: Adding a descendents as parents of ancestors or vice versa will result in endless recursion and thus stack overflow!
321         * @throws NullPointerException if <code>parent == null</code>
322         */
323        public void addParent(@NotNull final ActionFactory parent) throws NullPointerException {
324            //noinspection ConstantConditions
325            if (parent == null) {
326                throw new NullPointerException("null ActionFactory not allowed");
327            }
328            parents.add(parent);
329        }
330    
331        /** Add a Preferences to the list of used preferences.
332         * @param pref Preferences to add
333         * @throws NullPointerException if <code>pref == null</code>
334         */
335        public void addPref(@NotNull final Preferences pref) throws NullPointerException {
336            //noinspection ConstantConditions
337            if (pref == null) {
338                throw new NullPointerException("null ResourceBundle not allowed");
339            }
340            if (!prefs.contains(pref)) {
341                prefs.add(pref);
342            }
343        }
344    
345        /** Add a Preferences to the list of used preferences.
346         * @param clazz the class whose package a user preference node is desired
347         * @see Preferences#userNodeForPackage(Class)
348         * @throws NullPointerException if <code>clazz == null</code>
349         */
350        public void addPref(@NotNull final Class<?> clazz) {
351            //noinspection ConstantConditions
352            if (clazz == null) {
353                throw new NullPointerException("null Class not allowed");
354            }
355            addPref(userNodeForPackage(clazz));
356        }
357    
358        /** Creates actions.
359         * This is a loop variant of {@link #createAction(boolean,String)}.
360         * The actions created can be retrieved using {@link #getAction(String)} or via the ActionMap returned by {@link #getActionMap()}.
361         * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
362         * @param keys Keys of actions to create
363         * @return Array with created actions
364         * @throws NullPointerException in case keys is or contains <code>null</code>
365         */
366        public Action[] createActions(final boolean store, @NotNull final String... keys) throws NullPointerException {
367            final Action[] actions = new Action[keys.length];
368            for (int i = 0; i < keys.length; i++) {
369                actions[i] = createAction(store, keys[i]);
370            }
371            return actions;
372        }
373    
374        /** Create an Action.
375         * The created Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
376         * retreive using {@link #getActionMap()}.
377         * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
378         * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles and as ActionMap key (may not be
379         * <code>null</code>)
380         * @return created Action, which is a dummy in the sense that its {@link Action#actionPerformed(ActionEvent)} method does not do anything
381         * @throws NullPointerException in case <var>key</var> was <code>null</code>
382         * @see #createAction(boolean,String,Object)
383         * @see #createToggle(boolean,String,Object)
384         * @see #initAction(boolean,Action,String)
385         */
386        public Action createAction(final boolean store, @NotNull final String key) throws NullPointerException {
387            // initAction() checks for null key
388            return initAction(store, new DummyAction(), key);
389        }
390    
391        /** Initialize an Action.
392         * This is a convenience method for Action users which want to use the services provided by this {@link ActionFactory} class but need more
393         * sophisticated Action objects they created on their own.
394         * So you can simply create an Action and pass it to this Initialization method to fill its data.
395         * The initialized Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
396         * retreive using {@link #getActionMap()}.
397         * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
398         * @param action Action to fill
399         * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles and as ActionMap key (may not be <code>null</code>)
400         * @return the supplied Action object (<var>action</var>) is returned for convenience
401         * @throws NullPointerException in case <var>key</var> was <code>null</code>
402         */
403        @SuppressWarnings({"NestedAssignment"})
404        public Action initAction(final boolean store, @NotNull final Action action, @NotNull final String key) throws NullPointerException {
405            if (key == null) { throw new NullPointerException("null key for Action initialization not allowed."); }
406            action.putValue(ACTION_ID, key);
407            String value;
408            if ((value = getString(key + ".text"            )) != null) { action.putValue(NAME,              value); }
409            if ((value = getString(key + ".shortdescription")) != null) { action.putValue(SHORT_DESCRIPTION, value); }
410            if ((value = getString(key + ".longdescription" )) != null) { action.putValue(LONG_DESCRIPTION,  value); }
411            if ((value = getString(key + ".accel"           )) != null) { action.putValue(ACCELERATOR_KEY,   getKeyStroke(value)); }
412            if ((value = getString(key + ".accel2"          )) != null) { action.putValue(ACCELERATOR_KEY_2, getKeyStroke(value)); }
413            if ((value = getString(key + ".mnemonic"        )) != null) { action.putValue(MNEMONIC_KEY,      getKeyStroke(value).getKeyCode()); }
414            if ((value = getString(key + ".icon"            )) != null) {
415                final Icon image = getDefaultIconManager().getIcon(value);
416                if (image != null) { action.putValue(SMALL_ICON, image); }
417            }
418            action.putValue(REFLECTION_MESSAGE_PROVIDER, this);
419            if (store) {
420                actionMap.put(key, action);
421            }
422            return action;
423        }
424    
425        /** Get a String.
426         * First looks one pref after another, in their addition order.
427         * Then looks one bundle after another, in their addition order.
428         * @param key Key to get String for
429         * @return the first value found or <code>null</code> if no value could be found
430         * @throws NullPointerException if <var>key</var> is <code>null</code>
431         */
432        @Nullable public String getString(@NotNull final String key) throws NullPointerException {
433            if (key == null) {
434                throw new NullPointerException("null key not allowed");
435            }
436            String value = null;
437            for (final Preferences pref : prefs) {
438                value = pref.get(key, value);
439                if (value != null) {
440                    return value;
441                }
442            }
443            for (final ResourceBundle bundle : bundles) {
444                try {
445                    value = bundle.getString(key);
446                    return value;
447                } catch (final MissingResourceException e) { /* ignore */
448                } catch (final ClassCastException e) { /* ignore */
449                } // ignore exceptions because they don't mean errors just there's no resource, so parents are checked or null is returned.
450            }
451            for (final ActionFactory parent : parents) {
452                value = parent.getString(key);
453                if (value != null) {
454                    return value;
455                }
456            }
457            return null;
458        }
459    
460        /** Get a String from the preferences, ignoring the resource bundles.
461         * @param key Key to get String for
462         * @return the first value found or <code>null</code> if no value could be found
463         * @throws NullPointerException if <var>key</var> is <code>null</code>
464         */
465        @Nullable public String getStringFromPrefs(@NotNull final String key) throws NullPointerException {
466            if (key == null) {
467                throw new NullPointerException("null key not allowed");
468            }
469            String value = null;
470            for (final Preferences pref : prefs) {
471                value = pref.get(key, value);
472                if (value != null) {
473                    return value;
474                }
475            }
476            for (final ActionFactory parent : parents) {
477                value = parent.getStringFromPrefs(key);
478                if (value != null) {
479                    return value;
480                }
481            }
482            return null;
483        }
484    
485        /** Get a String from the resource bundles, ignoring the preferences.
486         * @param key Key to get String for
487         * @return the first value found or <code>null</code> if no value could be found
488         * @throws NullPointerException if <var>key</var> is <code>null</code>
489         */
490        @Nullable public String getStringFromBundles(@NotNull final String key) throws NullPointerException {
491            if (key == null) {
492                throw new NullPointerException("null key not allowed");
493            }
494            String value = null;
495            for (final ResourceBundle bundle : bundles) {
496                try {
497                    value = bundle.getString(key);
498                    return value;
499                } catch (final MissingResourceException e) { /* ignore */
500                } catch (final ClassCastException e) { /* ignore */
501                } // ignore exceptions because they don't mean errors just there's no resource, so parents are checked or null is returned.
502            }
503            for (final ActionFactory parent : parents) {
504                value = parent.getStringFromBundles(key);
505                if (value != null) {
506                    return value;
507                }
508            }
509            return null;
510        }
511        /** Creates actions.
512         * This is a loop variant of {@link #createAction(boolean,String,Object)}.
513         * The actions created can be retrieved using {@link #getAction(String)} or via the ActionMap returned by {@link #getActionMap()}.
514         * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
515         * @param target Target object
516         * @param keys Keys of actions to create
517         * @return Array with created actions
518         * @throws NullPointerException in case keys is or contains <code>null</code>
519         */
520        public Action[] createActions(final boolean store, final Object target, final String... keys) throws NullPointerException {
521            final Action[] actions = new Action[keys.length];
522            for (int i = 0; i < keys.length; i++) {
523                actions[i] = createAction(store, keys[i], target);
524            }
525            return actions;
526        }
527    
528        /** Create an Action.
529         * The created Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
530         * retreive using {@link #getActionMap()}.
531         * The supplied object needs to have a zero argument method named <var>key</var>.
532         * You may pass <code>null</code> as object, which means that the object the method is invoked on is not defined yet.
533         * You may safely use the Action, it will not throw any Exceptions upon {@link Action#actionPerformed(ActionEvent)} but simply silently do nothing.
534         * The desired object can be set later using <code>action.putValue({@link ReflectionAction#REFLECTION_TARGET}, <var>object</var>)</code>.
535         * <p />
536         * Users of this method can assume that the returned object behaves quite like {@link ReflectionAction}.
537         * Wether or not this method returns an instance of {@link ReflectionAction} should not be relied on.
538         * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
539         * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles, as ActionMap key and as Reflection Method
540         * name within the supplied object (may not be <code>null</code>)
541         * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(ActionEvent)})
542         * @return created Action
543         * @throws NullPointerException in case <var>key</var> was <code>null</code>
544         * @see #createAction(boolean,String)
545         * @see #createToggle(boolean,String,Object)
546         * @see #initAction(boolean,Action,String)
547         */
548        public Action createAction(final boolean store, final String key, final Object object) throws NullPointerException {
549            if (key == null) { throw new NullPointerException("null key for Action creation not allowed."); }
550            final Action action = new ReflectionAction(key, object);
551            initAction(store, action, key);
552            return action;
553        }
554    
555        /** Method for creating a Menu.
556         * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
557         * @param menuKey action key for Menu
558         * @param keys Action keys for menu items
559         * @return menu created from the menu definition found
560         * @throws Error in case action definitions for <var>keys</var> weren't found
561         */
562        public JMenu createMenu(final boolean store, final String menuKey, final String... keys) throws Error {
563            final JMenu menu = new JMenu(createAction(store, menuKey));
564            for (final String key : keys) {
565                if (key != null && key.length() == 0) {
566                    /* ignore this for empty menus */
567                } else if (key == null || "-".equals(key)) {
568                    menu.addSeparator();
569                } else {
570                    final Action action = getAction(key);
571                    if (action == null) {
572                        throw new Error("No Action for key " + key);
573                    }
574                    if (action instanceof ToggleAction) {
575                        menu.add(((ToggleAction) action).createCheckBoxMenuItem());
576                    } else {
577                        menu.add(action);
578                    }
579                }
580            }
581            return menu;
582        }
583    
584        /** Get an Action.
585         * For an action to be retrieved with this method, it must have been initialized with {@link #initAction(boolean,Action,String)}, either directly by
586         * invoking {@link #initAction(boolean,Action,String)} or indirectly by invoking one of this class' creation methods like {@link #createAction(boolean,String)},
587         * {@link #createAction(boolean,String,Object)} or {@link #createToggle(boolean,String,Object)}.
588         * @param key Key of action to get
589         * @return Action for <var>key</var>
590         * This method does the same as <code>getActionMap().get(key)</code>.
591         */
592        public Action getAction(final String key) {
593            Action action = actionMap.get(key);
594            if (action == null) {
595                for (final ActionProvider actionProvider : actionProviders) {
596                    action = actionProvider.getAction(key);
597                    if (action != null) {
598                        actionMap.put(key, action);
599                        break;
600                    }
601                }
602            }
603            return action;
604        }
605    
606        /** Method for creating a menubar.
607         * @param store whether to store the initialized Actions in the ActionMap of this ActionFactory
608         * @param barKey Action key of menu to create
609         * @return JMenuBar created for <var>barKey</var>
610         */
611        public JMenuBar createMenuBar(final boolean store, final String barKey) {
612            final JMenuBar menuBar = new JMenuBar();
613            for (final String key : getString(barKey + ".menubar").split("\\s+")) {
614                menuBar.add(createMenu(store, key));
615            }
616            return menuBar;
617        }
618    
619        /** Method for creating a Menu.
620         * This method assumes that the underlying properties contain an entry like <code>key + ".menu"</code> which lists the menu element's keys.
621         * Submenus are build recursively.
622         * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
623         * @param menuKey action key for menu
624         * @return menu created from the menu definition found
625         * @throws Error in case a menu definition for <var>menuKey</var> wasn't found
626         */
627        public JMenu createMenu(final boolean store, final String menuKey) throws Error {
628            final JMenu menu = new JMenu(createAction(store, menuKey));
629            for (final String key : getString(menuKey + ".menu").split("\\s+")) {
630                if (key != null && key.length() == 0) {
631                    /* ignore this for empty menus */
632                } else if (key == null || "-".equals(key)) {
633                    menu.addSeparator();
634                } else if (getString(key + ".menu") != null) {
635                    menu.add(createMenu(store, key));
636                } else {
637                    final Action action = getAction(key);
638                    if (action == null) {
639                        throw new Error("No Action for key " + key);
640                    }
641                    if (action instanceof ToggleAction) {
642                        menu.add(((ToggleAction) action).createCheckBoxMenuItem());
643                    } else {
644                        menu.add(action);
645                    }
646                }
647            }
648            return menu;
649        }
650    
651        /** Method for creating a menubar.
652         * @param store whether to store the initialized Actions in the ActionMap of this ActionFactory
653         * @param barKey Action key of menu to create
654         * @param target Target object
655         * @return JMenuBar created for <var>barKey</var>
656         */
657        public JMenuBar createMenuBar(final boolean store, final String barKey, final Object target) {
658            final JMenuBar menuBar = new JMenuBar();
659            final String menuBarSpec = getString(barKey + ".menubar");
660            if (menuBarSpec == null) {
661                throw new RuntimeException("Missing Resource for " + barKey + ".menubar");
662            }
663            for (final String key : menuBarSpec.split("\\s+")) {
664                menuBar.add(createMenu(store, key, target));
665            }
666            return menuBar;
667        }
668    
669        /** Method for creating a Menu.
670         * This method assumes that the underlying properties contain an entry like <code>key + ".menu"</code> which lists the menu element's keys.
671         * Submenus are build recursively.
672         * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
673         * @param menuKey action key for menu
674         * @param target Target object
675         * @return menu created from the menu definition found
676         * @throws Error in case a menu definition for <var>menuKey</var> wasn't found
677         */
678        public JMenu createMenu(final boolean store, final String menuKey, final Object target) throws Error {
679            final JMenu menu = new JMenu(createAction(store, menuKey));
680            for (final String key : getString(menuKey + ".menu").split("\\s+")) {
681                if (key != null && key.length() == 0) {
682                    /* ignore this for empty menus */
683                } else if (key == null || "-".equals(key)) {
684                    menu.addSeparator();
685                } else if (getString(key + ".menu") != null) {
686                    menu.add(createMenu(store, key, target));
687                } else {
688                    Action action = null;
689                    if (store) {
690                        action = getAction(key);
691                    }
692                    if (action == null) {
693                        action = createAction(store, key, target);
694                    }
695                    if (action == null) {
696                        throw new Error("No Action for key " + key);
697                    }
698                    if (action instanceof ToggleAction) {
699                        menu.add(((ToggleAction) action).createCheckBoxMenuItem());
700                    } else {
701                        menu.add(action);
702                    }
703                }
704            }
705            return menu;
706        }
707    
708        /** Creates actions.
709         * This is a loop variant of {@link #createToggle(boolean,String,Object)}.
710         * The actions created can be retrieved using {@link #getAction(String)} or via the ActionMap returned by {@link #getActionMap()}.
711         * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
712         * @param target Target object
713         * @param keys Keys of actions to create
714         * @throws NullPointerException in case <var>keys</var> was or contained <code>null</code>
715         */
716        public void createToggles(final boolean store, final Object target, final String... keys) throws NullPointerException {
717            for (final String key : keys) {
718                createToggle(store, key, target);
719            }
720        }
721    
722        /** Create an Action.
723         * The created Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
724         * retreive using {@link #getActionMap()}.
725         * The supplied object needs to have a boolean return void argument getter method and a void return boolean argument setter method matching the
726         * <var>key</var>.
727         * You may pass <code>null</code> as object, which means that the object the getters and setters are invoked on is not defined yet.
728         * You may safely use the Action, it will not throw any Exceptions upon {@link Action#actionPerformed(ActionEvent)} but simply silently do nothing.
729         * The desired object can be set later using <code>action.putValue({@link ToggleAction#REFLECTION_TARGET}, <var>object</var>)</code>.
730         * <p />
731         * Users of this method can assume that the returned object behaves quite like {@link ToggleAction}.
732         * Wether or not this method returns an instance of {@link ToggleAction} shuold not be relied on.
733         * @see #createAction(boolean,String)
734         * @see #createAction(boolean,String,Object)
735         * @see #initAction(boolean,Action,String)
736         * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
737         * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles, as ActionMap key and as Property name within
738         * the supplied object (may not be <code>null</code>)
739         * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(ActionEvent)})
740         * @throws NullPointerException in case <var>key</var> was <code>null</code>
741         * @return ToggleAction
742         */
743        public Action createToggle(final boolean store, final String key, final Object object) throws NullPointerException {
744            final Action action = new ToggleAction();
745            initAction(store, action, key);
746            action.putValue(REFLECTION_PROPERTY_NAME, key);
747            action.putValue(REFLECTION_TARGET, object);
748            return action;
749        }
750    
751        /** Method for creating a toolbar.
752         * @param keys Action keys for toolbar entries
753         * @return JToolBar created for <var>keys</var>
754         */
755        public JToolBar createToolBar(final String... keys) {
756            final JToolBar toolBar = new JToolBar();
757            for (final String key : keys) {
758                if (key == null || "-".equals(key)) {
759                    toolBar.addSeparator();
760                } else {
761                    final Action action = getAction(key);
762                    if (action == null) {
763                        throw new Error("No Action for key " + key);
764                    }
765                    toolBar.add(action);
766                }
767            }
768            return toolBar;
769        }
770    
771        /** Method for creating a toolbar.
772         * @param barKey Action keys of toolbar to create
773         * @return JToolBar created for <var>barKey</var>
774         */
775        public JToolBar createToolBar(final String barKey) {
776            return createToolBar(getString(barKey + ".toolbar").split("\\s+"));
777        }
778    
779        /** Method for creating a toolbar.
780         * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(ActionEvent)})
781         * @param keys Action keys for toolbar entries
782         * @return JToolBar created for <var>keys</var>
783         */
784        public JToolBar createToolBar(final Object object, final String... keys) {
785            final JToolBar toolBar = new JToolBar();
786            for (final String key : keys) {
787                if (key == null || "-".equals(key)) {
788                    toolBar.addSeparator();
789                } else {
790                    Action action = getAction(key);
791                    if (action == null) {
792                        action = createAction(false, key, object);
793                    }
794                    toolBar.add(action);
795                }
796            }
797            return toolBar;
798        }
799    
800        /** Method for creating a toolbar.
801         * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(ActionEvent)})
802         * @param barKey Action keys of toolbar to create
803         * @return JToolBar created for <var>barKey</var>
804         */
805        public JToolBar createToolBar(final Object object, final String barKey) {
806            return createToolBar(object, getString(barKey + ".toolbar").split("\\s+"));
807        }
808    
809        /** Method to find the JMenuItem for a specific Action key.
810         * @param menuBar JMenuBar to search
811         * @param key Key to find JMenuItem for
812         * @return JMenuItem for key or <code>null</code> if none found
813         */
814        public JMenuItem find(final JMenuBar menuBar, final String key) {
815            return find(menuBar, getAction(key));
816        }
817    
818        /** Get an icon.
819         * @param key i18n key for icon
820         * @return icon
821         */
822        public Icon getIcon(final String key) {
823            return getDefaultIconManager().getIcon(getString(key));
824        }
825    
826        /** Show a localized message dialog.
827         * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
828         * no <code>Frame</code>, a default <code>Frame</code> is used
829         * @param messageType the type of message to be displayed
830         * @param key localization key to use for the title, the message and eventually the icon
831         * @param args formatting arguments for the message text
832         * @deprecated use {@link #showMessageDialog(Component, String, Object...)} instead and put the messagetype in the action.properties file.
833         */
834        @Deprecated public void showMessageDialog(final Component parentComponent, final int messageType, final String key, final Object... args) {
835            JOptionPane.showMessageDialog(parentComponent, format(key + ".message", args), format(key + ".title", args), messageType);
836        }
837    
838        /** Show a localized message dialog.
839         * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
840         * no <code>Frame</code>, a default <code>Frame</code> is used
841         * @param key localization key to use for the title, the message and eventually the icon
842         * @param args formatting arguments for the message text
843         */
844        public void showMessageDialog(final Component parentComponent, final String key, final Object... args) {
845            JOptionPane.showMessageDialog(parentComponent, format(key + ".message", args), format(key + ".title", args), getMessageType(key));
846        }
847    
848        /** Get the message type for a dialog.
849         * @param dialogKey dialog key
850         * @return message type
851         */
852        public int getMessageType(final String dialogKey) {
853            final String typeText = getString(dialogKey + ".messageType");
854            if (typeText == null) {
855                return JOptionPane.PLAIN_MESSAGE;
856            }
857            if (!typeText.endsWith("_MESSAGE")) {
858                System.err.println("Warning: Illegal type value " + typeText + " for dialog " + dialogKey); // TODO: I18N/L10N
859                return JOptionPane.PLAIN_MESSAGE;
860            }
861            if ("PLAIN_MESSAGE".equals(typeText)) {
862                return JOptionPane.PLAIN_MESSAGE;
863            } else if ("QUESTION_MESSAGE".equals(typeText)) {
864                return JOptionPane.QUESTION_MESSAGE;
865            } else if ("WARNING_MESSAGE".equals(typeText)) {
866                return JOptionPane.WARNING_MESSAGE;
867            } else if ("INFORMATION_MESSAGE".equals(typeText)) {
868                return JOptionPane.INFORMATION_MESSAGE;
869            } else if ("ERROR_MESSAGE".equals(typeText)) {
870                return JOptionPane.ERROR_MESSAGE;
871            } else {
872                // key not known, try reflection.
873                try {
874                    final Field f = JOptionPane.class.getField(typeText);
875                    if (f.getType() == Integer.TYPE) {
876                        return f.getInt(null);
877                    }
878                } catch (final NoSuchFieldException e) {
879                    System.err.println("Warning: Field " + typeText + " not found in JOptionPane (dialog: " + dialogKey + ")."); // TODO: I18N/L10N
880                    e.printStackTrace();  //TODO
881                } catch (final IllegalAccessException e) {
882                    System.err.println("Warning: Field " + typeText + " not accessible in JOptionPane (dialog: " + dialogKey + ")."); // TODO: I18N/L10N
883                    e.printStackTrace();  //TODO
884                }
885                return JOptionPane.PLAIN_MESSAGE;
886            }
887        }
888    
889        /** Formats a message with parameters.
890         * It's a proxy method for using {@link MessageFormat}.
891         * @param key message key
892         * @param args parameters
893         * @return formatted String
894         * @see MessageFormat#format(String,Object...)
895         */
896        public String format(final String key, final Object... args) {
897            return MessageFormat.format(getString(key), args);
898        }
899    
900        /** Show a localized confirmation dialog which the user can suppress in future (remembering his choice).
901         * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
902         * no <code>Frame</code>, a default <code>Frame</code> is used
903         * @param optionType the option type
904         * @param messageType the type of message to be displayed
905         * @param key localization key to use for the title, the message and eventually the icon
906         * @param args formatting arguments for the message text
907         * @return an integer indicating the option selected by the user now or eventually in a previous choice
908         * @throws IllegalStateException if no preferences are associated
909         */
910        public int showOnetimeConfirmDialog(final Component parentComponent, final int optionType, final int messageType, final String key, final Object... args) throws IllegalStateException {
911            @NonNls String showString = getString(key + ".show");
912            if (showString == null) {
913                showString = getString(key + ".showDefault");
914            }
915            if (showString == null) {
916                showString = "true";
917            }
918            final boolean show = !"false".equalsIgnoreCase(showString); // undefined should be treated true
919            if (!show) {
920                try {
921                    return Integer.parseInt(getString(key + ".choice"));
922                } catch (final Exception ignore) {
923                    /* ignore, continue with dialog then. */
924                }
925            }
926            final JCheckBox dontShowAgain = new JCheckBox(getString("dialogDontShowAgain"), true);
927            final int result = JOptionPane.showConfirmDialog(parentComponent, new Object[] { format(key, args), dontShowAgain }, format(key + ".title", args), optionType, messageType);
928            if (!dontShowAgain.isSelected()) {
929                if (prefs.size() > 0) {
930                    prefs.get(0).put(key + ".show", "false");
931                    prefs.get(0).put(key + ".choice", Integer.toString(result));
932                } else {
933                    throw new IllegalStateException("Cannot store prefs for this dialog - no Preferences associated with this ActionFactory!");
934                }
935            }
936            return result;
937        }
938    
939        /** Show a localized message dialog which the user can suppress in future.
940         * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
941         * no <code>Frame</code>, a default <code>Frame</code> is used
942         * @param messageType the type of message to be displayed
943         * @param key localization key to use for the title, the message and eventually the icon
944         * @param args formatting arguments for the message text
945         * @throws IllegalStateException if no preferences are associated
946         */
947        public void showOnetimeMessageDialog(final Component parentComponent, final int messageType, final String key, final Object... args) throws IllegalStateException {
948            String showString = getString(key + ".show");
949            if (showString == null) {
950                showString = getString(key + ".showDefault");
951            }
952            if (showString == null) {
953                showString = "true";
954            }
955            final boolean show = !"false".equalsIgnoreCase(showString); // undefined should be treated true
956            if (show) {
957                final JCheckBox dontShowAgain = new JCheckBox(getString("dialogDontShowAgain"), true);
958                JOptionPane.showMessageDialog(parentComponent, new Object[] { format(key, args), dontShowAgain }, format(key + ".title", args), messageType);
959                if (!dontShowAgain.isSelected()) {
960                    if (prefs.size() > 0) {
961                        prefs.get(0).put(key + ".show", "false");
962                    } else {
963                        throw new IllegalStateException("Cannot store prefs for this dialog - no Preferences associated with this ActionFactory!");
964                    }
965                }
966            }
967        }
968    
969        /** Show a localized question dialog.
970         * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
971         * no <code>Frame</code>, a default <code>Frame</code> is used
972         * @param key localization key to use for the title, the message and eventually the icon
973         * @param args formatting arguments for the message text
974         * @return <code>true</code> if user confirmed, otherwise <code>false</code>
975         */
976        public boolean showQuestionDialog(final Component parentComponent, final String key, final Object... args) {
977            return showConfirmDialog(parentComponent, JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, key, args) == JOptionPane.YES_OPTION;
978        }
979    
980        /** Show a localized confirmation dialog.
981         * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
982         * no <code>Frame</code>, a default <code>Frame</code> is used
983         * @param optionType the option type
984         * @param messageType the type of message to be displayed
985         * @param key localization key to use for the title, the message and eventually the icon
986         * @param args formatting arguments for the message text
987         * @return an integer indicating the option selected by the user
988         */
989        public int showConfirmDialog(final Component parentComponent, final int optionType, final int messageType, final String key, final Object...  args) {
990            return JOptionPane.showConfirmDialog(parentComponent, format(key, args), format(key + ".title", args), optionType, messageType);
991        }
992    
993        /** Creates a label for a specified key.
994         * @param key Key to create label for
995         * @param args formatting arguments for the label text
996         * @return JLabel for key Key
997         * @note the label text will be the key if no String for the key was found
998         */
999        public JLabel createLabel(final String key, final Object... args) {
1000            return createLabel((Component) null, key, args);
1001        }
1002    
1003        /** Creates a label for a specified key and component.
1004         * @param component Component to associate label to (maybe <code>null</code>)
1005         * @param key Key to create label for
1006         * @param args formatting arguments for the label text
1007         * @return JLabel for key Key
1008         * @note the label text will be the key if no String for the key was found
1009         * @todo support icons
1010         * @todo support mnemonics
1011         * @todo alignments and textpositions
1012         */
1013        public JLabel createLabel(final Component component, final String key, final Object... args) {
1014            final String labelText = format(key, args);
1015            final JLabel label = new JLabel(labelText != null ? labelText : key);
1016            label.setLabelFor(component);
1017            return label;
1018        }
1019    
1020        /** Registers an ActionProvider with this ActionFactory.
1021         * @param actionProvider ActionProvider to register
1022         */
1023        public void addActionProvider(final ActionProvider actionProvider) {
1024            actionProviders.add(actionProvider);
1025        }
1026    
1027    } // class ActionFactory