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    
015    package org.apache.tapestry5.internal.plastic;
016    
017    import org.apache.tapestry5.internal.plastic.asm.ClassReader;
018    import org.apache.tapestry5.internal.plastic.asm.ClassWriter;
019    import org.apache.tapestry5.internal.plastic.asm.Opcodes;
020    import org.apache.tapestry5.internal.plastic.asm.tree.*;
021    import org.apache.tapestry5.plastic.*;
022    
023    import java.lang.annotation.Annotation;
024    import java.lang.reflect.Modifier;
025    import java.util.*;
026    import 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")
033    public 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    }