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