001// Copyright 2011-2013 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.tapestry5.internal.plastic; 016 017import org.apache.tapestry5.internal.plastic.asm.ClassReader; 018import org.apache.tapestry5.internal.plastic.asm.ClassWriter; 019import org.apache.tapestry5.internal.plastic.asm.Opcodes; 020import org.apache.tapestry5.internal.plastic.asm.tree.*; 021import org.apache.tapestry5.plastic.*; 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025import java.io.BufferedInputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.lang.annotation.Annotation; 029import java.lang.reflect.Modifier; 030import java.util.*; 031import java.util.concurrent.CopyOnWriteArrayList; 032 033/** 034 * Responsible for managing a class loader that allows ASM {@link ClassNode}s 035 * to be instantiated as runtime classes. 036 */ 037@SuppressWarnings("rawtypes") 038public class PlasticClassPool implements ClassLoaderDelegate, Opcodes, PlasticClassListenerHub 039{ 040 private static final Logger LOGGER = LoggerFactory.getLogger(PlasticClassPool.class); 041 042 final PlasticClassLoader loader; 043 044 private final PlasticManagerDelegate delegate; 045 046 private final Set<String> controlledPackages; 047 048 private final Map<String, Boolean> checkedExceptionCache = new HashMap<String, Boolean>(); 049 050 051 // Would use Deque, but that's added in 1.6 and we're still striving for 1.5 code compatibility. 052 053 private final Stack<String> activeInstrumentClassNames = new Stack<String>(); 054 055 /** 056 * Maps class names to instantiators for that class name. 057 * Synchronized on the loader. 058 */ 059 private final Map<String, ClassInstantiator> instantiators = PlasticInternalUtils.newMap(); 060 061 private final InheritanceData emptyInheritanceData = new InheritanceData(null); 062 063 private final StaticContext emptyStaticContext = new StaticContext(); 064 065 private final List<PlasticClassListener> listeners = new CopyOnWriteArrayList<PlasticClassListener>(); 066 067 private final Cache<String, TypeCategory> typeName2Category = new Cache<String, TypeCategory>() 068 { 069 @Override 070 protected TypeCategory convert(String typeName) 071 { 072 ClassNode cn = constructClassNodeFromBytecode(typeName); 073 074 return Modifier.isInterface(cn.access) ? TypeCategory.INTERFACE : TypeCategory.CLASS; 075 } 076 }; 077 078 static class BaseClassDef 079 { 080 final InheritanceData inheritanceData; 081 082 final StaticContext staticContext; 083 084 public BaseClassDef(InheritanceData inheritanceData, StaticContext staticContext) 085 { 086 this.inheritanceData = inheritanceData; 087 this.staticContext = staticContext; 088 } 089 } 090 091 /** 092 * Map from FQCN to BaseClassDef. Synchronized on the loader. 093 */ 094 private final Map<String, BaseClassDef> baseClassDefs = PlasticInternalUtils.newMap(); 095 096 097 private final Map<String, FieldInstrumentations> instrumentations = PlasticInternalUtils.newMap(); 098 099 private final Map<String, String> transformedClassNameToImplementationClassName = PlasticInternalUtils.newMap(); 100 101 102 private final FieldInstrumentations placeholder = new FieldInstrumentations(null); 103 104 105 private final Set<TransformationOption> options; 106 107 /** 108 * Creates the pool with a set of controlled packages; all classes in the controlled packages are loaded by the 109 * pool's class loader, and all top-level classes in the controlled packages are transformed via the delegate. 110 * 111 * @param parentLoader 112 * typically, the Thread's context class loader 113 * @param delegate 114 * responsible for end stages of transforming top-level classes 115 * @param controlledPackages 116 * set of package names (note: retained, not copied) 117 * @param options 118 * used when transforming classes 119 */ 120 public PlasticClassPool(ClassLoader parentLoader, PlasticManagerDelegate delegate, Set<String> controlledPackages, 121 Set<TransformationOption> options) 122 { 123 loader = new PlasticClassLoader(parentLoader, this); 124 this.delegate = delegate; 125 this.controlledPackages = controlledPackages; 126 this.options = options; 127 } 128 129 public ClassLoader getClassLoader() 130 { 131 return loader; 132 } 133 134 public Class realizeTransformedClass(ClassNode classNode, InheritanceData inheritanceData, 135 StaticContext staticContext) 136 { 137 synchronized (loader) 138 { 139 Class result = realize(PlasticInternalUtils.toClassName(classNode.name), ClassType.PRIMARY, classNode); 140 baseClassDefs.put(result.getName(), new BaseClassDef(inheritanceData, staticContext)); 141 142 return result; 143 } 144 145 } 146 147 public Class realize(String primaryClassName, ClassType classType, ClassNode classNode) 148 { 149 synchronized (loader) 150 { 151 if (!listeners.isEmpty()) 152 { 153 fire(toEvent(primaryClassName, classType, classNode)); 154 } 155 156 byte[] bytecode = toBytecode(classNode); 157 158 String className = PlasticInternalUtils.toClassName(classNode.name); 159 160 return loader.defineClassWithBytecode(className, bytecode); 161 } 162 } 163 164 private PlasticClassEvent toEvent(final String primaryClassName, final ClassType classType, 165 final ClassNode classNode) 166 { 167 return new PlasticClassEvent() 168 { 169 @Override 170 public ClassType getType() 171 { 172 return classType; 173 } 174 175 @Override 176 public String getPrimaryClassName() 177 { 178 return primaryClassName; 179 } 180 181 @Override 182 public String getDissasembledBytecode() 183 { 184 return PlasticInternalUtils.dissasembleBytecode(classNode); 185 } 186 187 @Override 188 public String getClassName() 189 { 190 return PlasticInternalUtils.toClassName(classNode.name); 191 } 192 }; 193 } 194 195 private void fire(PlasticClassEvent event) 196 { 197 for (PlasticClassListener listener : listeners) 198 { 199 listener.classWillLoad(event); 200 } 201 } 202 203 private byte[] toBytecode(ClassNode classNode) 204 { 205 ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); 206 207 classNode.accept(writer); 208 209 return writer.toByteArray(); 210 } 211 212 public AnnotationAccess createAnnotationAccess(String className) 213 { 214 try 215 { 216 final Class<?> searchClass = loader.loadClass(className); 217 218 return new AnnotationAccess() 219 { 220 @Override 221 public <T extends Annotation> boolean hasAnnotation(Class<T> annotationType) 222 { 223 return getAnnotation(annotationType) != null; 224 } 225 226 @Override 227 public <T extends Annotation> T getAnnotation(Class<T> annotationType) 228 { 229 return searchClass.getAnnotation(annotationType); 230 } 231 }; 232 } catch (Exception ex) 233 { 234 throw new RuntimeException(ex); 235 } 236 } 237 238 public AnnotationAccess createAnnotationAccess(List<AnnotationNode> annotationNodes) 239 { 240 if (annotationNodes == null) 241 { 242 return EmptyAnnotationAccess.SINGLETON; 243 } 244 245 final Map<String, Object> cache = PlasticInternalUtils.newMap(); 246 final Map<String, AnnotationNode> nameToNode = PlasticInternalUtils.newMap(); 247 248 for (AnnotationNode node : annotationNodes) 249 { 250 nameToNode.put(PlasticInternalUtils.objectDescriptorToClassName(node.desc), node); 251 } 252 253 return new AnnotationAccess() 254 { 255 @Override 256 public <T extends Annotation> boolean hasAnnotation(Class<T> annotationType) 257 { 258 return nameToNode.containsKey(annotationType.getName()); 259 } 260 261 @Override 262 public <T extends Annotation> T getAnnotation(Class<T> annotationType) 263 { 264 String className = annotationType.getName(); 265 266 Object result = cache.get(className); 267 268 if (result == null) 269 { 270 result = buildAnnotation(className); 271 272 if (result != null) 273 cache.put(className, result); 274 } 275 276 return annotationType.cast(result); 277 } 278 279 private Object buildAnnotation(String className) 280 { 281 AnnotationNode node = nameToNode.get(className); 282 283 if (node == null) 284 return null; 285 286 return createAnnotation(className, node); 287 } 288 }; 289 } 290 291 Class loadClass(String className) 292 { 293 try 294 { 295 return loader.loadClass(className); 296 } catch (Exception ex) 297 { 298 throw new RuntimeException(String.format("Unable to load class %s: %s", className, 299 PlasticInternalUtils.toMessage(ex)), ex); 300 } 301 } 302 303 protected Object createAnnotation(String className, AnnotationNode node) 304 { 305 AnnotationBuilder builder = new AnnotationBuilder(loadClass(className), this); 306 307 node.accept(builder); 308 309 return builder.createAnnotation(); 310 } 311 312 @Override 313 public boolean shouldInterceptClassLoading(String className) 314 { 315 int searchFromIndex = className.length() - 1; 316 317 while (true) 318 { 319 int dotx = className.lastIndexOf('.', searchFromIndex); 320 321 if (dotx < 0) 322 break; 323 324 String packageName = className.substring(0, dotx); 325 326 if (controlledPackages.contains(packageName)) 327 return true; 328 329 searchFromIndex = dotx - 1; 330 } 331 332 return false; 333 } 334 335 // Hopefully the synchronized will not cause a deadlock 336 337 @Override 338 public synchronized Class<?> loadAndTransformClass(String className) throws ClassNotFoundException 339 { 340 // Inner classes are not transformed, but they are loaded by the same class loader. 341 342 if (className.contains("$")) 343 { 344 return loadInnerClass(className); 345 } 346 347 // TODO: What about interfaces, enums, annotations, etc. ... they shouldn't be in the package, but 348 // we should generate a reasonable error message. 349 350 if (activeInstrumentClassNames.contains(className)) 351 { 352 StringBuilder builder = new StringBuilder(""); 353 String sep = ""; 354 355 for (String name : activeInstrumentClassNames) 356 { 357 builder.append(sep); 358 builder.append(name); 359 360 sep = ", "; 361 } 362 363 throw new IllegalStateException(String.format("Unable to transform class %s as it is already in the process of being transformed; there is a cycle among the following classes: %s.", 364 className, builder)); 365 } 366 367 activeInstrumentClassNames.push(className); 368 369 try 370 { 371 372 InternalPlasticClassTransformation transformation = getPlasticClassTransformation(className); 373 374 delegate.transform(transformation.getPlasticClass()); 375 376 ClassInstantiator createInstantiator = transformation.createInstantiator(); 377 ClassInstantiator configuredInstantiator = delegate.configureInstantiator(className, createInstantiator); 378 379 instantiators.put(className, configuredInstantiator); 380 381 return transformation.getTransformedClass(); 382 } finally 383 { 384 activeInstrumentClassNames.pop(); 385 } 386 } 387 388 private Class loadInnerClass(String className) 389 { 390 ClassNode classNode = constructClassNodeFromBytecode(className); 391 392 interceptFieldAccess(classNode); 393 394 return realize(className, ClassType.INNER, classNode); 395 } 396 397 private void interceptFieldAccess(ClassNode classNode) 398 { 399 for (MethodNode method : classNode.methods) 400 { 401 interceptFieldAccess(classNode.name, method); 402 } 403 } 404 405 private void interceptFieldAccess(String classInternalName, MethodNode method) 406 { 407 InsnList insns = method.instructions; 408 409 ListIterator it = insns.iterator(); 410 411 while (it.hasNext()) 412 { 413 AbstractInsnNode node = (AbstractInsnNode) it.next(); 414 415 int opcode = node.getOpcode(); 416 417 if (opcode != GETFIELD && opcode != PUTFIELD) 418 { 419 continue; 420 } 421 422 FieldInsnNode fnode = (FieldInsnNode) node; 423 424 String ownerInternalName = fnode.owner; 425 426 if (ownerInternalName.equals(classInternalName)) 427 { 428 continue; 429 } 430 431 FieldInstrumentation instrumentation = getFieldInstrumentation(ownerInternalName, fnode.name, opcode == GETFIELD); 432 433 if (instrumentation == null) 434 { 435 continue; 436 } 437 438 // Replace the field access node with the appropriate method invocation. 439 440 insns.insertBefore(fnode, new MethodInsnNode(INVOKEVIRTUAL, ownerInternalName, instrumentation.methodName, instrumentation.methodDescription)); 441 442 it.remove(); 443 } 444 } 445 446 447 /** 448 * For a fully-qualified class name of an <em>existing</em> class, loads the bytes for the class 449 * and returns a PlasticClass instance. 450 * 451 * @throws ClassNotFoundException 452 */ 453 public InternalPlasticClassTransformation getPlasticClassTransformation(String className) 454 throws ClassNotFoundException 455 { 456 assert PlasticInternalUtils.isNonBlank(className); 457 458 ClassNode classNode = constructClassNodeFromBytecode(className); 459 460 String baseClassName = PlasticInternalUtils.toClassName(classNode.superName); 461 462 instrumentations.put(classNode.name, new FieldInstrumentations(classNode.superName)); 463 464 // TODO: check whether second parameter should really be null 465 return createTransformation(baseClassName, classNode, null, false); 466 } 467 468 /** 469 * @param baseClassName 470 * class from which the transformed class extends 471 * @param classNode 472 * node for the class 473 * @param implementationClassNode 474 * node for the implementation class. May be null. 475 * @param proxy 476 * if true, the class is a new empty class; if false an existing class that's being transformed 477 * @throws ClassNotFoundException 478 */ 479 private InternalPlasticClassTransformation createTransformation(String baseClassName, ClassNode classNode, ClassNode implementationClassNode, boolean proxy) 480 throws ClassNotFoundException 481 { 482 if (shouldInterceptClassLoading(baseClassName)) 483 { 484 loader.loadClass(baseClassName); 485 486 BaseClassDef def = baseClassDefs.get(baseClassName); 487 488 assert def != null; 489 490 return new PlasticClassImpl(classNode, implementationClassNode, this, def.inheritanceData, def.staticContext, proxy); 491 } 492 493 // When the base class is Object, or otherwise not in a transformed package, 494 // then start with the empty 495 return new PlasticClassImpl(classNode, implementationClassNode, this, emptyInheritanceData, emptyStaticContext, proxy); 496 } 497 498 /** 499 * Constructs a class node by reading the raw bytecode for a class and instantiating a ClassNode 500 * (via {@link ClassReader#accept(org.apache.tapestry5.internal.plastic.asm.ClassVisitor, int)}). 501 * 502 * @param className 503 * fully qualified class name 504 * @return corresponding ClassNode 505 */ 506 public ClassNode constructClassNodeFromBytecode(String className) 507 { 508 byte[] bytecode = readBytecode(className); 509 510 if (bytecode == null) 511 return null; 512 513 return PlasticInternalUtils.convertBytecodeToClassNode(bytecode); 514 } 515 516 private byte[] readBytecode(String className) 517 { 518 ClassLoader parentClassLoader = loader.getParent(); 519 520 return PlasticInternalUtils.readBytecodeForClass(parentClassLoader, className, true); 521 } 522 523 public PlasticClassTransformation createTransformation(String baseClassName, String newClassName) 524 { 525 return createTransformation(baseClassName, newClassName, null); 526 } 527 528 public PlasticClassTransformation createTransformation(String baseClassName, String newClassName, String implementationClassName) 529 { 530 try 531 { 532 ClassNode newClassNode = new ClassNode(); 533 534 final String internalNewClassNameinternalName = PlasticInternalUtils.toInternalName(newClassName); 535 final String internalBaseClassName = PlasticInternalUtils.toInternalName(baseClassName); 536 newClassNode.visit(PlasticConstants.DEFAULT_VERSION_OPCODE, ACC_PUBLIC, internalNewClassNameinternalName, null, internalBaseClassName, null); 537 538 ClassNode implementationClassNode = null; 539 540 if (implementationClassName != null) 541 { 542 // When decorating or advising a service, implementationClassName is the name 543 // of a proxy class already, such as "$ServiceName_[random string]", 544 // which doesn't exist as a file in the classpath, just in memory. 545 // So we need to keep what's the original implementation class name 546 // for each proxy, even a proxy around a proxy. 547 if (transformedClassNameToImplementationClassName.containsKey(implementationClassName)) 548 { 549 implementationClassName = 550 transformedClassNameToImplementationClassName.get(implementationClassName); 551 } 552 553 if (!implementationClassName.startsWith("com.sun.proxy")) 554 { 555 556 try 557 { 558 implementationClassNode = readClassNode(implementationClassName); 559 } catch (IOException e) 560 { 561 LOGGER.warn(String.format("Unable to load class %s as the implementation of service %s", 562 implementationClassName, baseClassName)); 563 // Go on. Probably a proxy class 564 } 565 566 } 567 568 transformedClassNameToImplementationClassName.put(newClassName, implementationClassName); 569 570 } 571 572 return createTransformation(baseClassName, newClassNode, implementationClassNode, true); 573 } catch (ClassNotFoundException ex) 574 { 575 throw new RuntimeException(String.format("Unable to create class %s as sub-class of %s: %s", newClassName, 576 baseClassName, PlasticInternalUtils.toMessage(ex)), ex); 577 } 578 579 } 580 581 private ClassNode readClassNode(String className) throws IOException 582 { 583 return readClassNode(className, getClassLoader()); 584 } 585 586 static ClassNode readClassNode(String className, ClassLoader classLoader) throws IOException 587 { 588 ClassNode classNode = new ClassNode(); 589 final String location = PlasticInternalUtils.toInternalName(className) + ".class"; 590 InputStream inputStream = classLoader.getResourceAsStream(location); 591 BufferedInputStream bis = new BufferedInputStream(inputStream); 592 ClassReader classReader = new ClassReader(inputStream); 593 inputStream.close(); 594 bis.close(); 595 classReader.accept(classNode, 0); 596 return classNode; 597 598 } 599 600 public ClassInstantiator getClassInstantiator(String className) 601 { 602 synchronized (loader) 603 { 604 if (!instantiators.containsKey(className)) 605 { 606 try 607 { 608 loader.loadClass(className); 609 } catch (ClassNotFoundException ex) 610 { 611 throw new RuntimeException(ex); 612 } 613 } 614 615 ClassInstantiator result = instantiators.get(className); 616 617 if (result == null) 618 { 619 // TODO: Verify that the problem is incorrect package, and not any other failure. 620 621 StringBuilder b = new StringBuilder(); 622 b.append("Class '") 623 .append(className) 624 .append("' is not a transformed class. Transformed classes should be in one of the following packages: "); 625 626 String sep = ""; 627 628 List<String> names = new ArrayList<String>(controlledPackages); 629 Collections.sort(names); 630 631 for (String name : names) 632 { 633 b.append(sep); 634 b.append(name); 635 636 sep = ", "; 637 } 638 639 String message = b.append('.').toString(); 640 641 throw new IllegalArgumentException(message); 642 } 643 644 return result; 645 } 646 } 647 648 TypeCategory getTypeCategory(String typeName) 649 { 650 synchronized (loader) 651 { 652 // TODO: Is this the right place to cache this data? 653 654 return typeName2Category.get(typeName); 655 } 656 } 657 658 @Override 659 public void addPlasticClassListener(PlasticClassListener listener) 660 { 661 assert listener != null; 662 663 listeners.add(listener); 664 } 665 666 @Override 667 public void removePlasticClassListener(PlasticClassListener listener) 668 { 669 assert listener != null; 670 671 listeners.remove(listener); 672 } 673 674 boolean isEnabled(TransformationOption option) 675 { 676 return options.contains(option); 677 } 678 679 680 void setFieldReadInstrumentation(String classInternalName, String fieldName, FieldInstrumentation fi) 681 { 682 instrumentations.get(classInternalName).read.put(fieldName, fi); 683 } 684 685 686 private FieldInstrumentations getFieldInstrumentations(String classInternalName) 687 { 688 FieldInstrumentations result = instrumentations.get(classInternalName); 689 690 if (result != null) 691 { 692 return result; 693 } 694 695 String className = PlasticInternalUtils.toClassName(classInternalName); 696 697 // If it is a top-level (not inner) class in a controlled package, then we 698 // will recursively load the class, to identify any field instrumentations 699 // in it. 700 if (!className.contains("$") && shouldInterceptClassLoading(className)) 701 { 702 try 703 { 704 loadAndTransformClass(className); 705 706 // The key is written into the instrumentations map as a side-effect 707 // of loading the class. 708 return instrumentations.get(classInternalName); 709 } catch (Exception ex) 710 { 711 throw new RuntimeException(PlasticInternalUtils.toMessage(ex), ex); 712 } 713 } 714 715 // Either a class outside of controlled packages, or an inner class. Use a placeholder 716 // that contains empty maps. 717 718 result = placeholder; 719 instrumentations.put(classInternalName, result); 720 721 return result; 722 } 723 724 FieldInstrumentation getFieldInstrumentation(String ownerClassInternalName, String fieldName, boolean forRead) 725 { 726 String currentName = ownerClassInternalName; 727 728 while (true) 729 { 730 731 if (currentName == null) 732 { 733 return null; 734 } 735 736 FieldInstrumentations instrumentations = getFieldInstrumentations(currentName); 737 738 FieldInstrumentation instrumentation = instrumentations.get(fieldName, forRead); 739 740 if (instrumentation != null) 741 { 742 return instrumentation; 743 } 744 745 currentName = instrumentations.superClassInternalName; 746 } 747 } 748 749 750 void setFieldWriteInstrumentation(String classInternalName, String fieldName, FieldInstrumentation fi) 751 { 752 instrumentations.get(classInternalName).write.put(fieldName, fi); 753 } 754 755 boolean isCheckedException(String exceptionName) 756 { 757 Boolean cached = checkedExceptionCache.get(exceptionName); 758 759 if (cached != null) 760 { 761 return cached; 762 } 763 764 try 765 { 766 Class asClass = getClassLoader().loadClass(exceptionName); 767 768 769 boolean checked = !(Error.class.isAssignableFrom(asClass) || 770 RuntimeException.class.isAssignableFrom(asClass)); 771 772 checkedExceptionCache.put(exceptionName, checked); 773 774 return checked; 775 } catch (Exception e) 776 { 777 throw new RuntimeException(e); 778 } 779 } 780} 781 782