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.newConcurrentMap();
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, false));
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        ClassInstantiator result = instantiators.get(className);
603
604        if (result == null)
605        {
606            try
607            {
608                loader.loadClass(className);
609                result = instantiators.get(className);
610            } catch (ClassNotFoundException ex)
611            {
612                throw new RuntimeException(ex);
613            }
614        }
615
616
617        if (result != null)
618        {
619            return result;
620        }
621
622        // TODO: Verify that the problem is incorrect package, and not any other failure.
623
624        StringBuilder b = new StringBuilder();
625        b.append("Class '")
626                .append(className)
627                .append("' is not a transformed class. Transformed classes should be in one of the following packages: ");
628
629        String sep = "";
630
631        List<String> names = new ArrayList<String>(controlledPackages);
632        Collections.sort(names);
633
634        for (String name : names)
635        {
636            b.append(sep);
637            b.append(name);
638
639            sep = ", ";
640        }
641
642        String message = b.append('.').toString();
643
644        throw new IllegalArgumentException(message);
645    }
646
647    TypeCategory getTypeCategory(String typeName)
648    {
649        synchronized (loader)
650        {
651            // TODO: Is this the right place to cache this data?
652
653            return typeName2Category.get(typeName);
654        }
655    }
656
657    @Override
658    public void addPlasticClassListener(PlasticClassListener listener)
659    {
660        assert listener != null;
661
662        listeners.add(listener);
663    }
664
665    @Override
666    public void removePlasticClassListener(PlasticClassListener listener)
667    {
668        assert listener != null;
669
670        listeners.remove(listener);
671    }
672
673    boolean isEnabled(TransformationOption option)
674    {
675        return options.contains(option);
676    }
677
678
679    void setFieldReadInstrumentation(String classInternalName, String fieldName, FieldInstrumentation fi)
680    {
681        instrumentations.get(classInternalName).read.put(fieldName, fi);
682    }
683
684
685    private FieldInstrumentations getFieldInstrumentations(String classInternalName)
686    {
687        FieldInstrumentations result = instrumentations.get(classInternalName);
688
689        if (result != null)
690        {
691            return result;
692        }
693
694        String className = PlasticInternalUtils.toClassName(classInternalName);
695
696        // If it is a top-level (not inner) class in a controlled package, then we
697        // will recursively load the class, to identify any field instrumentations
698        // in it.
699        if (!className.contains("$") && shouldInterceptClassLoading(className))
700        {
701            try
702            {
703                loadAndTransformClass(className);
704
705                // The key is written into the instrumentations map as a side-effect
706                // of loading the class.
707                return instrumentations.get(classInternalName);
708            } catch (Exception ex)
709            {
710                throw new RuntimeException(PlasticInternalUtils.toMessage(ex), ex);
711            }
712        }
713
714        // Either a class outside of controlled packages, or an inner class. Use a placeholder
715        // that contains empty maps.
716
717        result = placeholder;
718        instrumentations.put(classInternalName, result);
719
720        return result;
721    }
722
723    FieldInstrumentation getFieldInstrumentation(String ownerClassInternalName, String fieldName, boolean forRead)
724    {
725        String currentName = ownerClassInternalName;
726
727        while (true)
728        {
729
730            if (currentName == null)
731            {
732                return null;
733            }
734
735            FieldInstrumentations instrumentations = getFieldInstrumentations(currentName);
736
737            FieldInstrumentation instrumentation = instrumentations.get(fieldName, forRead);
738
739            if (instrumentation != null)
740            {
741                return instrumentation;
742            }
743
744            currentName = instrumentations.superClassInternalName;
745        }
746    }
747
748
749    void setFieldWriteInstrumentation(String classInternalName, String fieldName, FieldInstrumentation fi)
750    {
751        instrumentations.get(classInternalName).write.put(fieldName, fi);
752    }
753
754    boolean isCheckedException(String exceptionName)
755    {
756        Boolean cached = checkedExceptionCache.get(exceptionName);
757
758        if (cached != null)
759        {
760            return cached;
761        }
762
763        try
764        {
765            Class asClass = getClassLoader().loadClass(exceptionName);
766
767
768            boolean checked = !(Error.class.isAssignableFrom(asClass) ||
769                    RuntimeException.class.isAssignableFrom(asClass));
770
771            checkedExceptionCache.put(exceptionName, checked);
772
773            return checked;
774        } catch (Exception e)
775        {
776            throw new RuntimeException(e);
777        }
778    }
779}
780
781