EventHandler.java 22.7 KB
Newer Older
Tom Tromey committed
1 2 3 4 5 6 7 8 9
/* java.beans.EventHandler
   Copyright (C) 2004, 2005 Free Software Foundation, Inc.

This file is part of GNU Classpath.

GNU Classpath is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
10

Tom Tromey committed
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
GNU Classpath 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
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA.

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version. */


package java.beans;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * <p>EventHandler forms a bridge between dynamically created listeners and
 * arbitrary properties and methods.</p>
49
 *
Tom Tromey committed
50 51 52 53
 * <p>You can use this class to easily create listener implementations for
 * some basic interactions between an event source and its target. Using
 * the three static methods named <code>create</code> you can create
 * these listener implementations.</p>
54
 *
Tom Tromey committed
55
 * <p>See the documentation of each method for usage examples.</p>
56
 *
Tom Tromey committed
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
 * @author Jerry Quinn (jlquinn@optonline.net)
 * @author Robert Schuster (thebohemian@gmx.net)
 * @since 1.4
 */
public class EventHandler implements InvocationHandler
{
  // The name of the method that will be implemented.  If null, any method.
  private String listenerMethod;

  // The object to call action on.
  private Object target;

  // The name of the method or property setter in target.
  private String action;

  // The property to extract from an event passed to listenerMethod.
  private String property;

  // The target objects Class.
  private Class targetClass;
77

Tom Tromey committed
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
  // String class doesn't already have a capitalize routine.
  private String capitalize(String s)
  {
    return s.substring(0, 1).toUpperCase() + s.substring(1);
  }

  /**
   * Creates a new <code>EventHandler</code> instance.
   *
   * <p>Typical creation is done with the create method, not by knewing an
   * EventHandler.</p>
   *
   * <p>This constructs an EventHandler that will connect the method
   * listenerMethodName to target.action, extracting eventPropertyName from
   * the first argument of listenerMethodName. and sending it to action.</p>
93
   *
Tom Tromey committed
94
   * <p>Throws a <code>NullPointerException</code> if the <code>target</code>
95
   * argument is <code>null</code>.
Tom Tromey committed
96 97 98 99 100 101 102
   *
   * @param target Object that will perform the action.
   * @param action A property or method of the target.
   * @param eventPropertyName A readable property of the inbound event.
   * @param listenerMethodName The listener method name triggering the action.
   */
  public EventHandler(Object target, String action, String eventPropertyName,
103
                      String listenerMethodName)
Tom Tromey committed
104 105
  {
    this.target = target;
106

Tom Tromey committed
107 108 109 110
    // Retrieving the class is done for two reasons:
    // 1) The class object is needed very frequently in the invoke() method.
    // 2) The constructor should throw a NullPointerException if target is null.
    targetClass = target.getClass();
111 112 113

    this.action = action;       // Turn this into a method or do we wait till
                // runtime
Tom Tromey committed
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
    property = eventPropertyName;
    listenerMethod = listenerMethodName;
  }

  /**
   * Returns the event property name.
   */
  public String getEventPropertyName()
  {
    return property;
  }

  /**
   * Returns the listener's method name.
   */
  public String getListenerMethodName()
  {
    return listenerMethod;
  }

  /**
   * Returns the target object.
   */
  public Object getTarget()
  {
    return target;
  }

  /**
   * Returns the action method name.
   */
  public String getAction()
  {
    return action;
  }

  // Fetch a qualified property like a.b.c from object o.  The properties can
  // be boolean isProp or object getProp properties.
  //
  // Returns a length 2 array with the first entry containing the value
  // extracted from the property, and the second entry contains the class of
  // the method return type.
  //
  // We play this game because if the method returns a native type, the return
  // value will be a wrapper.  If we then take the type of the wrapper and use
  // it to locate the action method that takes the native type, it won't match.
  private Object[] getProperty(Object o, String prop)
  {
    // Isolate the first property name from a.b.c.
    int pos;
    String rest = null;
    if ((pos = prop.indexOf('.')) != -1)
      {
167 168
        rest = prop.substring(pos + 1);
        prop = prop.substring(0, pos);
Tom Tromey committed
169 170 171 172 173 174
      }

    // Find a method named getProp.  It could be isProp instead.
    Method getter;
    try
      {
175 176
        // Look for boolean property getter isProperty
        getter = o.getClass().getMethod("is" + capitalize(prop));
Tom Tromey committed
177 178 179 180 181
      }
    catch (NoSuchMethodException nsme1)
      {
        try {
          // Look for regular property getter getProperty
182
          getter = o.getClass().getMethod("get" + capitalize(prop));
Tom Tromey committed
183 184 185
        } catch(NoSuchMethodException nsme2) {
            try {
            // Finally look for a method of the name prop
186
            getter = o.getClass().getMethod(prop);
Tom Tromey committed
187 188 189 190 191 192 193 194
            } catch(NoSuchMethodException nsme3) {
                // Ok, give up with an intelligent hint for the user.
                throw new RuntimeException("Method not called: Could not find a property or method '" + prop
                        + "' in " + o.getClass() + " while following the property argument '" + property + "'.");
            }
        }
      }
    try {
195
      Object val = getter.invoke(o);
Tom Tromey committed
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211

      if (rest != null)
        return getProperty(val, rest);

      return new Object[] {val, getter.getReturnType()};
    } catch(InvocationTargetException ite) {
        throw new RuntimeException("Method not called: Property or method '" + prop + "' has thrown an exception.", ite);
    } catch(IllegalAccessException iae) {
        // This cannot happen because we looked up method with Class.getMethod()
        // which returns public methods only.
        throw (InternalError) new InternalError("Non-public method was invoked.").initCause(iae);
    }
  }

