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