Source for java.beans.EventHandler

   1: /* java.beans.EventHandler
   2:    Copyright (C) 2004, 2005 Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10: 
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: 
  39: package java.beans;
  40: 
  41: import java.lang.reflect.InvocationHandler;
  42: import java.lang.reflect.InvocationTargetException;
  43: import java.lang.reflect.Method;
  44: import java.lang.reflect.Proxy;
  45: 
  46: /**
  47:  * <p>EventHandler forms a bridge between dynamically created listeners and
  48:  * arbitrary properties and methods.</p>
  49:  *
  50:  * <p>You can use this class to easily create listener implementations for
  51:  * some basic interactions between an event source and its target. Using
  52:  * the three static methods named <code>create</code> you can create
  53:  * these listener implementations.</p>
  54:  *
  55:  * <p>See the documentation of each method for usage examples.</p>
  56:  *
  57:  * @author Jerry Quinn (jlquinn@optonline.net)
  58:  * @author Robert Schuster (thebohemian@gmx.net)
  59:  * @since 1.4
  60:  */
  61: public class EventHandler implements InvocationHandler
  62: {
  63:   // The name of the method that will be implemented.  If null, any method.
  64:   private String listenerMethod;
  65: 
  66:   // The object to call action on.
  67:   private Object target;
  68: 
  69:   // The name of the method or property setter in target.
  70:   private String action;
  71: 
  72:   // The property to extract from an event passed to listenerMethod.
  73:   private String property;
  74: 
  75:   // The target objects Class.
  76:   private Class targetClass;
  77: 
  78:   // String class doesn't already have a capitalize routine.
  79:   private String capitalize(String s)
  80:   {
  81:     return s.substring(0, 1).toUpperCase() + s.substring(1);
  82:   }
  83: 
  84:   /**
  85:    * Creates a new <code>EventHandler</code> instance.
  86:    *
  87:    * <p>Typical creation is done with the create method, not by knewing an
  88:    * EventHandler.</p>
  89:    *
  90:    * <p>This constructs an EventHandler that will connect the method
  91:    * listenerMethodName to target.action, extracting eventPropertyName from
  92:    * the first argument of listenerMethodName. and sending it to action.</p>
  93:    *
  94:    * <p>Throws a <code>NullPointerException</code> if the <code>target</code>
  95:    * argument is <code>null</code>.
  96:    *
  97:    * @param target Object that will perform the action.
  98:    * @param action A property or method of the target.
  99:    * @param eventPropertyName A readable property of the inbound event.
 100:    * @param listenerMethodName The listener method name triggering the action.
 101:    */
 102:   public EventHandler(Object target, String action, String eventPropertyName,
 103:                       String listenerMethodName)
 104:   {
 105:     this.target = target;
 106: 
 107:     // Retrieving the class is done for two reasons:
 108:     // 1) The class object is needed very frequently in the invoke() method.
 109:     // 2) The constructor should throw a NullPointerException if target is null.
 110:     targetClass = target.getClass();
 111: 
 112:     this.action = action;       // Turn this into a method or do we wait till
 113:                 // runtime
 114:     property = eventPropertyName;
 115:     listenerMethod = listenerMethodName;
 116:   }
 117: 
 118:   /**
 119:    * Returns the event property name.
 120:    */
 121:   public String getEventPropertyName()
 122:   {
 123:     return property;
 124:   }
 125: 
 126:   /**
 127:    * Returns the listener's method name.
 128:    */
 129:   public String getListenerMethodName()
 130:   {
 131:     return listenerMethod;
 132:   }
 133: 
 134:   /**
 135:    * Returns the target object.
 136:    */
 137:   public Object getTarget()
 138:   {
 139:     return target;
 140:   }
 141: 
 142:   /**
 143:    * Returns the action method name.
 144:    */
 145:   public String getAction()
 146:   {
 147:     return action;
 148:   }
 149: 
 150:   // Fetch a qualified property like a.b.c from object o.  The properties can
 151:   // be boolean isProp or object getProp properties.
 152:   //
 153:   // Returns a length 2 array with the first entry containing the value
 154:   // extracted from the property, and the second entry contains the class of
 155:   // the method return type.
 156:   //
 157:   // We play this game because if the method returns a native type, the return
 158:   // value will be a wrapper.  If we then take the type of the wrapper and use
 159:   // it to locate the action method that takes the native type, it won't match.
 160:   private Object[] getProperty(Object o, String prop)
 161:   {
 162:     // Isolate the first property name from a.b.c.
 163:     int pos;
 164:     String rest = null;
 165:     if ((pos = prop.indexOf('.')) != -1)
 166:       {
 167:         rest = prop.substring(pos + 1);
 168:         prop = prop.substring(0, pos);
 169:       }
 170: 
 171:     // Find a method named getProp.  It could be isProp instead.
 172:     Method getter;
 173:     try
 174:       {
 175:         // Look for boolean property getter isProperty
 176:         getter = o.getClass().getMethod("is" + capitalize(prop));
 177:       }
 178:     catch (NoSuchMethodException nsme1)
 179:       {
 180:         try {
 181:           // Look for regular property getter getProperty
 182:           getter = o.getClass().getMethod("get" + capitalize(prop));
 183:         } catch(NoSuchMethodException nsme2) {
 184:             try {
 185:             // Finally look for a method of the name prop
 186:             getter = o.getClass().getMethod(prop);
 187:             } catch(NoSuchMethodException nsme3) {
 188:                 // Ok, give up with an intelligent hint for the user.
 189:                 throw new RuntimeException("Method not called: Could not find a property or method '" + prop
 190:                         + "' in " + o.getClass() + " while following the property argument '" + property + "'.");
 191:             }
 192:         }
 193:       }
 194:     try {
 195:       Object val = getter.invoke(o);
 196: 
 197:       if (rest != null)
 198:         return getProperty(val, rest);
 199: 
 200:       return new Object[] {val, getter.getReturnType()};
 201:     } catch(InvocationTargetException ite) {
 202:         throw new RuntimeException("Method not called: Property or method '" + prop + "' has thrown an exception.", ite);
 203:     } catch(IllegalAccessException iae) {
 204:         // This cannot happen because we looked up method with Class.getMethod()
 205:         // which returns public methods only.
 206:         throw (InternalError) new InternalError("Non-public method was invoked.").initCause(iae);
 207:     }
 208:   }
 209: 
 210:   /**
 211:    * Invokes the <code>EventHandler</code>.
 212:    *
 213:    * <p>This method is normally called by the listener's proxy implementation.</p>
 214:    *
 215:    * @param proxy The listener interface that is implemented using
 216:    * the proxy mechanism.
 217:    * @param method The method that was called on the proxy instance.
 218:    * @param arguments The arguments which where given to the method.
 219:    * @throws Throwable <code>NoSuchMethodException</code> is thrown when the EventHandler's
 220:    * action method or property cannot be found.
 221:    */
 222:   public Object invoke(Object proxy, Method method, Object[] arguments)
 223:   {
 224:       try {
 225:       // The method instance of the target object. We have to find out which
 226:       // one we have to invoke.
 227:       Method actionMethod = null;
 228: 
 229:     // Listener methods that weren't specified are ignored.  If listenerMethod
 230:     // is null, then all listener methods are processed.
 231:     if (listenerMethod != null && !method.getName().equals(listenerMethod))
 232:       return null;
 233: 
 234:     // If a property is defined we definitely need a valid object at
 235:     // arguments[0] that can be used to retrieve a value to which the
 236:     // property of the target gets set.
 237:     if(property != null) {
 238:       // Extracts the argument. We will let it fail with a NullPointerException
 239:       // the caller used a listener method that has no arguments.
 240:       Object event = arguments[0];
 241: 
 242:       // Obtains the property XXX propertyType keeps showing up null - why?
 243:       // because the object inside getProperty changes, but the ref variable
 244:       // can't change this way, dolt!  need a better way to get both values out
 245:       // - need method and object to do the invoke and get return type
 246:       Object v[] = getProperty(event, property);
 247:       Object[] args = new Object[] { v[0] };
 248: 
 249:       // Changes the class array that controls which method signature we are going
 250:       // to look up in the target object.
 251:       Class[] argTypes = new Class[] { initClass((Class) v[1]) };
 252: 
 253:       // Tries to  find a setter method to which we can apply the
 254:       while(argTypes[0] != null) {
 255:       try
 256:       {
 257:         // Look for a property setter for action.
 258:         actionMethod = targetClass.getMethod("set" + capitalize(action), argTypes);
 259: 
 260:         return actionMethod.invoke(target, args);
 261:       }
 262:     catch (NoSuchMethodException e)
 263:       {
 264:         // If action as property didn't work, try as method later.
 265:       }
 266: 
 267:       argTypes[0] = nextClass(argTypes[0]);
 268:       }
 269: 
 270:       // We could not find a suitable setter method. Now we try again interpreting
 271:       // action as the method name itself.
 272:       // Since we probably have changed the block local argTypes array
 273:       // we need to rebuild it.
 274:       argTypes = new Class[] { initClass((Class) v[1]) };
 275: 
 276:       // Tries to  find a setter method to which we can apply the
 277:       while(argTypes[0] != null) {
 278:         try
 279:         {
 280:           actionMethod = targetClass.getMethod(action, argTypes);
 281: 
 282:           return actionMethod.invoke(target, args);
 283:         }
 284:         catch (NoSuchMethodException e)
 285:         {
 286:         }
 287: 
 288:         argTypes[0] = nextClass(argTypes[0]);
 289:       }
 290: 
 291:         throw new RuntimeException("Method not called: Could not find a public method named '"
 292:                 + action + "' in target " + targetClass + " which takes a '"
 293:                 + v[1] + "' argument or a property of this type.");
 294:       }
 295: 
 296:     // If property was null we will search for a no-argument method here.
 297:     // Note: The ordering of method lookups is important because we want to prefer no-argument
 298:     // calls like the JDK does. This means if we have actionMethod() and actionMethod(Event) we will
 299:     // call the first *EVEN* if we have a valid argument for the second method. This is behavior compliant
 300:     // to the JDK.
 301:     // If actionMethod() is not available but there is a actionMethod(Event) we take this. That makes us
 302:     // more specification compliant than the JDK itself because this one will fail in such a case.
 303:     try
 304:       {
 305:       actionMethod = targetClass.getMethod(action);
 306:       }
 307:     catch(NoSuchMethodException nsme)
 308:       {
 309:         // Note: If we want to be really strict the specification says that a no-argument method should
 310:         // accept an EventObject (or subclass I guess). However since the official implementation is broken
 311:         // anyways, it's more flexible without the EventObject restriction and we are compatible on everything
 312:         // else this can stay this way.
 313:         if(arguments != null && arguments.length >= 1/* && arguments[0] instanceof EventObject*/) {
 314:             Class[] targetArgTypes = new Class[] { initClass(arguments[0].getClass()) };
 315: 
 316:             while(targetArgTypes[0] != null) {
 317:                 try
 318:                 {
 319:                   // If no property exists we expect the first element of the arguments to be
 320:                   // an EventObject which is then applied to the target method.
 321: 
 322:                   actionMethod = targetClass.getMethod(action, targetArgTypes);
 323: 
 324:                   return actionMethod.invoke(target, new Object[] { arguments[0] });
 325:                 }
 326:                 catch(NoSuchMethodException nsme2)
 327:                 {
 328: 
 329:                 }
 330: 
 331:                 targetArgTypes[0] = nextClass(targetArgTypes[0]);
 332:             }
 333: 
 334:         }
 335:       }
 336: 
 337:     // If we do not have a Method instance at this point this means that all our tries
 338:     // failed. The JDK throws an ArrayIndexOutOfBoundsException in this case.
 339:     if(actionMethod == null)
 340:       throw new ArrayIndexOutOfBoundsException(0);
 341: 
 342:     // Invoke target.action(property)
 343:     return actionMethod.invoke(target);
 344:       } catch(InvocationTargetException ite) {
 345:          throw new RuntimeException(ite.getCause());
 346:       } catch(IllegalAccessException iae) {
 347:           // Cannot happen because we always use getMethod() which returns public
 348:           // methods only. Otherwise there is something seriously broken in
 349:           // GNU Classpath.
 350:           throw (InternalError) new InternalError("Non-public method was invoked.").initCause(iae);
 351:       }
 352:   }
 353: 
 354:   /**
 355:    * <p>Returns the primitive type for every wrapper class or the
 356:    * class itself if it is no wrapper class.</p>
 357:    *
 358:    * <p>This is needed because to be able to find both kinds of methods:
 359:    * One that takes a wrapper class as the first argument and one that
 360:    * accepts a primitive instead.</p>
 361:    */
 362:   private Class initClass(Class klass) {
 363:    if(klass == Boolean.class) {
 364:     return Boolean.TYPE;
 365:    } else if(klass == Byte.class) {
 366:     return Byte.TYPE;
 367:    } else if(klass == Short.class) {
 368:     return Short.TYPE;
 369:    } else if(klass == Integer.class) {
 370:     return Integer.TYPE;
 371:    } else if(klass == Long.class) {
 372:     return Long.TYPE;
 373:    } else if(klass == Float.class) {
 374:     return Float.TYPE;
 375:    } else if(klass == Double.class) {
 376:     return Double.TYPE;
 377:    } else {
 378:     return klass;
 379:    }
 380:   }
 381: 
 382:   /**
 383:    *
 384:    *
 385:    * @param klass
 386:    * @return
 387:    */
 388:   private Class nextClass(Class klass) {
 389:     if(klass == Boolean.TYPE) {
 390:     return Boolean.class;
 391:    } else if(klass == Byte.TYPE) {
 392:     return Byte.class;
 393:    } else if(klass == Short.TYPE) {
 394:     return Short.class;
 395:    } else if(klass == Integer.TYPE) {
 396:     return Integer.class;
 397:    } else if(klass == Long.TYPE) {
 398:     return Long.class;
 399:    } else if(klass == Float.TYPE) {
 400:     return Float.class;
 401:    } else if(klass == Double.TYPE) {
 402:     return Double.class;
 403:    } else {
 404:     return klass.getSuperclass();
 405:    }
 406:    }
 407: 
 408:   /**
 409:    * <p>Constructs an implementation of <code>listenerInterface</code>
 410:    * to dispatch events.</p>
 411:    *
 412:    * <p>You can use such an implementation to simply call a public
 413:    * no-argument method of an arbitrary target object or to forward
 414:    * the first argument of the listener method to the target method.</p>
 415:    *
 416:    * <p>Call this method like:</p>
 417:    * <code>
 418:    * button.addActionListener((ActionListener)
 419:    *    EventHandler.create(ActionListener.class, target, "dispose"));
 420:    * </code>
 421:    *
 422:    * <p>to achieve the following behavior:</p>
 423:    * <code>
 424:    * button.addActionListener(new ActionListener() {
 425:    *    public void actionPerformed(ActionEvent ae) {
 426:    *        target.dispose();
 427:    *    }
 428:    * });
 429:    * </code>
 430:    *
 431:    * <p>That means if you need a listener implementation that simply calls a
 432:    * a no-argument method on a given instance for <strong>each</strong>
 433:    * method of the listener interface.</p>
 434:    *
 435:    * <p>Note: The <code>action</code> is interpreted as a method name. If your target object
 436:    * has no no-argument method of the given name the EventHandler tries to find
 437:    * a method with the same name but which can accept the first argument of the
 438:    * listener method. Usually this will be an event object but any other object
 439:    * will be forwarded, too. Keep in mind that using a property name instead of a
 440:    * real method here is wrong and will throw an <code>ArrayIndexOutOfBoundsException</code>
 441:    * whenever one of the listener methods is called.<p/>
 442:    *
 443:    * <p>The <code>EventHandler</code> will automatically convert primitives
 444:    * to their wrapper class and vice versa. Furthermore it will call
 445:    * a target method if it accepts a superclass of the type of the
 446:    * first argument of the listener method.</p>
 447:    *
 448:    * <p>In case that the method of the target object throws an exception
 449:    * it will be wrapped in a <code>RuntimeException</code> and thrown out
 450:    * of the listener method.</p>
 451:    *
 452:    * <p>In case that the method of the target object cannot be found an
 453:    * <code>ArrayIndexOutOfBoundsException</code> will be thrown when the
 454:    * listener method is invoked.</p>
 455:    *
 456:    * <p>A call to this method is equivalent to:
 457:    * <code>create(listenerInterface, target, action, null, null)</code></p>
 458:    *
 459:    * @param listenerInterface Listener interface to implement.
 460:    * @param target Object to invoke action on.
 461:    * @param action Target property or method to invoke.
 462:    * @return A constructed proxy object.
 463:    */
 464:   public static <T> T create(Class<T> listenerInterface, Object target,
 465:                              String action)
 466:   {
 467:     return create(listenerInterface, target, action, null, null);
 468:   }
 469: 
 470:   /**
 471:    * <p>Constructs an implementation of <code>listenerInterface</code>
 472:    * to dispatch events.</p>
 473:    *
 474:    * <p>Use this method if you want to create an implementation that retrieves
 475:    * a property value from the <b>first</b> argument of the listener method
 476:    * and applies it to the target's property or method. This first argument
 477:    * of the listener is usually an event object but any other object is
 478:    * valid, too.</p>
 479:    *
 480:    * <p>You can set the value of <code>eventPropertyName</code> to "prop"
 481:    * to denote the retrieval of a property named "prop" from the event
 482:    * object. In case that no such property exists the <code>EventHandler</code>
 483:    * will try to find a method with that name.</p>
 484:    *
 485:    * <p>If you set <code>eventPropertyName</code> to a value like this "a.b.c"
 486:    * <code>EventHandler</code> will recursively evaluate the properties "a", "b"
 487:    * and "c". Again if no property can be found the <code>EventHandler</code>
 488:    * tries a method name instead. This allows mixing the names, too: "a.toString"
 489:    * will retrieve the property "a" from the event object and will then call
 490:    * the method "toString" on it.</p>
 491:    *
 492:    * <p>An exception thrown in any of these methods will provoke a
 493:    * <code>RuntimeException</code> to be thrown which contains an
 494:    * <code>InvocationTargetException</code> containing the triggering exception.</p>
 495:    *
 496:    * <p>If you set <code>eventPropertyName</code> to a non-null value the
 497:    * <code>action</code> parameter will be interpreted as a property name
 498:    * or a method name of the target object.</p>
 499:    *
 500:    * <p>Any object retrieved from the event object and applied to the
 501:    * target will converted from primitives to their wrapper class or
 502:    * vice versa or applied to a method that accepts a superclass
 503:    * of the object.</p>
 504:    *
 505:    * <p>Examples:</p>
 506:    * <p>The following code:</p><code>
 507:    * button.addActionListener(
 508:    *    new ActionListener() {
 509:    *        public void actionPerformed(ActionEvent ae) {
 510:    *            Object o = ae.getSource().getClass().getName();
 511:    *            textField.setText((String) o);
 512:    *        }
 513:    *    });
 514:    * </code>
 515:    *
 516:    * <p>Can be expressed using the <code>EventHandler</code> like this:</p>
 517:    * <p>
 518:    * <code>button.addActionListener((ActionListener)
 519:    *    EventHandler.create(ActionListener.class, textField, "text", "source.class.name");
 520:    * <code>
 521:    * </p>
 522:    *
 523:    * <p>As said above you can specify the target as a method, too:</p>
 524:    * <p>
 525:    * <code>button.addActionListener((ActionListener)
 526:    *    EventHandler.create(ActionListener.class, textField, "setText", "source.class.name");
 527:    * <code>
 528:    * </p>
 529:    *
 530:    * <p>Furthermore you can use method names in the property:</p>
 531:    * <p>
 532:    * <code>button.addActionListener((ActionListener)
 533:    *    EventHandler.create(ActionListener.class, textField, "setText", "getSource.getClass.getName");
 534:    * <code>
 535:    * </p>
 536:    *
 537:    * <p>Finally you can mix names:</p>
 538:    * <p>
 539:    * <code>button.addActionListener((ActionListener)
 540:    *    EventHandler.create(ActionListener.class, textField, "setText", "source.getClass.name");
 541:    * <code>
 542:    * </p>
 543:    *
 544:    * <p>A call to this method is equivalent to:
 545:    * <code>create(listenerInterface, target, action, null, null)</code>
 546:    * </p>
 547:    *
 548:    * @param listenerInterface Listener interface to implement.
 549:    * @param target Object to invoke action on.
 550:    * @param action Target property or method to invoke.
 551:    * @param eventPropertyName Name of property to extract from event.
 552:    * @return A constructed proxy object.
 553:    */
 554:   public static <T> T create(Class<T> listenerInterface, Object target,
 555:                              String action, String eventPropertyName)
 556:   {
 557:     return create(listenerInterface, target, action, eventPropertyName, null);
 558:   }
 559: 
 560:   /**
 561:    * <p>Constructs an implementation of <code>listenerInterface</code>
 562:    * to dispatch events.</p>
 563:    *
 564:    * <p>Besides the functionality described for {@link create(Class, Object, String)}
 565:    * and {@link create(Class, Object, String, String)} this method allows you
 566:    * to filter the listener method that should have an effect. Look at these
 567:    * method's documentation for more information about the <code>EventHandler</code>'s
 568:    * usage.</p>
 569:    *
 570:    * <p>If you want to call <code>dispose</code> on a <code>JFrame</code> instance
 571:    * when the <code>WindowListener.windowClosing()</code> method was invoked use
 572:    * the following code:</p>
 573:    * <p>
 574:    * <code>
 575:    * EventHandler.create(WindowListener.class, jframeInstance, "dispose", null, "windowClosing");
 576:    * </code>
 577:    * </p>
 578:    *
 579:    * <p>A <code>NullPointerException</code> is thrown if the <code>listenerInterface</code>
 580:    * or <code>target</code> argument are <code>null</code>.
 581:    *
 582:    * @param listenerInterface Listener interface to implement.
 583:    * @param target Object to invoke action on.
 584:    * @param action Target method name to invoke.
 585:    * @param eventPropertyName Name of property to extract from event.
 586:    * @param listenerMethodName Listener method to implement.
 587:    * @return A constructed proxy object.
 588:    */
 589:   public static <T> T create(Class<T> listenerInterface, Object target,
 590:                              String action, String eventPropertyName,
 591:                              String listenerMethodName)
 592:   {
 593:     // Create EventHandler instance
 594:     EventHandler eh = new EventHandler(target, action, eventPropertyName,
 595:                                        listenerMethodName);
 596: 
 597:     // Create proxy object passing in the event handler
 598:     Object proxy = Proxy.newProxyInstance(listenerInterface.getClassLoader(),
 599:                                           new Class<?>[] {listenerInterface},
 600:                                           eh);
 601: 
 602:     return (T) proxy;
 603:   }
 604: }