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