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