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:                          null);
 178:       }
 179:     catch (NoSuchMethodException nsme1)
 180:       {
 181:         try {
 182:           // Look for regular property getter getProperty
 183:           getter = o.getClass().getMethod("get" + capitalize(prop),
 184:                          null);
 185:         } catch(NoSuchMethodException nsme2) {
 186:             try {
 187:             // Finally look for a method of the name prop
 188:             getter = o.getClass().getMethod(prop, null);
 189:             } catch(NoSuchMethodException nsme3) {
 190:                 // Ok, give up with an intelligent hint for the user.
 191:                 throw new RuntimeException("Method not called: Could not find a property or method '" + prop
 192:                         + "' in " + o.getClass() + " while following the property argument '" + property + "'.");
 193:             }
 194:         }
 195:       }
 196:     try {
 197:       Object val = getter.invoke(o, null);
 198: 
 199:       if (rest != null)
 200:         return getProperty(val, rest);
 201: 
 202:       return new Object[] {val, getter.getReturnType()};
 203:     } catch(InvocationTargetException ite) {
 204:         throw new RuntimeException("Method not called: Property or method '" + prop + "' has thrown an exception.", ite);
 205:     } catch(IllegalAccessException iae) {
 206:         // This cannot happen because we looked up method with Class.getMethod()
 207:         // which returns public methods only.
 208:         throw (InternalError) new InternalError("Non-public method was invoked.").initCause(iae);
 209:     }
 210:   }
 211: 
 212:   /**
 213:    * Invokes the <code>EventHandler</code>.
 214:    * 
 215:    * <p>This method is normally called by the listener's proxy implementation.</p>
 216:    * 
 217:    * @param proxy The listener interface that is implemented using
 218:    * the proxy mechanism.
 219:    * @param method The method that was called on the proxy instance.
 220:    * @param arguments The arguments which where given to the method.
 221:    * @throws Throwable <code>NoSuchMethodException</code> is thrown when the EventHandler's
 222:    * action method or property cannot be found.
 223:    */
 224:   public Object invoke(Object proxy, Method method, Object[] arguments)
 225:   {
 226:       try {
 227:       // The method instance of the target object. We have to find out which
 228:       // one we have to invoke.
 229:       Method actionMethod = null;
 230: 
 231:     // Listener methods that weren't specified are ignored.  If listenerMethod
 232:     // is null, then all listener methods are processed.
 233:     if (listenerMethod != null && !method.getName().equals(listenerMethod))
 234:       return null;
 235: 
 236:     // If a property is defined we definitely need a valid object at
 237:     // arguments[0] that can be used to retrieve a value to which the
 238:     // property of the target gets set.
 239:     if(property != null) {
 240:       // Extracts the argument. We will let it fail with a NullPointerException
 241:       // the caller used a listener method that has no arguments.
 242:       Object event = arguments[0];
 243: 
 244:       // Obtains the property XXX propertyType keeps showing up null - why?
 245:       // because the object inside getProperty changes, but the ref variable
 246:       // can't change this way, dolt!  need a better way to get both values out
 247:       // - need method and object to do the invoke and get return type
 248:       Object v[] = getProperty(event, property);
 249:       Object[] args = new Object[] { v[0] };
 250:       
 251:       // Changes the class array that controls which method signature we are going
 252:       // to look up in the target object.
 253:       Class[] argTypes = new Class[] { initClass((Class) v[1]) };
 254:     
 255:       // Tries to  find a setter method to which we can apply the
 256:       while(argTypes[0] != null) {
 257:       try
 258:       {
 259:         // Look for a property setter for action.
 260:         actionMethod = targetClass.getMethod("set" + capitalize(action), argTypes);
 261: 
 262:         return actionMethod.invoke(target, args);
 263:       }
 264:     catch (NoSuchMethodException e)
 265:       {
 266:         // If action as property didn't work, try as method later.
 267:       }
 268:     
 269:       argTypes[0] = nextClass(argTypes[0]);
 270:       }
 271:       
 272:       // We could not find a suitable setter method. Now we try again interpreting
 273:       // action as the method name itself.
 274:       // Since we probably have changed the block local argTypes array 
 275:       // we need to rebuild it.
 276:       argTypes = new Class[] { initClass((Class) v[1]) };
 277:     
 278:       // Tries to  find a setter method to which we can apply the
 279:       while(argTypes[0] != null) {
 280:         try
 281:         {
 282:           actionMethod = targetClass.getMethod(action, argTypes);
 283: 
 284:           return actionMethod.invoke(target, args);
 285:         }
 286:         catch (NoSuchMethodException e)
 287:         {
 288:         }
 289:         
 290:         argTypes[0] = nextClass(argTypes[0]);
 291:       }
 292:         
 293:         throw new RuntimeException("Method not called: Could not find a public method named '"
 294:                 + action + "' in target " + targetClass + " which takes a '"
 295:                 + v[1] + "' argument or a property of this type.");
 296:       }      
 297:   
 298:     // If property was null we will search for a no-argument method here.
 299:     // Note: The ordering of method lookups is important because we want to prefer no-argument
 300:     // calls like the JDK does. This means if we have actionMethod() and actionMethod(Event) we will
 301:     // call the first *EVEN* if we have a valid argument for the second method. This is behavior compliant
 302:     // to the JDK.
 303:     // If actionMethod() is not available but there is a actionMethod(Event) we take this. That makes us
 304:     // more specification compliant than the JDK itself because this one will fail in such a case.
 305:     try
 306:       {
 307:       actionMethod = targetClass.getMethod(action, null);
 308:       }
 309:     catch(NoSuchMethodException nsme)
 310:       {
 311:         // Note: If we want to be really strict the specification says that a no-argument method should
 312:         // accept an EventObject (or subclass I guess). However since the official implementation is broken
 313:         // anyways, it's more flexible without the EventObject restriction and we are compatible on everything
 314:         // else this can stay this way.
 315:         if(arguments != null && arguments.length >= 1/* && arguments[0] instanceof EventObject*/) {
 316:             Class[] targetArgTypes = new Class[] { initClass(arguments[0].getClass()) };
 317:             
 318:             while(targetArgTypes[0] != null) {
 319:                 try
 320:                 {
 321:                   // If no property exists we expect the first element of the arguments to be
 322:                   // an EventObject which is then applied to the target method.
 323:       
 324:                   actionMethod = targetClass.getMethod(action, targetArgTypes);
 325:               
 326:                   return actionMethod.invoke(target, new Object[] { arguments[0] });
 327:                 }
 328:                 catch(NoSuchMethodException nsme2)
 329:                 {
 330:                     
 331:                 }
 332:                 
 333:                 targetArgTypes[0] = nextClass(targetArgTypes[0]);
 334:             }
 335:           
 336:         }
 337:       }
 338: 
 339:     // If we do not have a Method instance at this point this means that all our tries
 340:     // failed. The JDK throws an ArrayIndexOutOfBoundsException in this case.
 341:     if(actionMethod == null)
 342:       throw new ArrayIndexOutOfBoundsException(0);
 343:     
 344:     // Invoke target.action(property)
 345:     return actionMethod.invoke(target, null);
 346:       } catch(InvocationTargetException ite) {
 347:          throw new RuntimeException(ite.getCause());
 348:       } catch(IllegalAccessException iae) {
 349:           // Cannot happen because we always use getMethod() which returns public
 350:           // methods only. Otherwise there is something seriously broken in
 351:           // GNU Classpath.
 352:           throw (InternalError) new InternalError("Non-public method was invoked.").initCause(iae);
 353:       }
 354:   }
 355:   
 356:   /**
 357:    * <p>Returns the primitive type for every wrapper class or the
 358:    * class itself if it is no wrapper class.</p>
 359:    * 
 360:    * <p>This is needed because to be able to find both kinds of methods:
 361:    * One that takes a wrapper class as the first argument and one that
 362:    * accepts a primitive instead.</p>
 363:    */
 364:   private Class initClass(Class klass) {
 365:    if(klass == Boolean.class) {
 366:     return Boolean.TYPE;    
 367:    } else if(klass == Byte.class) {
 368:     return Byte.TYPE;   
 369:    } else if(klass == Short.class) {
 370:     return Short.TYPE;   
 371:    } else if(klass == Integer.class) {
 372:     return Integer.TYPE;   
 373:    } else if(klass == Long.class) {
 374:     return Long.TYPE;   
 375:    } else if(klass == Float.class) {
 376:     return Float.TYPE;   
 377:    } else if(klass == Double.class) {
 378:     return Double.TYPE;   
 379:    } else {
 380:     return klass;   
 381:    }
 382:   }
 383: 
 384:   /**
 385:    * 
 386:    * 
 387:    * @param klass
 388:    * @return
 389:    */
 390:   private Class nextClass(Class klass) {
 391:     if(klass == Boolean.TYPE) {
 392:     return Boolean.class;    
 393:    } else if(klass == Byte.TYPE) {
 394:     return Byte.class;   
 395:    } else if(klass == Short.TYPE) {
 396:     return Short.class;   
 397:    } else if(klass == Integer.TYPE) {
 398:     return Integer.class;   
 399:    } else if(klass == Long.TYPE) {
 400:     return Long.class;   
 401:    } else if(klass == Float.TYPE) {
 402:     return Float.class;   
 403:    } else if(klass == Double.TYPE) {
 404:     return Double.class;   
 405:    } else {
 406:     return klass.getSuperclass();
 407:    }
 408:    }
 409:   
 410:   /**
 411:    * <p>Constructs an implementation of <code>listenerInterface</code>
 412:    * to dispatch events.</p>
 413:    * 
 414:    * <p>You can use such an implementation to simply call a public
 415:    * no-argument method of an arbitrary target object or to forward
 416:    * the first argument of the listener method to the target method.</p>
 417:    * 
 418:    * <p>Call this method like:</p>
 419:    * <code>
 420:    * button.addActionListener((ActionListener)
 421:    *    EventHandler.create(ActionListener.class, target, "dispose"));
 422:    * </code>
 423:    * 
 424:    * <p>to achieve the following behavior:</p>
 425:    * <code>
 426:    * button.addActionListener(new ActionListener() {
 427:    *    public void actionPerformed(ActionEvent ae) {
 428:    *        target.dispose();
 429:    *    }
 430:    * });
 431:    * </code>
 432:    * 
 433:    * <p>That means if you need a listener implementation that simply calls a
 434:    * a no-argument method on a given instance for <strong>each</strong>
 435:    * method of the listener interface.</p>
 436:    * 
 437:    * <p>Note: The <code>action</code> is interpreted as a method name. If your target object
 438:    * has no no-argument method of the given name the EventHandler tries to find
 439:    * a method with the same name but which can accept the first argument of the
 440:    * listener method. Usually this will be an event object but any other object
 441:    * will be forwarded, too. Keep in mind that using a property name instead of a
 442:    * real method here is wrong and will throw an <code>ArrayIndexOutOfBoundsException</code>
 443:    * whenever one of the listener methods is called.<p/>
 444:    *
 445:    * <p>The <code>EventHandler</code> will automatically convert primitives
 446:    * to their wrapper class and vice versa. Furthermore it will call
 447:    * a target method if it accepts a superclass of the type of the
 448:    * first argument of the listener method.</p>
 449:    * 
 450:    * <p>In case that the method of the target object throws an exception
 451:    * it will be wrapped in a <code>RuntimeException</code> and thrown out
 452:    * of the listener method.</p>
 453:    * 
 454:    * <p>In case that the method of the target object cannot be found an
 455:    * <code>ArrayIndexOutOfBoundsException</code> will be thrown when the
 456:    * listener method is invoked.</p>
 457:    * 
 458:    * <p>A call to this method is equivalent to:
 459:    * <code>create(listenerInterface, target, action, null, null)</code></p>
 460:    *
 461:    * @param listenerInterface Listener interface to implement.
 462:    * @param target Object to invoke action on.
 463:    * @param action Target property or method to invoke.
 464:    * @return A constructed proxy object.
 465:    */
 466:   public static <T> T create(Class<T> listenerInterface, Object target,
 467:                  String action)
 468:   {
 469:     return create(listenerInterface, target, action, null, null);
 470:   }
 471: 
 472:   /**
 473:    * <p>Constructs an implementation of <code>listenerInterface</code>
 474:    * to dispatch events.</p>
 475:    *
 476:    * <p>Use this method if you want to create an implementation that retrieves
 477:    * a property value from the <b>first</b> argument of the listener method
 478:    * and applies it to the target's property or method. This first argument
 479:    * of the listener is usually an event object but any other object is
 480:    * valid, too.</p>
 481:    * 
 482:    * <p>You can set the value of <code>eventPropertyName</code> to "prop"
 483:    * to denote the retrieval of a property named "prop" from the event
 484:    * object. In case that no such property exists the <code>EventHandler</code>
 485:    * will try to find a method with that name.</p>
 486:    * 
 487:    * <p>If you set <code>eventPropertyName</code> to a value like this "a.b.c"
 488:    * <code>EventHandler</code> will recursively evaluate the properties "a", "b"
 489:    * and "c". Again if no property can be found the <code>EventHandler</code>
 490:    * tries a method name instead. This allows mixing the names, too: "a.toString"
 491:    * will retrieve the property "a" from the event object and will then call
 492:    * the method "toString" on it.</p>
 493:    * 
 494:    * <p>An exception thrown in any of these methods will provoke a
 495:    * <code>RuntimeException</code> to be thrown which contains an
 496:    * <code>InvocationTargetException</code> containing the triggering exception.</p>
 497:    * 
 498:    * <p>If you set <code>eventPropertyName</code> to a non-null value the
 499:    * <code>action</code> parameter will be interpreted as a property name
 500:    * or a method name of the target object.</p>
 501:    *   
 502:    * <p>Any object retrieved from the event object and applied to the
 503:    * target will converted from primitives to their wrapper class or
 504:    * vice versa or applied to a method that accepts a superclass
 505:    * of the object.</p>
 506:    *
 507:    * <p>Examples:</p>
 508:    * <p>The following code:</p><code>
 509:    * button.addActionListener(
 510:    *    new ActionListener() {
 511:    *        public void actionPerformed(ActionEvent ae) {
 512:    *            Object o = ae.getSource().getClass().getName();
 513:    *            textField.setText((String) o);
 514:    *        }
 515:    *    });
 516:    * </code>
 517:    * 
 518:    * <p>Can be expressed using the <code>EventHandler</code> like this:</p>
 519:    * <p>
 520:    * <code>button.addActionListener((ActionListener)
 521:    *    EventHandler.create(ActionListener.class, textField, "text", "source.class.name");
 522:    * <code>
 523:    * </p>
 524:    * 
 525:    * <p>As said above you can specify the target as a method, too:</p>
 526:    * <p>
 527:    * <code>button.addActionListener((ActionListener)
 528:    *    EventHandler.create(ActionListener.class, textField, "setText", "source.class.name");
 529:    * <code>
 530:    * </p>
 531:    * 
 532:    * <p>Furthermore you can use method names in the property:</p>
 533:    * <p>
 534:    * <code>button.addActionListener((ActionListener)
 535:    *    EventHandler.create(ActionListener.class, textField, "setText", "getSource.getClass.getName");
 536:    * <code>
 537:    * </p>
 538:    * 
 539:    * <p>Finally you can mix names:</p>
 540:    * <p>
 541:    * <code>button.addActionListener((ActionListener)
 542:    *    EventHandler.create(ActionListener.class, textField, "setText", "source.getClass.name");
 543:    * <code>
 544:    * </p>
 545:    * 
 546:    * <p>A call to this method is equivalent to:
 547:    * <code>create(listenerInterface, target, action, null, null)</code>
 548:    * </p>
 549:    *
 550:    * @param listenerInterface Listener interface to implement.
 551:    * @param target Object to invoke action on.
 552:    * @param action Target property or method to invoke.
 553:    * @param eventPropertyName Name of property to extract from event.
 554:    * @return A constructed proxy object.
 555:    */
 556:   public static <T> T create(Class<T> listenerInterface, Object target,
 557:                  String action, String eventPropertyName)
 558:   {
 559:     return create(listenerInterface, target, action, eventPropertyName, null);
 560:   }
 561: 
 562:   /**
 563:    * <p>Constructs an implementation of <code>listenerInterface</code>
 564:    * to dispatch events.</p>
 565:    *
 566:    * <p>Besides the functionality described for {@link create(Class, Object, String)}
 567:    * and {@link create(Class, Object, String, String)} this method allows you
 568:    * to filter the listener method that should have an effect. Look at these
 569:    * method's documentation for more information about the <code>EventHandler</code>'s
 570:    * usage.</p>
 571:    * 
 572:    * <p>If you want to call <code>dispose</code> on a <code>JFrame</code> instance
 573:    * when the <code>WindowListener.windowClosing()</code> method was invoked use
 574:    * the following code:</p>
 575:    * <p>
 576:    * <code>
 577:    * EventHandler.create(WindowListener.class, jframeInstance, "dispose", null, "windowClosing");
 578:    * </code>
 579:    * </p>
 580:    * 
 581:    * <p>A <code>NullPointerException</code> is thrown if the <code>listenerInterface</code>
 582:    * or <code>target</code> argument are <code>null</code>.
 583:    * 
 584:    * @param listenerInterface Listener interface to implement.
 585:    * @param target Object to invoke action on.
 586:    * @param action Target method name to invoke.
 587:    * @param eventPropertyName Name of property to extract from event.
 588:    * @param listenerMethodName Listener method to implement.
 589:    * @return A constructed proxy object.
 590:    */
 591:   public static <T> T create(Class<T> listenerInterface, Object target,
 592:                  String action, String eventPropertyName,
 593:                  String listenerMethodName)
 594:   {
 595:     // Create EventHandler instance
 596:     EventHandler eh = new EventHandler(target, action, eventPropertyName,
 597:                        listenerMethodName);
 598: 
 599:     // Create proxy object passing in the event handler
 600:     Object proxy = Proxy.newProxyInstance(listenerInterface.getClassLoader(),
 601:                       new Class<?>[] {listenerInterface},
 602:                       eh);
 603: 
 604:     return (T) proxy;
 605:   }
 606: }