    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}