  /**
   * Invokes the <code>EventHandler</code>.
212
   *
Tom Tromey committed
213
   * <p>This method is normally called by the listener's proxy implementation.</p>
214
   *
Tom Tromey committed
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
   * @param proxy The listener interface that is implemented using
   * the proxy mechanism.
   * @param method The method that was called on the proxy instance.
   * @param arguments The arguments which where given to the method.
   * @throws Throwable <code>NoSuchMethodException</code> is thrown when the EventHandler's
   * action method or property cannot be found.
   */
  public Object invoke(Object proxy, Method method, Object[] arguments)
  {
      try {
      // The method instance of the target object. We have to find out which
      // one we have to invoke.
      Method actionMethod = null;

    // Listener methods that weren't specified are ignored.  If listenerMethod
    // is null, then all listener methods are processed.
    if (listenerMethod != null && !method.getName().equals(listenerMethod))
      return null;

    // If a property is defined we definitely need a valid object at
    // arguments[0] that can be used to retrieve a value to which the
    // property of the target gets set.
    if(property != null) {
      // Extracts the argument. We will let it fail with a NullPointerException
      // the caller used a listener method that has no arguments.
      Object event = arguments[0];

      // Obtains the property XXX propertyType keeps showing up null - why?
      // because the object inside getProperty changes, but the ref variable
      // can't change this way, dolt!  need a better way to get both values out
      // - need method and object to do the invoke and get return type
      Object v[] = getProperty(event, property);
      Object[] args = new Object[] { v[0] };
248

Tom Tromey committed
249 250 251
      // Changes the class array that controls which method signature we are going
      // to look up in the target object.
      Class[] argTypes = new Class[] { initClass((Class) v[1]) };
252

Tom Tromey committed
253 254 255 256 257 258 259 260 261 262 263 264 265
      // Tries to  find a setter method to which we can apply the
      while(argTypes[0] != null) {
      try
      {
        // Look for a property setter for action.
        actionMethod = targetClass.getMethod("set" + capitalize(action), argTypes);

        return actionMethod.invoke(target, args);
      }
    catch (NoSuchMethodException e)
      {
        // If action as property didn't work, try as method later.
      }
266

Tom Tromey committed
267 268
      argTypes[0] = nextClass(argTypes[0]);
      }
269

Tom Tromey committed
270 271
      // We could not find a suitable setter method. Now we try again interpreting
      // action as the method name itself.
272
      // Since we probably have changed the block local argTypes array
Tom Tromey committed
273 274
      // we need to rebuild it.
      argTypes = new Class[] { initClass((Class) v[1]) };
275

Tom Tromey committed
276 277 278 279 280 281 282 283 284 285 286
      // Tries to  find a setter method to which we can apply the
      while(argTypes[0] != null) {
        try
        {
          actionMethod = targetClass.getMethod(action, argTypes);

          return actionMethod.invoke(target, args);
        }
        catch (NoSuchMethodException e)
        {
        }
287

Tom Tromey committed
288 289
        argTypes[0] = nextClass(argTypes[0]);
      }
290

Tom Tromey committed
291 292 293
        throw new RuntimeException("Method not called: Could not find a public method named '"
                + action + "' in target " + targetClass + " which takes a '"
                + v[1] + "' argument or a property of this type.");
294 295
      }

Tom Tromey committed
296 297 298 299 300 301 302 303 304
    // If property was null we will search for a no-argument method here.
    // Note: The ordering of method lookups is important because we want to prefer no-argument
    // calls like the JDK does. This means if we have actionMethod() and actionMethod(Event) we will
    // call the first *EVEN* if we have a valid argument for the second method. This is behavior compliant
    // to the JDK.
    // If actionMethod() is not available but there is a actionMethod(Event) we take this. That makes us
    // more specification compliant than the JDK itself because this one will fail in such a case.
    try
      {
305
      actionMethod = targetClass.getMethod(action);
Tom Tromey committed
306 307 308 309 310 311 312 313 314
      }
    catch(NoSuchMethodException nsme)
      {
        // Note: If we want to be really strict the specification says that a no-argument method should
        // accept an EventObject (or subclass I guess). However since the official implementation is broken
        // anyways, it's more flexible without the EventObject restriction and we are compatible on everything
        // else this can stay this way.
        if(arguments != null && arguments.length >= 1/* && arguments[0] instanceof EventObject*/) {
            Class[] targetArgTypes = new Class[] { initClass(arguments[0].getClass()) };
315

Tom Tromey committed
316 317 318 319 320
            while(targetArgTypes[0] != null) {
                try
                {
                  // If no property exists we expect the first element of the arguments to be
                  // an EventObject which is then applied to the target method.
321

Tom Tromey committed
322
                  actionMethod = targetClass.getMethod(action, targetArgTypes);
323

Tom Tromey committed
324 325 326 327
                  return actionMethod.invoke(target, new Object[] { arguments[0] });
                }
                catch(NoSuchMethodException nsme2)
                {
328

Tom Tromey committed
329
                }
330

Tom Tromey committed
331 332
                targetArgTypes[0] = nextClass(targetArgTypes[0]);
            }
333

Tom Tromey committed
334 335 336 337 338 339 340
        }
      }

    // If we do not have a Method instance at this point this means that all our tries
    // failed. The JDK throws an ArrayIndexOutOfBoundsException in this case.
    if(actionMethod == null)
      throw new ArrayIndexOutOfBoundsException(0);
341

Tom Tromey committed
342
    // Invoke target.action(property)
343
    return actionMethod.invoke(target);
Tom Tromey committed
344 345 346 347 348 349 350 351 352
      } catch(InvocationTargetException ite) {
         throw new RuntimeException(ite.getCause());
      } catch(IllegalAccessException iae) {
          // Cannot happen because we always use getMethod() which returns public
          // methods only. Otherwise there is something seriously broken in
          // GNU Classpath.
          throw (InternalError) new InternalError("Non-public method was invoked.").initCause(iae);
      }
  }
353

Tom Tromey committed
354 355 356
  /**
   * <p>Returns the primitive type for every wrapper class or the
   * class itself if it is no wrapper class.</p>
357
   *
Tom Tromey committed
358 359 360 361 362 363
   * <p>This is needed because to be able to find both kinds of methods:
   * One that takes a wrapper class as the first argument and one that
   * accepts a primitive instead.</p>
   */
  private Class initClass(Class klass) {
   if(klass == Boolean.class) {
364
    return Boolean.TYPE;
Tom Tromey committed
365
   } else if(klass == Byte.class) {
366
    return Byte.TYPE;
Tom Tromey committed
367
   } else if(klass == Short.class) {
368
    return Short.TYPE;
Tom Tromey committed
369
   } else if(klass == Integer.class) {
370
    return Integer.TYPE;
Tom Tromey committed
371
   } else if(klass == Long.class) {
372
    return Long.TYPE;
Tom Tromey committed
373
   } else if(klass == Float.class) {
374
    return Float.TYPE;
Tom Tromey committed
375
   } else if(klass == Double.class) {
376
    return Double.TYPE;
Tom Tromey committed
377
   } else {
378
    return klass;
Tom Tromey committed
379 380 381 382
   }
  }

