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