[ Team LiB ] Previous Section Next Section

9.2 Invoking a Named Method

Example 9-2 defines the Command class, which demonstrates another use of the Reflection API. A Command object encapsulates a Method object, an object on which the method is to be invoked, and an array of arguments to pass to the method. The invoke( ) method invokes the method on the specified object using the specified arguments. The actionPerformed( ) method does the same thing. If you've read Chapter 11, you know that this method implements the java.awt.event.ActionListener interface, which means that Command objects can be used as action listeners to respond to button presses, menu selections, and other events within a graphical user interface (GUI). GUI programs typically create a slew of ActionListener implementations to handle events. With the Command class, simple action listeners can be defined without creating lots of new classes. (The Command class also implements the InvocationHandler interface, which we'll learn about in the next section.)

The most useful feature (and the most complicated code) in the Command class is the parse( ) method, which parses a string that contains a method name and list of arguments to create a Command object. This is useful because it allows Command objects to be read from configuration files, for example. We'll use this feature of the Command class in Chapter 11. Note that the parse( ) method uses the Tokenizer interface and CharSequenceTokenizer class defined in Chapter 2.

Java does not allow methods to be passed directly as data values, but the Reflection API makes it possible for methods passed by name to be invoked indirectly. Note that this technique is not particularly efficient. For asynchronous event handling in a GUI, though, it is certainly efficient enough: indirect method invocation through the Reflection API is much faster than the response time required by the limits of human perception. Invoking a method by name is not an appropriate technique, however, when repetitive calls are required or when the computer is not waiting for human input. Thus, you should not use this technique for passing a comparison method to a sorting routine or passing a filename filter to a directory listing method, for example. In cases like these, you should use the standard technique of implementing a class that contains the desired method and passing an instance of the class to the appropriate routine.

Example 9-2. Command.java
package je3.reflect;
import java.awt.event.*;
import java.beans.*;
import java.lang.reflect.*;
import java.io.*;
import java.util.*;
import je3.classes.Tokenizer;
import je3.classes.CharSequenceTokenizer;

/**
 * This class represents a Method, the list of arguments to be passed
 * to that method, and the object on which the method is to be invoked.
 * The invoke( ) method invokes the method.  The actionPerformed( ) method
 * does the same thing, allowing this class to implement ActionListener
 * and be used to respond to ActionEvents generated in a GUI or elsewhere.
 * The static parse( ) method parses a string representation of a method
 * and its arguments.
 **/
public class Command implements ActionListener, InvocationHandler {
    Method m;       // The method to be invoked
    Object target;  // The object to invoke it on
    Object[  ] args;  // The arguments to pass to the method

    // An empty array; used for methods with no arguments at all.
    static final Object[  ] nullargs = new Object[  ] {  };

    /** This constructor creates a Command object for a no-arg method */
    public Command(Object target, Method m) { this(target, m, nullargs); }

    /**
     * This constructor creates a Command object for a method that takes the
     * specified array of arguments.  Note that the parse( ) method provides
     * another way to create a Command object
     **/
    public Command(Object target, Method m, Object[  ] args) {
        this.target = target;
        this.m = m;
        if (args == null) args = nullargs;
        this.args = args;
    }

    /**
     * This construct specifies the method to call by name.  It looks for a
     * method of the target object with the specified name and specified number
     * of arguments.  It does not attempt to verify the types of the arguments,
     * since wrapper object (Integer, Boolean, etc.) in the args[  ] array could
     * represent reference or primitive arguments.
     */
    public Command(Object target, String methodName, Object[  ] args) {
        this.target = target;
        if (args == null) args = nullargs;
        this.args = args;

        Method[  ] methods = target.getClass( ).getMethods( );
        for(int i = 0; i < methods.length; i++) {
            Method m = methods[i];
            if (m.getParameterTypes( ).length == args.length &&
                m.getName( ).equals(methodName)) {
                this.m = m;
                break;
            }
        }

        // If we didn't find a method, throw an exceptoin
        if (this.m == null)
            throw new IllegalArgumentException("Unknown method " + methodName);

    }

    /**
     * Invoke the Command by calling the method on its target and passing
     * the arguments.  See also actionPerformed( ), which does not throw the
     * checked exceptions that this method does.
     **/
    public void invoke( )
        throws IllegalAccessException, InvocationTargetException
    {
        m.invoke(target, args);  // Use reflection to invoke the method
    }

    /**
     * This method implements the ActionListener interface.  It is like
     * invoke( ) except that it catches the exceptions thrown by that method
     * and rethrows them as an unchecked RuntimeException
     **/
    public void actionPerformed(ActionEvent e) {
        try {
            invoke( );                          // Call the invoke method
        }
        catch (InvocationTargetException ex) { // But convert to unchecked
            throw new RuntimeException(ex);    // exceptions.  Note that we
        }                                      // chain to the original
        catch (IllegalAccessException ex) {    // exception.
            throw new RuntimeException(ex);
        }
    }