  /**
383 384
   *
   *
Tom Tromey committed
385 386 387 388 389
   * @param klass
   * @return
   */
  private Class nextClass(Class klass) {
    if(klass == Boolean.TYPE) {
390
    return Boolean.class;
Tom Tromey committed
391
   } else if(klass == Byte.TYPE) {
392
    return Byte.class;
Tom Tromey committed
393
   } else if(klass == Short.TYPE) {
394
    return Short.class;
Tom Tromey committed
395
   } else if(klass == Integer.TYPE) {
396
    return Integer.class;
Tom Tromey committed
397
   } else if(klass == Long.TYPE) {
398
    return Long.class;
Tom Tromey committed
399
   } else if(klass == Float.TYPE) {
400
    return Float.class;
Tom Tromey committed
401
   } else if(klass == Double.TYPE) {
402
    return Double.class;
Tom Tromey committed
403 404 405 406
   } else {
    return klass.getSuperclass();
   }
   }
407

Tom Tromey committed
408 409 410
  /**
   * <p>Constructs an implementation of <code>listenerInterface</code>
   * to dispatch events.</p>
411
   *
Tom Tromey committed
412 413 414
   * <p>You can use such an implementation to simply call a public
   * no-argument method of an arbitrary target object or to forward
   * the first argument of the listener method to the target method.</p>
415
   *
Tom Tromey committed
416 417 418 419 420
   * <p>Call this method like:</p>
   * <code>
   * button.addActionListener((ActionListener)
   *    EventHandler.create(ActionListener.class, target, "dispose"));
   * </code>
421
   *
Tom Tromey committed
422 423 424 425 426 427 428 429
   * <p>to achieve the following behavior:</p>
   * <code>
   * button.addActionListener(new ActionListener() {
   *    public void actionPerformed(ActionEvent ae) {
   *        target.dispose();
   *    }
   * });
   * </code>
430
   *
Tom Tromey committed
431 432 433
   * <p>That means if you need a listener implementation that simply calls a
   * a no-argument method on a given instance for <strong>each</strong>
   * method of the listener interface.</p>
434
   *
Tom Tromey committed
435 436 437 438 439 440 441 442 443 444 445 446
   * <p>Note: The <code>action</code> is interpreted as a method name. If your target object
   * has no no-argument method of the given name the EventHandler tries to find
   * a method with the same name but which can accept the first argument of the
   * listener method. Usually this will be an event object but any other object
   * will be forwarded, too. Keep in mind that using a property name instead of a
   * real method here is wrong and will throw an <code>ArrayIndexOutOfBoundsException</code>
   * whenever one of the listener methods is called.<p/>
   *
   * <p>The <code>EventHandler</code> will automatically convert primitives
   * to their wrapper class and vice versa. Furthermore it will call
   * a target method if it accepts a superclass of the type of the
   * first argument of the listener method.</p>
447
   *
Tom Tromey committed
448 449 450
   * <p>In case that the method of the target object throws an exception
   * it will be wrapped in a <code>RuntimeException</code> and thrown out
   * of the listener method.</p>
451
   *
Tom Tromey committed
452 453 454
   * <p>In case that the method of the target object cannot be found an
   * <code>ArrayIndexOutOfBoundsException</code> will be thrown when the
   * listener method is invoked.</p>
455
   *
Tom Tromey committed
456 457 458 459 460 461 462 463
   * <p>A call to this method is equivalent to:
   * <code>create(listenerInterface, target, action, null, null)</code></p>
   *
   * @param listenerInterface Listener interface to implement.
   * @param target Object to invoke action on.
   * @param action Target property or method to invoke.
   * @return A constructed proxy object.
   */
464
  public static <T> T create(Class<T> listenerInterface, Object target,
465
                             String action)
Tom Tromey committed
466 467 468 469 470 471 472 473 474 475 476 477 478
  {
    return create(listenerInterface, target, action, null, null);
  }

