001    // Copyright 2006, 2007, 2008 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 javassist.*;
018    import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newMap;
019    import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newSet;
020    import org.apache.tapestry5.ioc.internal.util.Defense;
021    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
022    import org.apache.tapestry5.ioc.services.ClassFab;
023    import org.apache.tapestry5.ioc.services.ClassFabUtils;
024    import org.apache.tapestry5.ioc.services.MethodIterator;
025    import org.apache.tapestry5.ioc.services.MethodSignature;
026    import org.slf4j.Logger;
027    
028    import static java.lang.String.format;
029    import java.lang.reflect.Modifier;
030    import java.util.Formatter;
031    import java.util.Map;
032    import java.util.Set;
033    
034    /**
035     * Implementation of {@link org.apache.tapestry5.ioc.services.ClassFab}. Hides, as much as possible, the underlying
036     * library (Javassist).
037     */
038    public class ClassFabImpl extends AbstractFab implements ClassFab
039    {
040        private static final Map<Class, String> DEFAULT_RETURN = newMap();
041    
042        static
043        {
044            DEFAULT_RETURN.put(boolean.class, "false");
045            DEFAULT_RETURN.put(long.class, "0L");
046            DEFAULT_RETURN.put(float.class, "0.0f");
047            DEFAULT_RETURN.put(double.class, "0.0d");
048        }
049    
050        /**
051         * Add fields, methods, and constructors are added, their psuedo-code is appended to this description, which is used
052         * by toString().
053         */
054        private final StringBuilder description = new StringBuilder();
055    
056        private final Formatter formatter = new Formatter(description);
057    
058        private final Set<MethodSignature> addedSignatures = newSet();
059    
060        public ClassFabImpl(CtClassSource source, CtClass ctClass, Logger logger)
061        {
062            super(source, ctClass, logger);
063        }
064    
065        /**
066         * Returns a representation of the fabricated class, including inheritance, fields, constructors, methods and method
067         * bodies.
068         */
069        @Override
070        public String toString()
071        {
072            StringBuilder buffer = new StringBuilder("ClassFab[\n");
073    
074            try
075            {
076                buffer.append(buildClassAndInheritance());
077    
078                buffer.append(description.toString());
079            }
080            catch (Exception ex)
081            {
082                buffer.append(" *** ");
083                buffer.append(ex);
084            }
085    
086            buffer.append("\n]");
087    
088            return buffer.toString();
089        }
090    
091        private String buildClassAndInheritance() throws NotFoundException
092        {
093            StringBuilder buffer = new StringBuilder();
094    
095            buffer.append(Modifier.toString(getCtClass().getModifiers()));
096            buffer.append(" class ");
097            buffer.append(getName());
098            buffer.append(" extends ");
099            buffer.append(getCtClass().getSuperclass().getName());
100            buffer.append("\n");
101    
102            CtClass[] interfaces = getCtClass().getInterfaces();
103    
104            if (interfaces.length > 0)
105            {
106                buffer.append("  implements ");
107    
108                for (int i = 0; i < interfaces.length; i++)
109                {
110                    if (i > 0) buffer.append(", ");
111    
112                    buffer.append(interfaces[i].getName());
113                }
114    
115                buffer.append("\n\n");
116            }
117    
118            return buffer.toString();
119        }
120    
121        /**
122         * Returns the name of the class fabricated by this instance.
123         */
124        String getName()
125        {
126            return getCtClass().getName();
127        }
128    
129        public void addField(String name, Class type)
130        {
131            addField(name, Modifier.PRIVATE, type);
132        }
133    
134        public void addField(String name, int modifiers, Class type)
135        {
136            lock.check();
137    
138            CtClass ctType = toCtClass(type);
139    
140            try
141            {
142                CtField field = new CtField(ctType, name, getCtClass());
143                field.setModifiers(modifiers);
144    
145                getCtClass().addField(field);
146            }
147            catch (CannotCompileException ex)
148            {
149                // Have yet to find a way to make this happen!
150                throw new RuntimeException(ServiceMessages.unableToAddField(name, getCtClass(), ex), ex);
151            }
152    
153            formatter.format("%s %s %s;\n\n", Modifier.toString(modifiers), ClassFabUtils
154                    .toJavaClassName(type), name);
155        }
156    
157        public void proxyMethodsToDelegate(Class serviceInterface, String delegateExpression,
158                                           String toString)
159        {
160            lock.check();
161    
162            addInterface(serviceInterface);
163    
164            MethodIterator mi = new MethodIterator(serviceInterface);
165    
166            while (mi.hasNext())
167            {
168                MethodSignature sig = mi.next();
169    
170                // ($r) properly handles void methods for us, which keeps this simple.
171    
172                String body = format("return ($r) %s.%s($$);", delegateExpression, sig.getName());
173    
174                addMethod(Modifier.PUBLIC, sig, body);
175            }
176    
177            if (!mi.getToString()) addToString(toString);
178        }
179    
180        public void addToString(String toString)
181        {
182            lock.check();
183    
184            MethodSignature sig = new MethodSignature(String.class, "toString", null, null);
185    
186            // TODO: Very simple quoting here, will break down if the string itself contains
187            // double quotes or various other characters that need escaping.
188    
189            addMethod(Modifier.PUBLIC, sig, format("return \"%s\";", toString));
190        }
191    
192        public void addMethod(int modifiers, MethodSignature ms, String body)
193        {
194            lock.check();
195    
196            if (addedSignatures.contains(ms))
197                throw new RuntimeException(ServiceMessages.duplicateMethodInClass(ms, this));
198    
199            CtClass ctReturnType = toCtClass(ms.getReturnType());
200    
201            CtClass[] ctParameters = toCtClasses(ms.getParameterTypes());
202            CtClass[] ctExceptions = toCtClasses(ms.getExceptionTypes());
203    
204            CtMethod method = new CtMethod(ctReturnType, ms.getName(), ctParameters, getCtClass());
205    
206            try
207            {
208                method.setModifiers(modifiers);
209                method.setBody(body);
210                method.setExceptionTypes(ctExceptions);
211    
212                getCtClass().addMethod(method);
213            }
214            catch (Exception ex)
215            {
216                throw new RuntimeException(ServiceMessages.unableToAddMethod(ms, getCtClass(), ex), ex);
217            }
218    
219            addedSignatures.add(ms);
220    
221            // modifiers, return type, name
222    
223            formatter.format("%s %s %s", Modifier.toString(modifiers), ClassFabUtils
224                    .toJavaClassName(ms.getReturnType()), ms.getName());
225    
226            // parameters, exceptions and body from this:
227            addMethodDetailsToDescription(ms.getParameterTypes(), ms.getExceptionTypes(), body);
228    
229            description.append("\n\n");
230        }
231    
232        public void addNoOpMethod(MethodSignature signature)
233        {
234            lock.check();
235    
236            Class returnType = signature.getReturnType();
237    
238            if (returnType.equals(void.class))
239            {
240                addMethod(Modifier.PUBLIC, signature, "return;");
241                return;
242            }
243    
244            String value = "null";
245            if (returnType.isPrimitive())
246            {
247                value = DEFAULT_RETURN.get(returnType);
248                if (value == null) value = "0";
249            }
250    
251            addMethod(Modifier.PUBLIC, signature, "return " + value + ";");
252        }
253    
254        public void addConstructor(Class[] parameterTypes, Class[] exceptions, String body)
255        {
256            Defense.notBlank(body, "body");
257    
258            lock.check();
259    
260            CtClass[] ctParameters = toCtClasses(parameterTypes);
261            CtClass[] ctExceptions = toCtClasses(exceptions);
262    
263            try
264            {
265                CtConstructor constructor = new CtConstructor(ctParameters, getCtClass());
266                constructor.setExceptionTypes(ctExceptions);
267                constructor.setBody(body);
268    
269                getCtClass().addConstructor(constructor);
270            }
271            catch (Exception ex)
272            {
273                throw new RuntimeException(ServiceMessages.unableToAddConstructor(getCtClass(), ex), ex);
274            }
275    
276            description.append("public ");
277    
278            // This isn't quite right; we should strip the package portion off of the name.
279            // However, fabricated classes are almost always in the "default" package, so
280            // this is OK.
281    
282            description.append(getName());
283    
284            addMethodDetailsToDescription(parameterTypes, exceptions, body);
285    
286            description.append("\n\n");
287        }
288    
289        /**
290         * Adds a listing of method (or constructor) parameters and thrown exceptions, and the body, to the description
291         *
292         * @param parameterTypes types of method parameters, or null
293         * @param exceptions     types of throw exceptions, or null
294         * @param body           body of method or constructor
295         */
296        private void addMethodDetailsToDescription(Class[] parameterTypes, Class[] exceptions,
297                                                   String body)
298        {
299            description.append("(");
300    
301            int count = InternalUtils.size(parameterTypes);
302            for (int i = 0; i < count; i++)
303            {
304                if (i > 0) description.append(", ");
305    
306                description.append(ClassFabUtils.toJavaClassName(parameterTypes[i]));
307    
308                description.append(" $");
309                description.append(i + 1);
310            }
311    
312            description.append(")");
313    
314            count = InternalUtils.size(exceptions);
315            for (int i = 0; i < count; i++)
316            {
317                if (i == 0)
318                    description.append("\n  throws ");
319                else
320                    description.append(", ");
321    
322                // Since this can never be an array type, we don't need to use getJavaClassName
323    
324                description.append(exceptions[i].getName());
325            }
326    
327            description.append("\n");
328            description.append(body);
329        }
330    }