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 }