  /**
   * <p>Constructs an implementation of <code>listenerInterface</code>
   * to dispatch events.</p>
   *
   * <p>Use this method if you want to create an implementation that retrieves
   * a property value from the <b>first</b> argument of the listener method
   * and applies it to the target's property or method. This first argument
   * of the listener is usually an event object but any other object is
   * valid, too.</p>
479
   *
Tom Tromey committed
480 481 482 483
   * <p>You can set the value of <code>eventPropertyName</code> to "prop"
   * to denote the retrieval of a property named "prop" from the event
   * object. In case that no such property exists the <code>EventHandler</code>
   * will try to find a method with that name.</p>
484
   *
Tom Tromey committed
485 486 487 488 489 490
   * <p>If you set <code>eventPropertyName</code> to a value like this "a.b.c"
   * <code>EventHandler</code> will recursively evaluate the properties "a", "b"
   * and "c". Again if no property can be found the <code>EventHandler</code>
   * tries a method name instead. This allows mixing the names, too: "a.toString"
   * will retrieve the property "a" from the event object and will then call
   * the method "toString" on it.</p>
491
   *
Tom Tromey committed
492 493 494
   * <p>An exception thrown in any of these methods will provoke a
   * <code>RuntimeException</code> to be thrown which contains an
   * <code>InvocationTargetException</code> containing the triggering exception.</p>
495
   *
Tom Tromey committed
496 497 498
   * <p>If you set <code>eventPropertyName</code> to a non-null value the
   * <code>action</code> parameter will be interpreted as a property name
   * or a method name of the target object.</p>
499
   *
Tom Tromey committed
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
   * <p>Any object retrieved from the event object and applied to the
   * target will converted from primitives to their wrapper class or
   * vice versa or applied to a method that accepts a superclass
   * of the object.</p>
   *
   * <p>Examples:</p>
   * <p>The following code:</p><code>
   * button.addActionListener(
   *    new ActionListener() {
   *        public void actionPerformed(ActionEvent ae) {
   *            Object o = ae.getSource().getClass().getName();
   *            textField.setText((String) o);
   *        }
   *    });
   * </code>
515
   *
Tom Tromey committed
516 517 518 519 520 521
   * <p>Can be expressed using the <code>EventHandler</code> like this:</p>
   * <p>
   * <code>button.addActionListener((ActionListener)
   *    EventHandler.create(ActionListener.class, textField, "text", "source.class.name");
   * <code>
   * </p>
522
   *
Tom Tromey committed
523 524 525 526 527 528
   * <p>As said above you can specify the target as a method, too:</p>
   * <p>
   * <code>button.addActionListener((ActionListener)
   *    EventHandler.create(ActionListener.class, textField, "setText", "source.class.name");
   * <code>
   * </p>
529
   *
Tom Tromey committed
530 531 532 533 534 535
   * <p>Furthermore you can use method names in the property:</p>
   * <p>
   * <code>button.addActionListener((ActionListener)
   *    EventHandler.create(ActionListener.class, textField, "setText", "getSource.getClass.getName");
   * <code>
   * </p>
536
   *
Tom Tromey committed
537 538 539 540 541 542
   * <p>Finally you can mix names:</p>
   * <p>
   * <code>button.addActionListener((ActionListener)
   *    EventHandler.create(ActionListener.class, textField, "setText", "source.getClass.name");
   * <code>
   * </p>
543
   *
Tom Tromey committed
544 545 546 547 548 549 550 551 552 553
   * <p>A call to this method is equivalent to:
   * <code>create(listenerInterface, target, action, null, null)</code>
   * </p>
   *
   * @param listenerInterface Listener interface to implement.
   * @param target Object to invoke action on.
   * @param action Target property or method to invoke.
   * @param eventPropertyName Name of property to extract from event.
   * @return A constructed proxy object.
   */
554
  public static <T> T create(Class<T> listenerInterface, Object target,
555
                             String action, String eventPropertyName)
Tom Tromey committed
556 557 558 559 560 561 562 563 564 565 566 567 568
  {
    return create(listenerInterface, target, action, eventPropertyName, null);
  }

