001 // Copyright 2006, 2007, 2008, 2010 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.tapestry5.ioc.internal.services; 016 017 import static java.lang.String.format; 018 import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newMap; 019 import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newSet; 020 021 import java.lang.annotation.Annotation; 022 import java.lang.reflect.Method; 023 import java.lang.reflect.Modifier; 024 import java.util.Formatter; 025 import java.util.List; 026 import java.util.Map; 027 import java.util.Set; 028 029 import javassist.CannotCompileException; 030 import javassist.CtClass; 031 import javassist.CtConstructor; 032 import javassist.CtField; 033 import javassist.CtMethod; 034 import javassist.NotFoundException; 035 import javassist.bytecode.AnnotationsAttribute; 036 import javassist.bytecode.ClassFile; 037 import javassist.bytecode.ConstPool; 038 import javassist.bytecode.MethodInfo; 039 import javassist.bytecode.ParameterAnnotationsAttribute; 040 import javassist.bytecode.annotation.MemberValue; 041 042 import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 043 import org.apache.tapestry5.ioc.internal.util.InternalUtils; 044 import org.apache.tapestry5.ioc.services.ClassFab; 045 import org.apache.tapestry5.ioc.services.ClassFabUtils; 046 import org.apache.tapestry5.ioc.services.MethodIterator; 047 import org.apache.tapestry5.ioc.services.MethodSignature; 048 import org.slf4j.Logger; 049 050 /** 051 * Implementation of {@link org.apache.tapestry5.ioc.services.ClassFab}. Hides, as much as possible, the underlying 052 * library (Javassist). 053 */ 054 @SuppressWarnings("all") 055 public class ClassFabImpl extends AbstractFab implements ClassFab 056 { 057 private static final Map<Class, String> DEFAULT_RETURN = newMap(); 058 059 static 060 { 061 DEFAULT_RETURN.put(boolean.class, "false"); 062 DEFAULT_RETURN.put(long.class, "0L"); 063 DEFAULT_RETURN.put(float.class, "0.0f"); 064 DEFAULT_RETURN.put(double.class, "0.0d"); 065 } 066 067 /** 068 * Add fields, methods, and constructors are added, their psuedo-code is appended to this description, which is used 069 * by toString(). 070 */ 071 private final StringBuilder description = new StringBuilder(); 072 073 private final Formatter formatter = new Formatter(description); 074 075 private final Set<MethodSignature> addedSignatures = newSet(); 076 077 public ClassFabImpl(CtClassSource source, CtClass ctClass, Logger logger) 078 { 079 super(source, ctClass, logger); 080 } 081 082 /** 083 * Returns a representation of the fabricated class, including inheritance, fields, constructors, methods and method 084 * bodies. 085 */ 086 @Override 087 public String toString() 088 { 089 StringBuilder buffer = new StringBuilder("ClassFab[\n"); 090 091 try 092 { 093 buffer.append(buildClassAndInheritance()); 094 095 buffer.append(description.toString()); 096 } 097 catch (Exception ex) 098 { 099 buffer.append(" *** "); 100 buffer.append(ex); 101 } 102 103 buffer.append("\n]"); 104 105 return buffer.toString(); 106 } 107 108 private String buildClassAndInheritance() throws NotFoundException 109 { 110 StringBuilder buffer = new StringBuilder(); 111 112 buffer.append(Modifier.toString(getCtClass().getModifiers())); 113 buffer.append(" class "); 114 buffer.append(getName()); 115 buffer.append(" extends "); 116 buffer.append(getCtClass().getSuperclass().getName()); 117 buffer.append("\n"); 118 119 CtClass[] interfaces = getCtClass().getInterfaces(); 120 121 if (interfaces.length > 0) 122 { 123 buffer.append(" implements "); 124 125 for (int i = 0; i < interfaces.length; i++) 126 { 127 if (i > 0) 128 buffer.append(", "); 129 130 buffer.append(interfaces[i].getName()); 131 } 132 133 buffer.append("\n\n"); 134 } 135 136 return buffer.toString(); 137 } 138 139 /** 140 * Returns the name of the class fabricated by this instance. 141 */ 142 String getName() 143 { 144 return getCtClass().getName(); 145 } 146 147 public void addField(String name, Class type) 148 { 149 addField(name, Modifier.PRIVATE, type); 150 } 151 152 public void addField(String name, int modifiers, Class type) 153 { 154 lock.check(); 155 156 CtClass ctType = toCtClass(type); 157 158 try 159 { 160 CtField field = new CtField(ctType, name, getCtClass()); 161 field.setModifiers(modifiers); 162 163 getCtClass().addField(field); 164 } 165 catch (CannotCompileException ex) 166 { 167 // Have yet to find a way to make this happen! 168 throw new RuntimeException(ServiceMessages.unableToAddField(name, getCtClass(), ex), ex); 169 } 170 171 formatter.format("%s %s %s;\n\n", Modifier.toString(modifiers), ClassFabUtils.toJavaClassName(type), name); 172 } 173 174 public void proxyMethodsToDelegate(Class serviceInterface, String delegateExpression, String toString) 175 { 176 lock.check(); 177 178 addInterface(serviceInterface); 179 180 MethodIterator mi = new MethodIterator(serviceInterface); 181 182 while (mi.hasNext()) 183 { 184 MethodSignature sig = mi.next(); 185 186 // ($r) properly handles void methods for us, which keeps this simple. 187 188 String body = format("return ($r) %s.%s($$);", delegateExpression, sig.getName()); 189 190 addMethod(Modifier.PUBLIC, sig, body); 191 } 192 193 if (!mi.getToString()) 194 addToString(toString); 195 } 196 197 public void addToString(String toString) 198 { 199 lock.check(); 200 201 MethodSignature sig = new MethodSignature(String.class, "toString", null, null); 202 203 // TODO: Very simple quoting here, will break down if the string itself contains 204 // double quotes or various other characters that need escaping. 205 206 addMethod(Modifier.PUBLIC, sig, format("return \"%s\";", toString)); 207 } 208 209 public void addMethod(int modifiers, MethodSignature ms, String body) 210 { 211 lock.check(); 212 213 if (addedSignatures.contains(ms)) 214 throw new RuntimeException(ServiceMessages.duplicateMethodInClass(ms, this)); 215 216 CtClass ctReturnType = toCtClass(ms.getReturnType()); 217 218 CtClass[] ctParameters = toCtClasses(ms.getParameterTypes()); 219 CtClass[] ctExceptions = toCtClasses(ms.getExceptionTypes()); 220 221 CtMethod method = new CtMethod(ctReturnType, ms.getName(), ctParameters, getCtClass()); 222 223 try 224 { 225 method.setModifiers(modifiers); 226 method.setBody(body); 227 method.setExceptionTypes(ctExceptions); 228 229 getCtClass().addMethod(method); 230 } 231 catch (Exception ex) 232 { 233 throw new RuntimeException(ServiceMessages.unableToAddMethod(ms, getCtClass(), ex), ex); 234 } 235 236 addedSignatures.add(ms); 237 238 // modifiers, return type, name 239 240 formatter.format("%s %s %s", Modifier.toString(modifiers), ClassFabUtils.toJavaClassName(ms.getReturnType()), 241 ms.getName()); 242 243 // parameters, exceptions and body from this: 244 addMethodDetailsToDescription(ms.getParameterTypes(), ms.getExceptionTypes(), body); 245 246 description.append("\n\n"); 247 } 248 249 public void addNoOpMethod(MethodSignature signature) 250 { 251 lock.check(); 252 253 Class returnType = signature.getReturnType(); 254 255 if (returnType.equals(void.class)) 256 { 257 addMethod(Modifier.PUBLIC, signature, "return;"); 258 return; 259 } 260 261 String value = "null"; 262 if (returnType.isPrimitive()) 263 { 264 value = DEFAULT_RETURN.get(returnType); 265 if (value == null) 266 value = "0"; 267 } 268 269 addMethod(Modifier.PUBLIC, signature, "return " + value + ";"); 270 } 271 272 public void addConstructor(Class[] parameterTypes, Class[] exceptions, String body) 273 { 274 assert InternalUtils.isNonBlank(body); 275 lock.check(); 276 277 CtClass[] ctParameters = toCtClasses(parameterTypes); 278 CtClass[] ctExceptions = toCtClasses(exceptions); 279 280 try 281 { 282 CtConstructor constructor = new CtConstructor(ctParameters, getCtClass()); 283 constructor.setExceptionTypes(ctExceptions); 284 constructor.setBody(body); 285 286 getCtClass().addConstructor(constructor); 287 } 288 catch (Exception ex) 289 { 290 throw new RuntimeException(ServiceMessages.unableToAddConstructor(getCtClass(), ex), ex); 291 } 292 293 description.append("public "); 294 295 // This isn't quite right; we should strip the package portion off of the name. 296 // However, fabricated classes are almost always in the "default" package, so 297 // this is OK. 298 299 description.append(getName()); 300 301 addMethodDetailsToDescription(parameterTypes, exceptions, body); 302 303 description.append("\n\n"); 304 } 305 306 /** 307 * Adds a listing of method (or constructor) parameters and thrown exceptions, and the body, to the description 308 * 309 * @param parameterTypes 310 * types of method parameters, or null 311 * @param exceptions 312 * types of throw exceptions, or null 313 * @param body 314 * body of method or constructor 315 */ 316 private void addMethodDetailsToDescription(Class[] parameterTypes, Class[] exceptions, String body) 317 { 318 description.append("("); 319 320 int count = InternalUtils.size(parameterTypes); 321 for (int i = 0; i < count; i++) 322 { 323 if (i > 0) 324 description.append(", "); 325 326 description.append(ClassFabUtils.toJavaClassName(parameterTypes[i])); 327 328 description.append(" $"); 329 description.append(i + 1); 330 } 331 332 description.append(")"); 333 334 count = InternalUtils.size(exceptions); 335 for (int i = 0; i < count; i++) 336 { 337 if (i == 0) 338 description.append("\n throws "); 339 else 340 description.append(", "); 341 342 // Since this can never be an array type, we don't need to use getJavaClassName 343 344 description.append(exceptions[i].getName()); 345 } 346 347 description.append("\n"); 348 description.append(body); 349 } 350 351 public void copyClassAnnotationsFromDelegate(Class delegateClass) 352 { 353 lock.check(); 354 355 for (Annotation annotation : delegateClass.getAnnotations()) 356 { 357 try 358 { 359 addAnnotation(annotation); 360 } 361 catch (RuntimeException ex) 362 { 363 //Annotation processing may cause exceptions thrown by Javassist. 364 //To provide backward compatibility we have to continue even though copying a particular annotation failed. 365 getLogger().error(String.format("Failed to copy annotation '%s' from '%s'", annotation.annotationType(), delegateClass.getName())); 366 } 367 } 368 } 369 370 public void copyMethodAnnotationsFromDelegate(Class serviceInterface, Class delegateClass) 371 { 372 lock.check(); 373 374 for(MethodSignature sig: addedSignatures) 375 { 376 if(getMethod(sig, serviceInterface) == null) 377 continue; 378 379 Method method = getMethod(sig, delegateClass); 380 381 assert method != null; 382 383 CtMethod ctMethod = getCtMethod(sig); 384 385 Annotation[] annotations = method.getAnnotations(); 386 387 for (Annotation annotation : annotations) 388 { 389 try 390 { 391 addMethodAnnotation(ctMethod, annotation); 392 } 393 catch (RuntimeException ex) 394 { 395 //Annotation processing may cause exceptions thrown by Javassist. 396 //To provide backward compatibility we have to continue even though copying a particular annotation failed. 397 getLogger().error(String.format("Failed to copy annotation '%s' from method '%s' of class '%s'", 398 annotation.annotationType(), method.getName(), delegateClass.getName())); 399 } 400 } 401 402 try 403 { 404 addMethodParameterAnnotation(ctMethod, method.getParameterAnnotations()); 405 } 406 catch (RuntimeException ex) 407 { 408 //Annotation processing may cause exceptions thrown by Javassist. 409 //To provide backward compatibility we have to continue even though copying a particular annotation failed. 410 getLogger().error(String.format("Failed to copy parameter annotations from method '%s' of class '%s'", 411 method.getName(), delegateClass.getName())); 412 } 413 } 414 } 415 416 private CtMethod getCtMethod(MethodSignature sig) 417 { 418 try 419 { 420 return getCtClass().getDeclaredMethod(sig.getName(), toCtClasses(sig.getParameterTypes())); 421 } 422 catch (NotFoundException e) 423 { 424 throw new RuntimeException(e); 425 } 426 } 427 428 private Method getMethod(MethodSignature sig, Class clazz) 429 { 430 try 431 { 432 return clazz.getMethod(sig.getName(), sig.getParameterTypes()); 433 } 434 catch (Exception e) 435 { 436 return null; 437 } 438 } 439 440 private void addAnnotation(Annotation annotation) 441 { 442 443 final ClassFile classFile = getClassFile(); 444 445 AnnotationsAttribute attribute = (AnnotationsAttribute) classFile.getAttribute(AnnotationsAttribute.visibleTag); 446 447 if (attribute == null) 448 { 449 attribute = new AnnotationsAttribute(getConstPool(), AnnotationsAttribute.visibleTag); 450 } 451 452 final javassist.bytecode.annotation.Annotation copy = toJavassistAnnotation(annotation); 453 454 455 attribute.addAnnotation(copy); 456 457 classFile.addAttribute(attribute); 458 459 } 460 461 private void addMethodAnnotation(final CtMethod ctMethod, final Annotation annotation) { 462 463 MethodInfo methodInfo = ctMethod.getMethodInfo(); 464 465 AnnotationsAttribute attribute = (AnnotationsAttribute) methodInfo 466 .getAttribute(AnnotationsAttribute.visibleTag); 467 468 if (attribute == null) { 469 attribute = new AnnotationsAttribute(getConstPool(), AnnotationsAttribute.visibleTag); 470 } 471 472 final javassist.bytecode.annotation.Annotation copy = toJavassistAnnotation(annotation); 473 474 attribute.addAnnotation(copy); 475 476 methodInfo.addAttribute(attribute); 477 478 } 479 480 private void addMethodParameterAnnotation(final CtMethod ctMethod, final Annotation[][] parameterAnnotations) { 481 482 MethodInfo methodInfo = ctMethod.getMethodInfo(); 483 484 ParameterAnnotationsAttribute attribute = (ParameterAnnotationsAttribute) methodInfo 485 .getAttribute(ParameterAnnotationsAttribute.visibleTag); 486 487 if (attribute == null) { 488 attribute = new ParameterAnnotationsAttribute(getConstPool(), ParameterAnnotationsAttribute.visibleTag); 489 } 490 491 List<javassist.bytecode.annotation.Annotation[]> result = CollectionFactory.newList(); 492 493 for (Annotation[] next : parameterAnnotations) 494 { 495 List<javassist.bytecode.annotation.Annotation> list = CollectionFactory.newList(); 496 497 for (Annotation annotation : next) 498 { 499 final javassist.bytecode.annotation.Annotation copy = toJavassistAnnotation(annotation); 500 501 list.add(copy); 502 } 503 504 result.add(list.toArray(new javassist.bytecode.annotation.Annotation[]{})); 505 } 506 507 javassist.bytecode.annotation.Annotation[][] annotations = result.toArray(new javassist.bytecode.annotation.Annotation[][]{}); 508 509 attribute.setAnnotations(annotations); 510 511 methodInfo.addAttribute(attribute); 512 } 513 514 private ClassFile getClassFile() 515 { 516 return getCtClass().getClassFile(); 517 } 518 519 private ConstPool getConstPool() 520 { 521 return getClassFile().getConstPool(); 522 } 523 524 private javassist.bytecode.annotation.Annotation toJavassistAnnotation(final Annotation source) 525 { 526 527 final Class<? extends Annotation> annotationType = source.annotationType(); 528 529 final ConstPool constPool = getConstPool(); 530 531 final javassist.bytecode.annotation.Annotation copy = new javassist.bytecode.annotation.Annotation( 532 annotationType.getName(), constPool); 533 534 final Method[] methods = annotationType.getDeclaredMethods(); 535 536 for (final Method method : methods) 537 { 538 try 539 { 540 CtClass ctType = toCtClass(method.getReturnType()); 541 542 final MemberValue memberValue = javassist.bytecode.annotation.Annotation.createMemberValue(constPool, ctType); 543 final Object value = method.invoke(source); 544 545 memberValue.accept(new AnnotationMemberValueVisitor(constPool, getSource(), value)); 546 547 copy.addMemberValue(method.getName(), memberValue); 548 } 549 catch (final Exception e) 550 { 551 throw new RuntimeException(e); 552 } 553 } 554 555 return copy; 556 } 557 }