001    // Copyright 2006, 2007, 2008, 2010 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.ioc.internal.services;
016    
017    import static java.lang.String.format;
018    import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newMap;
019    import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newSet;
020    
021    import java.lang.annotation.Annotation;
022    import java.lang.reflect.Method;
023    import java.lang.reflect.Modifier;
024    import java.util.Formatter;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.Set;
028    
029    import javassist.CannotCompileException;
030    import javassist.CtClass;
031    import javassist.CtConstructor;
032    import javassist.CtField;
033    import javassist.CtMethod;
034    import javassist.NotFoundException;
035    import javassist.bytecode.AnnotationsAttribute;
036    import javassist.bytecode.ClassFile;
037    import javassist.bytecode.ConstPool;
038    import javassist.bytecode.MethodInfo;
039    import javassist.bytecode.ParameterAnnotationsAttribute;
040    import javassist.bytecode.annotation.MemberValue;
041    
042    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
043    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
044    import org.apache.tapestry5.ioc.services.ClassFab;
045    import org.apache.tapestry5.ioc.services.ClassFabUtils;
046    import org.apache.tapestry5.ioc.services.MethodIterator;
047    import org.apache.tapestry5.ioc.services.MethodSignature;
048    import org.slf4j.Logger;
049    
050    /**
051     * Implementation of {@link org.apache.tapestry5.ioc.services.ClassFab}. Hides, as much as possible, the underlying
052     * library (Javassist).
053     */
054    @SuppressWarnings("all")
055    public class ClassFabImpl extends AbstractFab implements ClassFab
056    {
057        private static final Map<Class, String> DEFAULT_RETURN = newMap();
058    
059        static
060        {
061            DEFAULT_RETURN.put(boolean.class, "false");
062            DEFAULT_RETURN.put(long.class, "0L");
063            DEFAULT_RETURN.put(float.class, "0.0f");
064            DEFAULT_RETURN.put(double.class, "0.0d");
065        }
066    
067        /**
068         * Add fields, methods, and constructors are added, their psuedo-code is appended to this description, which is used
069         * by toString().
070         */
071        private final StringBuilder description = new StringBuilder();
072    
073        private final Formatter formatter = new Formatter(description);
074    
075        private final Set<MethodSignature> addedSignatures = newSet();
076    
077        public ClassFabImpl(CtClassSource source, CtClass ctClass, Logger logger)
078        {
079            super(source, ctClass, logger);
080        }
081    
082        /**
083         * Returns a representation of the fabricated class, including inheritance, fields, constructors, methods and method
084         * bodies.
085         */
086        @Override
087        public String toString()
088        {
089            StringBuilder buffer = new StringBuilder("ClassFab[\n");
090    
091            try
092            {
093                buffer.append(buildClassAndInheritance());
094    
095                buffer.append(description.toString());
096            }
097            catch (Exception ex)
098            {
099                buffer.append(" *** ");
100                buffer.append(ex);
101            }
102    
103            buffer.append("\n]");
104    
105            return buffer.toString();
106        }
107    
108        private String buildClassAndInheritance() throws NotFoundException
109        {
110            StringBuilder buffer = new StringBuilder();
111    
112            buffer.append(Modifier.toString(getCtClass().getModifiers()));
113            buffer.append(" class ");
114            buffer.append(getName());
115            buffer.append(" extends ");
116            buffer.append(getCtClass().getSuperclass().getName());
117            buffer.append("\n");
118    
119            CtClass[] interfaces = getCtClass().getInterfaces();
120    
121            if (interfaces.length > 0)
122            {
123                buffer.append("  implements ");
124    
125                for (int i = 0; i < interfaces.length; i++)
126                {
127                    if (i > 0)
128                        buffer.append(", ");
129    
130                    buffer.append(interfaces[i].getName());
131                }
132    
133                buffer.append("\n\n");
134            }
135    
136            return buffer.toString();
137        }
138    
139        /**
140         * Returns the name of the class fabricated by this instance.
141         */
142        String getName()
143        {
144            return getCtClass().getName();
145        }
146    
147        public void addField(String name, Class type)
148        {
149            addField(name, Modifier.PRIVATE, type);
150        }
151    
152        public void addField(String name, int modifiers, Class type)
153        {
154            lock.check();
155    
156            CtClass ctType = toCtClass(type);
157    
158            try
159            {
160                CtField field = new CtField(ctType, name, getCtClass());
161                field.setModifiers(modifiers);
162    
163                getCtClass().addField(field);
164            }
165            catch (CannotCompileException ex)
166            {
167                // Have yet to find a way to make this happen!
168                throw new RuntimeException(ServiceMessages.unableToAddField(name, getCtClass(), ex), ex);
169            }
170    
171            formatter.format("%s %s %s;\n\n", Modifier.toString(modifiers), ClassFabUtils.toJavaClassName(type), name);
172        }
173    
174        public void proxyMethodsToDelegate(Class serviceInterface, String delegateExpression, String toString)
175        {
176            lock.check();
177    
178            addInterface(serviceInterface);
179    
180            MethodIterator mi = new MethodIterator(serviceInterface);
181    
182            while (mi.hasNext())
183            {
184                MethodSignature sig = mi.next();
185    
186                // ($r) properly handles void methods for us, which keeps this simple.
187    
188                String body = format("return ($r) %s.%s($$);", delegateExpression, sig.getName());
189    
190                addMethod(Modifier.PUBLIC, sig, body);
191            }
192    
193            if (!mi.getToString())
194                addToString(toString);
195        }
196    
197        public void addToString(String toString)
198        {
199            lock.check();
200    
201            MethodSignature sig = new MethodSignature(String.class, "toString", null, null);
202    
203            // TODO: Very simple quoting here, will break down if the string itself contains
204            // double quotes or various other characters that need escaping.
205    
206            addMethod(Modifier.PUBLIC, sig, format("return \"%s\";", toString));
207        }
208    
209        public void addMethod(int modifiers, MethodSignature ms, String body)
210        {
211            lock.check();
212    
213            if (addedSignatures.contains(ms))
214                throw new RuntimeException(ServiceMessages.duplicateMethodInClass(ms, this));
215    
216            CtClass ctReturnType = toCtClass(ms.getReturnType());
217    
218            CtClass[] ctParameters = toCtClasses(ms.getParameterTypes());
219            CtClass[] ctExceptions = toCtClasses(ms.getExceptionTypes());
220    
221            CtMethod method = new CtMethod(ctReturnType, ms.getName(), ctParameters, getCtClass());
222    
223            try
224            {
225                method.setModifiers(modifiers);
226                method.setBody(body);
227                method.setExceptionTypes(ctExceptions);
228    
229                getCtClass().addMethod(method);
230            }
231            catch (Exception ex)
232            {
233                throw new RuntimeException(ServiceMessages.unableToAddMethod(ms, getCtClass(), ex), ex);
234            }
235    
236            addedSignatures.add(ms);
237    
238            // modifiers, return type, name
239    
240            formatter.format("%s %s %s", Modifier.toString(modifiers), ClassFabUtils.toJavaClassName(ms.getReturnType()),
241                    ms.getName());
242    
243            // parameters, exceptions and body from this:
244            addMethodDetailsToDescription(ms.getParameterTypes(), ms.getExceptionTypes(), body);
245    
246            description.append("\n\n");
247        }
248    
249        public void addNoOpMethod(MethodSignature signature)
250        {
251            lock.check();
252    
253            Class returnType = signature.getReturnType();
254    
255            if (returnType.equals(void.class))
256            {
257                addMethod(Modifier.PUBLIC, signature, "return;");
258                return;
259            }
260    
261            String value = "null";
262            if (returnType.isPrimitive())
263            {
264                value = DEFAULT_RETURN.get(returnType);
265                if (value == null)
266                    value = "0";
267            }
268    
269            addMethod(Modifier.PUBLIC, signature, "return " + value + ";");
270        }
271    
272        public void addConstructor(Class[] parameterTypes, Class[] exceptions, String body)
273        {
274            assert InternalUtils.isNonBlank(body);
275            lock.check();
276    
277            CtClass[] ctParameters = toCtClasses(parameterTypes);
278            CtClass[] ctExceptions = toCtClasses(exceptions);
279    
280            try
281            {
282                CtConstructor constructor = new CtConstructor(ctParameters, getCtClass());
283                constructor.setExceptionTypes(ctExceptions);
284                constructor.setBody(body);
285    
286                getCtClass().addConstructor(constructor);
287            }
288            catch (Exception ex)
289            {
290                throw new RuntimeException(ServiceMessages.unableToAddConstructor(getCtClass(), ex), ex);
291            }
292    
293            description.append("public ");
294    
295            // This isn't quite right; we should strip the package portion off of the name.
296            // However, fabricated classes are almost always in the "default" package, so
297            // this is OK.
298    
299            description.append(getName());
300    
301            addMethodDetailsToDescription(parameterTypes, exceptions, body);
302    
303            description.append("\n\n");
304        }
305    
306        /**
307         * Adds a listing of method (or constructor) parameters and thrown exceptions, and the body, to the description
308         * 
309         * @param parameterTypes
310         *            types of method parameters, or null
311         * @param exceptions
312         *            types of throw exceptions, or null
313         * @param body
314         *            body of method or constructor
315         */
316        private void addMethodDetailsToDescription(Class[] parameterTypes, Class[] exceptions, String body)
317        {
318            description.append("(");
319    
320            int count = InternalUtils.size(parameterTypes);
321            for (int i = 0; i < count; i++)
322            {
323                if (i > 0)
324                    description.append(", ");
325    
326                description.append(ClassFabUtils.toJavaClassName(parameterTypes[i]));
327    
328                description.append(" $");
329                description.append(i + 1);
330            }
331    
332            description.append(")");
333    
334            count = InternalUtils.size(exceptions);
335            for (int i = 0; i < count; i++)
336            {
337                if (i == 0)
338                    description.append("\n  throws ");
339                else
340                    description.append(", ");
341    
342                // Since this can never be an array type, we don't need to use getJavaClassName
343    
344                description.append(exceptions[i].getName());
345            }
346    
347            description.append("\n");
348            description.append(body);
349        }
350        
351        public void copyClassAnnotationsFromDelegate(Class delegateClass)
352        {
353            lock.check();
354            
355            for (Annotation annotation : delegateClass.getAnnotations())
356            {
357                try
358                {
359                    addAnnotation(annotation);
360                }
361                catch (RuntimeException ex) 
362                {
363                    //Annotation processing may cause exceptions thrown by Javassist. 
364                    //To provide backward compatibility we have to continue even though copying a particular annotation failed.
365                    getLogger().error(String.format("Failed to copy annotation '%s' from '%s'", annotation.annotationType(), delegateClass.getName()));
366                }
367            }   
368        }
369        
370        public void copyMethodAnnotationsFromDelegate(Class serviceInterface, Class delegateClass)
371        {
372            lock.check();
373            
374            for(MethodSignature sig: addedSignatures)
375            {   
376                if(getMethod(sig, serviceInterface) == null)
377                    continue;
378                
379                Method method = getMethod(sig, delegateClass);
380                
381                assert method != null;
382                
383                CtMethod ctMethod = getCtMethod(sig);
384                
385                Annotation[] annotations = method.getAnnotations();
386                
387                for (Annotation annotation : annotations)
388                {   
389                    try
390                    {
391                        addMethodAnnotation(ctMethod, annotation);   
392                    }
393                    catch (RuntimeException ex) 
394                    {
395                        //Annotation processing may cause exceptions thrown by Javassist. 
396                        //To provide backward compatibility we have to continue even though copying a particular annotation failed.
397                        getLogger().error(String.format("Failed to copy annotation '%s' from method '%s' of class '%s'", 
398                                annotation.annotationType(), method.getName(), delegateClass.getName()));
399                    }
400                }
401                
402                try
403                {
404                    addMethodParameterAnnotation(ctMethod, method.getParameterAnnotations());
405                }
406                catch (RuntimeException ex) 
407                {
408                    //Annotation processing may cause exceptions thrown by Javassist. 
409                    //To provide backward compatibility we have to continue even though copying a particular annotation failed.
410                    getLogger().error(String.format("Failed to copy parameter annotations from method '%s' of class '%s'", 
411                                    method.getName(), delegateClass.getName()));
412                }
413            }
414        }
415        
416        private CtMethod getCtMethod(MethodSignature sig)
417        {
418            try
419            {
420                return getCtClass().getDeclaredMethod(sig.getName(), toCtClasses(sig.getParameterTypes()));
421            }
422            catch (NotFoundException e)
423            {
424                throw new RuntimeException(e);
425            }
426        }
427        
428        private Method getMethod(MethodSignature sig, Class clazz)
429        {
430            try
431            {
432                return clazz.getMethod(sig.getName(), sig.getParameterTypes());
433            }
434            catch (Exception e)
435            {
436                return null;
437            }
438        }
439    
440        private void addAnnotation(Annotation annotation)
441        {
442            
443            final ClassFile classFile = getClassFile();
444            
445            AnnotationsAttribute attribute = (AnnotationsAttribute) classFile.getAttribute(AnnotationsAttribute.visibleTag);
446            
447            if (attribute == null)
448            {
449                attribute = new AnnotationsAttribute(getConstPool(), AnnotationsAttribute.visibleTag);
450            }
451            
452            final javassist.bytecode.annotation.Annotation copy = toJavassistAnnotation(annotation);
453            
454            
455            attribute.addAnnotation(copy);
456            
457            classFile.addAttribute(attribute);
458            
459        }
460        
461        private void addMethodAnnotation(final CtMethod ctMethod, final Annotation annotation) {
462    
463            MethodInfo methodInfo = ctMethod.getMethodInfo();
464    
465            AnnotationsAttribute attribute = (AnnotationsAttribute) methodInfo
466                .getAttribute(AnnotationsAttribute.visibleTag);
467    
468            if (attribute == null) {
469                attribute = new AnnotationsAttribute(getConstPool(), AnnotationsAttribute.visibleTag);
470            }
471    
472            final javassist.bytecode.annotation.Annotation copy = toJavassistAnnotation(annotation);
473    
474            attribute.addAnnotation(copy);
475    
476            methodInfo.addAttribute(attribute);
477    
478        }
479    
480        private void addMethodParameterAnnotation(final CtMethod ctMethod, final Annotation[][] parameterAnnotations) {
481    
482            MethodInfo methodInfo = ctMethod.getMethodInfo();
483    
484            ParameterAnnotationsAttribute attribute = (ParameterAnnotationsAttribute) methodInfo
485                .getAttribute(ParameterAnnotationsAttribute.visibleTag);
486    
487            if (attribute == null) {
488                attribute = new ParameterAnnotationsAttribute(getConstPool(), ParameterAnnotationsAttribute.visibleTag);
489            }
490            
491            List<javassist.bytecode.annotation.Annotation[]> result = CollectionFactory.newList();
492            
493            for (Annotation[] next : parameterAnnotations) 
494            {
495                    List<javassist.bytecode.annotation.Annotation> list = CollectionFactory.newList();
496                    
497                            for (Annotation annotation : next) 
498                            {
499                            final javassist.bytecode.annotation.Annotation copy = toJavassistAnnotation(annotation);
500                            
501                            list.add(copy);
502                            }
503                            
504                            result.add(list.toArray(new javassist.bytecode.annotation.Annotation[]{}));
505                    }
506            
507            javassist.bytecode.annotation.Annotation[][] annotations = result.toArray(new javassist.bytecode.annotation.Annotation[][]{});
508            
509            attribute.setAnnotations(annotations);
510            
511            methodInfo.addAttribute(attribute);
512        }
513        
514        private ClassFile getClassFile()
515        {
516            return getCtClass().getClassFile();
517        }
518        
519        private ConstPool getConstPool() 
520        {   
521            return getClassFile().getConstPool();
522        }
523        
524        private javassist.bytecode.annotation.Annotation toJavassistAnnotation(final Annotation source)
525        {
526    
527            final Class<? extends Annotation> annotationType = source.annotationType();
528    
529            final ConstPool constPool = getConstPool();
530    
531            final javassist.bytecode.annotation.Annotation copy = new javassist.bytecode.annotation.Annotation(
532                    annotationType.getName(), constPool);
533    
534            final Method[] methods = annotationType.getDeclaredMethods();
535    
536            for (final Method method : methods)
537            {
538                try
539                {
540                    CtClass ctType = toCtClass(method.getReturnType());
541                    
542                    final MemberValue memberValue = javassist.bytecode.annotation.Annotation.createMemberValue(constPool, ctType);
543                    final Object value = method.invoke(source);
544    
545                    memberValue.accept(new AnnotationMemberValueVisitor(constPool, getSource(), value));
546    
547                    copy.addMemberValue(method.getName(), memberValue);
548                }
549                catch (final Exception e)
550                {
551                    throw new RuntimeException(e);
552                }
553            }
554    
555            return copy;
556        }
557    }