  /**
   * <p>Constructs an implementation of <code>listenerInterface</code>
   * to dispatch events.</p>
   *
   * <p>Besides the functionality described for {@link create(Class, Object, String)}
   * and {@link create(Class, Object, String, String)} this method allows you
   * to filter the listener method that should have an effect. Look at these
   * method's documentation for more information about the <code>EventHandler</code>'s
   * usage.</p>
569
   *
Tom Tromey committed
570 571 572 573 574 575 576 577
   * <p>If you want to call <code>dispose</code> on a <code>JFrame</code> instance
   * when the <code>WindowListener.windowClosing()</code> method was invoked use
   * the following code:</p>
   * <p>
   * <code>
   * EventHandler.create(WindowListener.class, jframeInstance, "dispose", null, "windowClosing");
   * </code>
   * </p>
578
   *
Tom Tromey committed
579 580
   * <p>A <code>NullPointerException</code> is thrown if the <code>listenerInterface</code>
   * or <code>target</code> argument are <code>null</code>.
581
   *
Tom Tromey committed
582 583 584 585 586 587 588
   * @param listenerInterface Listener interface to implement.
   * @param target Object to invoke action on.
   * @param action Target method name to invoke.
   * @param eventPropertyName Name of property to extract from event.
   * @param listenerMethodName Listener method to implement.
   * @return A constructed proxy object.
   */
589
  public static <T> T create(Class<T> listenerInterface, Object target,
590 591
                             String action, String eventPropertyName,
                             String listenerMethodName)
Tom Tromey committed
592 593 594
  {
    // Create EventHandler instance
    EventHandler eh = new EventHandler(target, action, eventPropertyName,
595
                                       listenerMethodName);
Tom Tromey committed
596 597 598

    // Create proxy object passing in the event handler
    Object proxy = Proxy.newProxyInstance(listenerInterface.getClassLoader(),
599 600
                                          new Class<?>[] {listenerInterface},
                                          eh);
Tom Tromey committed
601

602
    return (T) proxy;
Tom Tromey committed
603 604
  }
}