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