001 // Copyright 2004, 2005 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 015 package org.apache.tapestry.enhance; 016 017 import org.apache.commons.logging.Log; 018 import org.apache.hivemind.ApplicationRuntimeException; 019 import org.apache.hivemind.ClassResolver; 020 import org.apache.hivemind.HiveMind; 021 import org.apache.hivemind.Location; 022 import org.apache.hivemind.service.BodyBuilder; 023 import org.apache.hivemind.service.ClassFab; 024 import org.apache.hivemind.service.ClassFactory; 025 import org.apache.hivemind.service.MethodSignature; 026 import org.apache.hivemind.util.Defense; 027 import org.apache.hivemind.util.ToStringBuilder; 028 import org.apache.tapestry.services.ComponentConstructor; 029 import org.apache.tapestry.spec.IComponentSpecification; 030 import org.apache.tapestry.util.IdAllocator; 031 import org.apache.tapestry.util.ObjectIdentityMap; 032 033 import java.beans.BeanInfo; 034 import java.beans.IntrospectionException; 035 import java.beans.Introspector; 036 import java.beans.PropertyDescriptor; 037 import java.lang.reflect.Constructor; 038 import java.lang.reflect.Method; 039 import java.lang.reflect.Modifier; 040 import java.util.*; 041 042 /** 043 * Implementation of {@link org.apache.tapestry.enhance.EnhancementOperation}that 044 * knows how to collect class changes from enhancements. The method 045 * {@link #getConstructor()} finalizes the enhancement into a 046 * {@link org.apache.tapestry.services.ComponentConstructor}. 047 * 048 * @author Howard M. Lewis Ship 049 * @since 4.0 050 */ 051 public class EnhancementOperationImpl implements EnhancementOperation 052 { 053 static int _uid = 0; 054 055 private ClassResolver _resolver; 056 057 private IComponentSpecification _specification; 058 059 private Class _baseClass; 060 061 private ClassFab _classFab; 062 063 private final Set _claimedProperties = new HashSet(); 064 065 private final JavaClassMapping _javaClassMapping = new JavaClassMapping(); 066 067 private final List _constructorTypes = new ArrayList(); 068 069 private final List _constructorArguments = new ArrayList(); 070 071 private final ObjectIdentityMap _finalFields = new ObjectIdentityMap(); 072 073 /** 074 * Set of interfaces added to the enhanced class. 075 */ 076 077 private Set _addedInterfaces = new HashSet(); 078 079 /** 080 * Map of {@link BodyBuilder}, keyed on {@link MethodSignature}. 081 */ 082 083 private Map _incompleteMethods = new HashMap(); 084 085 /** 086 * Map of property names to {@link PropertyDescriptor}. 087 */ 088 089 private Map _properties = new HashMap(); 090 091 /** 092 * Used to incrementally assemble the constructor for the enhanced class. 093 */ 094 095 private BodyBuilder _constructorBuilder; 096 097 /** 098 * Makes sure that names created by 099 * {@link #addInjectedField(String, Class, Object)} have unique names. 100 */ 101 102 private final IdAllocator _idAllocator = new IdAllocator(); 103 104 /** 105 * Map keyed on MethodSignature, value is Location. Used to track which 106 * methods have been created, based on which location data (identified 107 * conflicts). 108 */ 109 110 private final Map _methods = new HashMap(); 111 112 // May be null 113 114 private final Log _log; 115 116 /** 117 * Alternate package private constructor used by the test suite, to bypass 118 * the defense checks above. 119 */ 120 121 EnhancementOperationImpl() 122 { 123 _log = null; 124 } 125 126 public EnhancementOperationImpl(ClassResolver classResolver, 127 IComponentSpecification specification, Class baseClass, 128 ClassFactory classFactory, Log log) 129 { 130 Defense.notNull(classResolver, "classResolver"); 131 Defense.notNull(specification, "specification"); 132 Defense.notNull(baseClass, "baseClass"); 133 Defense.notNull(classFactory, "classFactory"); 134 135 _resolver = classResolver; 136 _specification = specification; 137 _baseClass = baseClass; 138 139 introspectBaseClass(); 140 141 String name = newClassName(); 142 143 _classFab = classFactory.newClass(name, _baseClass); 144 _log = log; 145 } 146 147 public String toString() 148 { 149 ToStringBuilder builder = new ToStringBuilder(this); 150 151 builder.append("baseClass", _baseClass.getName()); 152 builder.append("claimedProperties", _claimedProperties); 153 builder.append("classFab", _classFab); 154 155 return builder.toString(); 156 } 157 158 /** 159 * We want to find the properties of the class, but in many cases, the class 160 * is abstract. Some JDK's (Sun) will include public methods from interfaces 161 * implemented by the class in the public declared methods for the class 162 * (which is used by the Introspector). Eclipse's built-in compiler does not 163 * appear to (this may have to do with compiler options I've been unable to 164 * track down). The solution is to augment the information provided directly 165 * by the Introspector with additional information compiled by Introspecting 166 * the interfaces directly or indirectly implemented by the class. 167 */ 168 private void introspectBaseClass() 169 { 170 try 171 { 172 synchronized(HiveMind.INTROSPECTOR_MUTEX) 173 { 174 addPropertiesDeclaredInBaseClass(); 175 } 176 } 177 catch (IntrospectionException ex) 178 { 179 throw new ApplicationRuntimeException(EnhanceMessages.unabelToIntrospectClass(_baseClass, ex), ex); 180 } 181 182 } 183 184 private void addPropertiesDeclaredInBaseClass() 185 throws IntrospectionException 186 { 187 Class introspectClass = _baseClass; 188 189 addPropertiesDeclaredInClass(introspectClass); 190 191 List interfaceQueue = new ArrayList(); 192 193 while(introspectClass != null) 194 { 195 addInterfacesToQueue(introspectClass, interfaceQueue); 196 197 introspectClass = introspectClass.getSuperclass(); 198 } 199 200 while(!interfaceQueue.isEmpty()) 201 { 202 Class interfaceClass = (Class) interfaceQueue.remove(0); 203 204 addPropertiesDeclaredInClass(interfaceClass); 205 206 addInterfacesToQueue(interfaceClass, interfaceQueue); 207 } 208 } 209 210 private void addInterfacesToQueue(Class introspectClass, List interfaceQueue) 211 { 212 Class[] interfaces = introspectClass.getInterfaces(); 213 214 for(int i = 0; i < interfaces.length; i++) 215 interfaceQueue.add(interfaces[i]); 216 } 217 218 private void addPropertiesDeclaredInClass(Class introspectClass) 219 throws IntrospectionException 220 { 221 222 BeanInfo bi = Introspector.getBeanInfo(introspectClass); 223 224 PropertyDescriptor[] pds = bi.getPropertyDescriptors(); 225 226 for(int i = 0; i < pds.length; i++) 227 { 228 PropertyDescriptor pd = pds[i]; 229 230 String name = pd.getName(); 231 232 if (!_properties.containsKey(name)) 233 _properties.put(name, pd); 234 } 235 } 236 237 public void claimProperty(String propertyName) 238 { 239 Defense.notNull(propertyName, "propertyName"); 240 241 if (_claimedProperties.contains(propertyName)) 242 throw new ApplicationRuntimeException(EnhanceMessages.claimedProperty(propertyName)); 243 244 _claimedProperties.add(propertyName); 245 } 246 247 /** 248 * {@inheritDoc} 249 */ 250 public boolean canClaimAsReadOnlyProperty(String propertyName) 251 { 252 if(_claimedProperties.contains(propertyName)) 253 return false; 254 255 PropertyDescriptor pd = getPropertyDescriptor(propertyName); 256 257 if (pd == null) 258 return false; 259 260 return pd.getWriteMethod() == null ? true : false; 261 } 262 263 public void claimReadonlyProperty(String propertyName) 264 { 265 claimProperty(propertyName); 266 267 PropertyDescriptor pd = getPropertyDescriptor(propertyName); 268 269 if (pd != null && pd.getWriteMethod() != null) 270 throw new ApplicationRuntimeException(EnhanceMessages.readonlyProperty(propertyName, pd.getWriteMethod())); 271 } 272 273 public void addField(String name, Class type) 274 { 275 _classFab.addField(name, type); 276 } 277 278 public String addInjectedField(String fieldName, Class fieldType, Object value) 279 { 280 Defense.notNull(fieldName, "fieldName"); 281 Defense.notNull(fieldType, "fieldType"); 282 Defense.notNull(value, "value"); 283 284 String existing = (String) _finalFields.get(value); 285 286 // See if this object has been previously added. 287 288 if (existing != null) 289 return existing; 290 291 // TODO: Should be ensure that the name is unique? 292 293 // Make sure that the field has a unique name (at least, among anything 294 // added 295 // via addFinalField(). 296 297 String uniqueName = _idAllocator.allocateId(fieldName); 298 299 // ClassFab doesn't have an option for saying the field should be final, 300 // just private. 301 // Doesn't make a huge difference. 302 303 _classFab.addField(uniqueName, fieldType); 304 305 int parameterIndex = addConstructorParameter(fieldType, value); 306 307 constructorBuilder().addln("{0} = ${1};", uniqueName, Integer.toString(parameterIndex)); 308 309 // Remember the mapping from the value to the field name. 310 311 _finalFields.put(value, uniqueName); 312 313 return uniqueName; 314 } 315 316 public Class convertTypeName(String type) 317 { 318 Defense.notNull(type, "type"); 319 320 Class result = _javaClassMapping.getType(type); 321 322 if (result == null) 323 { 324 result = _resolver.findClass(type); 325 326 _javaClassMapping.recordType(type, result); 327 } 328 329 return result; 330 } 331 332 public Class getPropertyType(String name) 333 { 334 Defense.notNull(name, "name"); 335 336 PropertyDescriptor pd = getPropertyDescriptor(name); 337 338 return pd == null ? null : pd.getPropertyType(); 339 } 340 341 public void validateProperty(String name, Class expectedType) 342 { 343 Defense.notNull(name, "name"); 344 Defense.notNull(expectedType, "expectedType"); 345 346 PropertyDescriptor pd = getPropertyDescriptor(name); 347 348 if (pd == null) 349 return; 350 351 Class propertyType = pd.getPropertyType(); 352 353 if (propertyType.equals(expectedType)) 354 return; 355 356 throw new ApplicationRuntimeException(EnhanceMessages.propertyTypeMismatch(_baseClass, name, propertyType, expectedType)); 357 } 358 359 PropertyDescriptor getPropertyDescriptor(String name) 360 { 361 return (PropertyDescriptor) _properties.get(name); 362 } 363 364 public String getAccessorMethodName(String propertyName) 365 { 366 Defense.notNull(propertyName, "propertyName"); 367 368 PropertyDescriptor pd = getPropertyDescriptor(propertyName); 369 370 if (pd != null && pd.getReadMethod() != null) 371 return pd.getReadMethod().getName(); 372 373 return EnhanceUtils.createAccessorMethodName(propertyName); 374 } 375 376 public void addMethod(int modifier, MethodSignature sig, String methodBody, Location location) 377 { 378 Defense.notNull(sig, "sig"); 379 Defense.notNull(methodBody, "methodBody"); 380 Defense.notNull(location, "location"); 381 382 Location existing = (Location) _methods.get(sig); 383 if (existing != null) 384 throw new ApplicationRuntimeException(EnhanceMessages.methodConflict(sig, existing), location, null); 385 386 _methods.put(sig, location); 387 388 _classFab.addMethod(modifier, sig, methodBody); 389 } 390 391 public Class getBaseClass() 392 { 393 return _baseClass; 394 } 395 396 public String getClassReference(Class clazz) 397 { 398 Defense.notNull(clazz, "clazz"); 399 400 String result = (String) _finalFields.get(clazz); 401 402 if (result == null) 403 result = addClassReference(clazz); 404 405 return result; 406 } 407 408 private String addClassReference(Class clazz) 409 { 410 StringBuffer buffer = new StringBuffer("_class$"); 411 412 Class c = clazz; 413 414 while(c.isArray()) 415 { 416 buffer.append("array$"); 417 c = c.getComponentType(); 418 } 419 420 buffer.append(c.getName().replace('.', '$')); 421 422 String fieldName = buffer.toString(); 423 424 return addInjectedField(fieldName, Class.class, clazz); 425 } 426 427 /** 428 * Adds a new constructor parameter, returning the new count. This is 429 * convienient, because the first element added is accessed as $1, etc. 430 */ 431 432 private int addConstructorParameter(Class type, Object value) 433 { 434 _constructorTypes.add(type); 435 _constructorArguments.add(value); 436 437 return _constructorArguments.size(); 438 } 439 440 private BodyBuilder constructorBuilder() 441 { 442 if (_constructorBuilder == null) 443 { 444 _constructorBuilder = new BodyBuilder(); 445 _constructorBuilder.begin(); 446 } 447 448 return _constructorBuilder; 449 } 450 451 /** 452 * Returns an object that can be used to construct instances of the enhanced 453 * component subclass. This should only be called once. 454 */ 455 456 public ComponentConstructor getConstructor() 457 { 458 try 459 { 460 finalizeEnhancedClass(); 461 462 Constructor c = findConstructor(); 463 464 Object[] params = _constructorArguments.toArray(); 465 466 return new ComponentConstructorImpl(c, params, _classFab.toString(), _specification.getLocation()); 467 } 468 catch (Throwable t) 469 { 470 throw new ApplicationRuntimeException(EnhanceMessages.classEnhancementFailure(_baseClass, t), _classFab, null, t); 471 } 472 } 473 474 void finalizeEnhancedClass() 475 { 476 finalizeIncompleteMethods(); 477 478 if (_constructorBuilder != null) 479 { 480 _constructorBuilder.end(); 481 482 Class[] types = (Class[]) _constructorTypes.toArray(new Class[_constructorTypes.size()]); 483 484 _classFab.addConstructor(types, null, _constructorBuilder.toString()); 485 } 486 487 if (_log != null && _log.isDebugEnabled()) 488 _log.debug("Creating class:\n\n" + _classFab); 489 } 490 491 private void finalizeIncompleteMethods() 492 { 493 Iterator i = _incompleteMethods.entrySet().iterator(); 494 while(i.hasNext()) 495 { 496 Map.Entry e = (Map.Entry) i.next(); 497 MethodSignature sig = (MethodSignature) e.getKey(); 498 BodyBuilder builder = (BodyBuilder) e.getValue(); 499 500 // Each BodyBuilder is created and given a begin(), this is 501 // the matching end() 502 503 builder.end(); 504 505 _classFab.addMethod(Modifier.PUBLIC, sig, builder.toString()); 506 } 507 } 508 509 private Constructor findConstructor() 510 { 511 Class componentClass = _classFab.createClass(); 512 513 // The fabricated base class always has exactly one constructor 514 515 return componentClass.getConstructors()[0]; 516 } 517 518 private String newClassName() 519 { 520 String baseName = _baseClass.getName(); 521 int dotx = baseName.lastIndexOf('.'); 522 523 return "$" + baseName.substring(dotx + 1) + "_" + _uid++; 524 } 525 526 public void extendMethodImplementation(Class interfaceClass, MethodSignature methodSignature, String code) 527 { 528 addInterfaceIfNeeded(interfaceClass); 529 530 BodyBuilder builder = (BodyBuilder) _incompleteMethods.get(methodSignature); 531 532 if (builder == null) 533 { 534 builder = createIncompleteMethod(methodSignature); 535 536 _incompleteMethods.put(methodSignature, builder); 537 } 538 539 builder.addln(code); 540 } 541 542 private void addInterfaceIfNeeded(Class interfaceClass) 543 { 544 if (implementsInterface(interfaceClass)) 545 return; 546 547 _classFab.addInterface(interfaceClass); 548 _addedInterfaces.add(interfaceClass); 549 } 550 551 public boolean implementsInterface(Class interfaceClass) 552 { 553 if (interfaceClass.isAssignableFrom(_baseClass)) 554 return true; 555 556 Iterator i = _addedInterfaces.iterator(); 557 while(i.hasNext()) 558 { 559 Class addedInterface = (Class) i.next(); 560 561 if (interfaceClass.isAssignableFrom(addedInterface)) 562 return true; 563 } 564 565 return false; 566 } 567 568 private BodyBuilder createIncompleteMethod(MethodSignature sig) 569 { 570 BodyBuilder result = new BodyBuilder(); 571 572 // Matched inside finalizeIncompleteMethods() 573 574 result.begin(); 575 576 if (existingImplementation(sig)) 577 result.addln("super.{0}($$);", sig.getName()); 578 579 return result; 580 } 581 582 /** 583 * Returns true if the base class implements the provided method as either a 584 * public or a protected method. 585 */ 586 587 private boolean existingImplementation(MethodSignature sig) 588 { 589 Method m = findMethod(sig); 590 591 return m != null && !Modifier.isAbstract(m.getModifiers()); 592 } 593 594 /** 595 * Finds a public or protected method in the base class. 596 */ 597 private Method findMethod(MethodSignature sig) 598 { 599 // Finding a public method is easy: 600 601 try 602 { 603 return _baseClass.getMethod(sig.getName(), sig.getParameterTypes()); 604 605 } 606 catch (NoSuchMethodException ex) 607 { 608 // Good; no super-implementation to invoke. 609 } 610 611 Class c = _baseClass; 612 613 while(c != Object.class) 614 { 615 try 616 { 617 return c.getDeclaredMethod(sig.getName(), sig 618 .getParameterTypes()); 619 } 620 catch (NoSuchMethodException ex) 621 { 622 // Ok, continue loop up to next base class. 623 } 624 625 c = c.getSuperclass(); 626 } 627 628 return null; 629 } 630 631 public List findUnclaimedAbstractProperties() 632 { 633 List result = new ArrayList(); 634 635 Iterator i = _properties.values().iterator(); 636 637 while(i.hasNext()) 638 { 639 PropertyDescriptor pd = (PropertyDescriptor) i.next(); 640 641 String name = pd.getName(); 642 643 if (_claimedProperties.contains(name)) 644 continue; 645 646 if (isAbstractProperty(pd)) 647 result.add(name); 648 } 649 650 return result; 651 } 652 653 /** 654 * A property is abstract if either its read method or it write method is 655 * abstract. We could do some additional checking to ensure that both are 656 * abstract if either is. Note that in many cases, there will only be one 657 * accessor (a reader or a writer). 658 */ 659 private boolean isAbstractProperty(PropertyDescriptor pd) 660 { 661 return isExistingAbstractMethod(pd.getReadMethod()) 662 || isExistingAbstractMethod(pd.getWriteMethod()); 663 } 664 665 private boolean isExistingAbstractMethod(Method m) 666 { 667 return m != null && Modifier.isAbstract(m.getModifiers()); 668 } 669 670 public IComponentSpecification getSpecification() 671 { 672 return _specification; 673 } 674 }