    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    }