    /**
     * This method implements the InvocationHandler interface, so that a
     * Command object can be used with Proxy objects.  Note that it simply
     * calls the no-argument invoke( ) method, ignoring its arguments and
     * returning null. This means that it is only useful for proxying
     * interfaces that define a single no-arg void method.
     **/
    public Object invoke(Object p, Method m, Object[  ] a) throws Throwable {
        invoke( );
        return null;
    }

    /**
     * This static method creates a Command using the specified target object
     * and the specified string.  The string should contain the method name
     * followed by an optional parenthesized comma-separated argument list and
     * a semicolon.  The arguments may be boolean, integer or double literals,
     * or double-quoted strings.  The parser is lenient about missing commas,
     * semicolons, and quotes, but throws an IOException if it cannot parse the
     * string.
     **/
    public static Command parse(Object target, String text) throws IOException
    {
        String methodname;                 // The name of the method
        ArrayList args = new ArrayList( );  // Hold arguments as we parse them.
        ArrayList types = new ArrayList( ); // Hold argument types.

        Tokenizer t = new CharSequenceTokenizer(text);
        t.skipSpaces(true).tokenizeWords(true).tokenizeNumbers(true);
        t.quotes("\"'", "\"'");

        if (t.next( ) != Tokenizer.WORD)
            throw new IOException("Missing method name for command");
        methodname = t.tokenText( );
        t.next( );
        if (t.tokenType( ) == '(') {
            t.next( );
            for(;;) { // Loop through all arguments
                int c = t.tokenType( );
                if (c == ')') {
                    // Consume closing paren
                    t.next( );
                    break;  // and break out of list
                }

                if (c == Tokenizer.WORD) {
                    String word = t.tokenText( );
                    if (word.equals("true")) {
                        args.add(Boolean.TRUE);
                        types.add(boolean.class);
                    }
                    else if (word.equals("false")) {
                        args.add(Boolean.FALSE);
                        types.add(boolean.class);
                    }
                    else {  // Treat unquoted identifiers as strings...
                        args.add(word);
                        types.add(String.class);
                    }
                }
                else if (c == '"') {  // double-quoted string
                    args.add(t.tokenText( ));
                    types.add(String.class);
                }
                else if (c == '\'') { // single-quoted character
                    args.add(new Character(t.tokenText( ).charAt(0)));
                    types.add(char.class);
                }
                else if (c == Tokenizer.NUMBER) {  // An integer
                    args.add(new Integer(Integer.parseInt(t.tokenText( ))));
                    types.add(int.class);
                }
                else {  // Anything else is a syntax error
                    throw new IOException("Unexpected token " + t.tokenText( ) +
                                          " in argument list of " +
                                          methodname + "( ).");
                }

                // Consume the token we just parsed, and then consume an
                // optional comma.
                if (t.next( ) == ',') t.next( );
            }
        }

        // Consume optional semicolon after method name or argument list
        if (t.tokenType( ) == ';') t.next( );


        // We've parsed the argument list.
        // Next, convert the lists of argument values and types to arrays
        Object[  ] argValues = args.toArray( );
        Class[  ] argtypes = (Class[  ])types.toArray(new Class[argValues.length]);

        // At this point, we've got a method name and arrays of argument
        // values and types.  Use reflection on the class of the target object
        // to find a method with the given name and argument types.  Throw
        // an exception if we can't find the named method.
        Method method;
        try { method = target.getClass( ).getMethod(methodname, argtypes); }
        catch (Exception e) {
            throw new IOException("No such method found, or wrong argument " +
                                  "types: " + methodname);
        }

        // Finally, create and return a Command object, using the target object
        // passed to this method, the Method object we obtained above, and
        // the array of argument values we parsed from the string.
        return new Command(target, method, argValues);
    }


    /**
     * This simple program demonstrates how a Command object can be parsed from
     * a string and used as an ActionListener object in a Swing application.
     **/
    static class Test {
        public static void main(String[  ] args) throws IOException {
            javax.swing.JFrame f = new javax.swing.JFrame("Command Test");
            javax.swing.JButton b1 = new javax.swing.JButton("Tick");
            javax.swing.JButton b2 = new javax.swing.JButton("Tock");
            javax.swing.JLabel label = new javax.swing.JLabel("Hello world");
            java.awt.Container pane = f.getContentPane( );

            pane.add(b1, java.awt.BorderLayout.WEST);
            pane.add(b2, java.awt.BorderLayout.EAST);
            pane.add(label, java.awt.BorderLayout.NORTH);

            b1.addActionListener(Command.parse(label, "setText(\"tick\");"));
            b2.addActionListener(Command.parse(label, "setText(\"tock\");"));

            f.pack( );
            f.show( );
        }
    }
}
    [ Team LiB ] Previous Section Next Section