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}