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