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