KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > bsf > smartValueObject > tools > JavaAssistInstrumentor


1 package org.bsf.smartValueObject.tools;
2
3 import javassist.*;
4 import org.apache.commons.logging.Log;
5 import org.apache.commons.logging.LogFactory;
6
7 import java.io.*;
8 import java.util.Properties JavaDoc;
9
10 /**
11  * Javassist specific implementation.
12  * <p>This class makes heavy use of advanced javassist features
13  * like runtime compilation.
14  *
15  * @see org.bsf.smartValueObject.tools.Instrumentor
16  * @see <a HREF="http://www.csg.is.titech.ac.jp/~chiba/javassist/">Javassist homepage</a>
17  */

18 public class JavaAssistInstrumentor implements Instrumentor {
19     private static final Log log = LogFactory.getLog(JavaAssistInstrumentor.class);
20     /** Default pool to obtain CtClasses from. */
21     private static final ClassPool pool;
22     /** The codeconverter to be used to change field access. */
23     private static final CodeConverter converter = new CodeConverter();
24     /** Custom classloader to define classes at runtime. */
25     private static InstClassLoader instCL = new InstClassLoader(JavaAssistInstrumentor.class.getClassLoader());
26     /** A The modified class in javassist's representation. */
27     private CtClass ctclass = null;
28     /** The trap method to be used for field access. */
29     private CtMethod standardTrap = null;
30
31     static {
32         // default classpool = java.lang.Object.class.getClassLoader()
33
pool = ClassPool.getDefault();
34         pool.insertClassPath(new LoaderClassPath(JavaAssistInstrumentor.class.getClassLoader()));
35     }
36
37     /**
38      * Creates new instance. Use <tt>modifyClass()</tt> to do the
39      * actual modification.
40      */

41     public JavaAssistInstrumentor() {
42     }
43
44     /**
45      * The instrumentor is initialized by this constructor.
46      * @param name class to be modified, either as path or in package notation.
47      * @throws org.bsf.smartValueObject.tools.InstrumentorException when encountering problems while loading/
48      * modifying the class.
49      */

50     public JavaAssistInstrumentor(String JavaDoc name) throws InstrumentorException {
51         modifyClass(name);
52     }
53
54     /**
55      * Added for convenience.
56      * @param clazz to be modified
57      * @throws org.bsf.smartValueObject.tools.InstrumentorException
58      */

59     public JavaAssistInstrumentor(Class JavaDoc clazz) throws InstrumentorException {
60         this(clazz.getName());
61     }
62
63     public void modifyClass(String JavaDoc name) throws InstrumentorException {
64         if (name.endsWith(".class")) {
65             name = fileToClass(name);
66         }
67
68         try {
69             ctclass = pool.get(name);
70             if (alreadyModified(ctclass)) {
71                 return;
72             } else {
73                 modifyClass(ctclass);
74             }
75         } catch (Exception JavaDoc e) {
76             throw new InstrumentorException("JavaAssistInstrumentor: error while transforming", e);
77         }
78     }
79
80     public void modifyClass(String JavaDoc basedir, String JavaDoc file) throws InstrumentorException {
81         try {
82             FileInputStream fis = new FileInputStream(new File(basedir, file));
83             byte[] bytecode = readStream(fis);
84             pool.insertClassPath(new ByteArrayClassPath(fileToClass(file), bytecode));
85             pool.appendClassPath(basedir);
86         } catch (NotFoundException e) {
87             throw new InstrumentorException(e);
88         } catch (IOException e) {
89             throw new InstrumentorException(e);
90         }
91         modifyClass(file);
92     }
93
94     public byte[] getBytecode() throws InstrumentorException {
95         if (ctclass == null) throw new IllegalStateException JavaDoc("use modifyClass first");
96         try {
97             return ctclass.toBytecode();
98         } catch (Exception JavaDoc e) {
99             throw new InstrumentorException(e);
100         }
101     }
102
103     public Class JavaDoc defineClass() {
104         return instCL.loadClass(ctclass);
105     }
106
107     /**
108      * Applies all necessary modifications to make class versionable.
109      * @param cc class to be modified.
110      * @return modified class.
111      */

112     private CtClass modifyClass(CtClass cc) throws InstrumentorException {
113         log.debug("modifyClass: " + cc);
114
115         try {
116             standardTrap = createTrapWrite(cc);
117             addFieldInterceptors(cc);
118             makeFieldsPublic(cc);
119             makeVersionable(cc);
120             cc.instrument(converter);
121         } catch (Exception JavaDoc e) {
122             log.warn("exeception while modifying", e);
123             throw new InstrumentorException(e);
124         }
125
126         return cc;
127     }
128
129     /**
130      * Makes fields public.
131      * @param cc
132      */

133     private void makeFieldsPublic(CtClass cc) {
134         CtField[] fields = cc.getDeclaredFields();
135         for (int i = 0; i < fields.length; i++) {
136             CtField field = fields[i];
137             if ((field.getModifiers() & Modifier.STATIC) == 0)
138                 field.setModifiers(Modifier.PUBLIC);
139         }
140     }
141
142     /**
143      * Makes class versionable.
144      * Adds version field, implements VERSIONINTERFACE by delegating
145      * all the methods to it.
146      *
147      * @see org.bsf.smartValueObject.tools.Instrumentor#VERSIONINTERFACE
148      * @see org.bsf.smartValueObject.tools.Instrumentor#VERSIONFIELD
149      */

150     private void makeVersionable(CtClass cc)
151             throws CannotCompileException, NotFoundException, InstrumentorException {
152         CtField versionField = addVersionField(cc);
153         CtClass versionInterface = pool.get(VERSIONINTERFACE);
154         addDelegations(versionInterface, versionField, cc);
155         cc.addInterface(versionInterface);
156     }
157
158     /**
159      * Generic method to implement an interface by delegation.
160      * <p>E.g. <code>declaring.isDirty()</code> ==>
161      * <code>declaring.field.isDirty()</code>.
162      *
163      * @param iface interface to implement.
164      * @param field field to delegate to.
165      * @param declaring class to add interface to.
166      */

167     private void addDelegations(CtClass iface, CtField field, CtClass declaring)
168             throws InstrumentorException, NotFoundException, CannotCompileException {
169
170         if (!iface.isInterface()) {
171             throw new InstrumentorException("need Interface");
172         }
173         if (!field.getType().subtypeOf(iface)) {
174             throw new InstrumentorException("field doesn't implement interface");
175         }
176
177         // use all methods as declared by the interface
178
CtMethod[] methods = iface.getDeclaredMethods();
179         for (int i = 0; i < methods.length; i++) {
180             CtMethod method = methods[i];
181             StringBuffer JavaDoc body = new StringBuffer JavaDoc();
182             if (method.getReturnType() != CtClass.voidType) {
183                 body.append("return ");
184             }
185
186             // ($$) javassist specific macro (expanded to parameters)
187
// field.methodname(1st parameter, 2nd parameter...)
188
body.append(field.getName() + (".") + method.getName() + "($$);");
189
190             // make new method using the signature of the interface's method
191
// and the body defined above, add it to class
192
CtMethod newMethod = CtNewMethod.make(
193                     method.getReturnType(),
194                     method.getName(),
195                     method.getParameterTypes(),
196                     method.getExceptionTypes(),
197                     body.toString(),
198                     declaring);
199             declaring.addMethod(newMethod);
200         }
201     }
202
203     /** Adds interceptors to all fields declared in cc. */
204     private void addFieldInterceptors(CtClass cc)
205             throws NotFoundException, CannotCompileException {
206         CtField[] fields = cc.getDeclaredFields();
207
208         for (int i = 0; i < fields.length; i++) {
209             if ((fields[i].getModifiers() & Modifier.STATIC) != 0) {
210                 continue;
211             }
212
213             addFieldInterceptor(fields[i], SMARTCONTAINERS);
214         }
215     }
216
217     /**
218      * Adds an interceptor to a field (for write access).
219      * Implemented by 'wrapping around' a trap method for type safety.
220      * Because field interception can only be intercepted by calling
221      * a static method we need to implement a static wrapper called 'trap'
222      * which will finally do an interception on the object itself.
223      *
224      * <p>write access to field foo of type Bar will result in:
225      * <code>static write_foo(Object o, Bar bar) { trap_method };</code>
226      *
227      * @param field field to be intercepted.
228      * @param ifaces interfaces with their 'smart' replacements.
229      */

230     private void addFieldInterceptor(CtField field, Properties JavaDoc ifaces)
231             throws NotFoundException, CannotCompileException {
232         String JavaDoc name = field.getName();
233         CtClass cc = field.getDeclaringClass();
234         log.debug("addFieldInterceptor: " + name);
235
236         CtMethod trap;
237         String JavaDoc fieldtype = field.getType().getName();
238         String JavaDoc replacement = ifaces.getProperty(fieldtype);
239
240         // create special traps if we assign to interfaces with 'smart'
241
// replacements
242
if (replacement != null) {
243             trap = createTrapWriteGeneric(cc, fieldtype, replacement);
244         } else {
245             // else use standard trap
246
trap = standardTrap;
247         }
248
249         CtClass[] writeParam = new CtClass[2];
250         writeParam[0] = pool.get("java.lang.Object");
251         writeParam[1] = field.getType();
252         CtMethod method = CtNewMethod.wrapped(
253                 CtClass.voidType, // return type
254
fieldWrite(name), // name of method
255
writeParam, // params
256
null, // execeptions
257
trap, // body
258
CtMethod.ConstParameter.string(name), // const parameter (String/int/null)
259
cc); // declaring class
260
// needed because replaceFieldWrite dispatches
261
// only to static methods
262
method.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
263         cc.addMethod(method);
264
265         converter.replaceFieldWrite(field, cc, fieldWrite(name));
266     }
267
268     /** Convention to name methods. */
269     private static String JavaDoc fieldWrite(String JavaDoc name) {
270         return "write_" + name;
271     }
272
273     /**
274      * Adds a version field to the class.
275      * @param cc target class.
276      * @return the version field.
277      * @throws javassist.CannotCompileException
278      * @throws javassist.NotFoundException
279      * @see org.bsf.smartValueObject.tools.Instrumentor#VERSIONFIELD
280      * @see #createVersionField(javassist.CtClass declaring)
281      */

282     private CtField addVersionField(CtClass cc)
283             throws CannotCompileException, NotFoundException {
284         log.debug("addVersionField: " + cc);
285         CtField field;
286
287         field = createVersionField(cc);
288         // add field to class, it will be initialised by 'new' on runtime
289
// e.g. Version version = new Version();
290
cc.addField(field, CtField.Initializer.byNew(pool.get(VERSIONCLASS)));
291
292         return field;
293     }
294
295     /**
296      * Creates the version field.
297      *
298      * @see #addVersionField(javassist.CtClass cc)
299      */

300     private CtField createVersionField(CtClass declaring)
301             throws CannotCompileException, NotFoundException {
302         String JavaDoc name = VERSIONFIELD;
303         CtClass type = pool.get(VERSIONCLASS);
304         CtField field = new CtField(type, name, declaring);
305         field.setModifiers(Modifier.PUBLIC);
306
307         return field;
308     }
309
310     /**
311      * Creates a 'trap' for interception.
312      * The created 'trap' will call VERSIONMETHOD (e.g. 'touch')
313      * on the object and set the field using reflection. In case of fields in
314      * the java.lang.* package or primitive types we do an invocation of the
315      * equals method to verify if a real change has taken place or if the field
316      * already contains the value. In this case the object will not be
317      * marked as 'dirty'.
318      *
319      * @param cc target class.
320      * @return trap method.
321      * @see #addFieldInterceptor
322      */

323     private CtMethod createTrapWrite(CtClass cc)
324             throws CannotCompileException {
325         String JavaDoc classname = cc.getName();
326
327         String JavaDoc body =
328                 "protected static Object trapWrite(Object[] args, String name) {" +
329                 classname + " foo = (" + classname + ") args[0];" +
330                 "try {" +
331                 " java.lang.reflect.Field field = foo.getClass().getField(name);" +
332                 " if (" + VERSIONHELPER + ".doEquals(field)) {" +
333                 " if (field.get(foo) != null && field.get(foo).equals(args[1]))" +
334                 " return null;" +
335                 " else if (args[1] == null)" +
336                 " return null;" +
337                 " }" +
338                 " field.set(foo, args[1]); " +
339                 " ((" + VERSIONINTERFACE + ")foo)." + VERSIONMETHOD + "(field.getName());" +
340                 "} catch (Throwable t) { throw new RuntimeException(t); } " +
341                 "return null;" +
342                 "}";
343
344         log.debug(body);
345         return CtNewMethod.make(body, cc);
346     }
347
348     /**
349      * To replace assignment to specific interfaces by a wrapped version.
350      * This allows for Collections doing versionable transactions (remove,...).
351      * Assignments to fields of type 'dumb' are replaced by <code>new SmartXXX(dumb,
352      * versionable)</code>.
353      * <p>By using 'versionable' as the second parameter, the newly created object gets
354      * a reference to the version state of its parent.
355      *
356      * @param cc target class.
357      * @param dumb package name of class to replace.
358      * @param smart package name of replacing class.
359      * @return trap method.
360      */

361     private CtMethod createTrapWriteGeneric(CtClass cc, String JavaDoc dumb, String JavaDoc smart)
362             throws CannotCompileException {
363         String JavaDoc classname = cc.getName();
364         int lastDot = dumb.lastIndexOf('.');
365         String JavaDoc suffix;
366         if (lastDot != -1) {
367             suffix = dumb.substring(lastDot + 1);
368         } else {
369             suffix = dumb;
370         }
371
372         String JavaDoc body =
373                 "protected static Object trapWrite" + suffix + "(Object[] args, String name) {" +
374                 classname + " foo = (" + classname + ") args[0];" +
375                 "try { " +
376                 " java.lang.reflect.Field field = foo.getClass().getField(name);" +
377                 dumb + " o = new " + smart +
378                 " ((" + dumb + ") args[1]," +
379                 // using version object of parent
380
// " (" + VERSIONINTERFACE + ") foo);" +
381
// use new version object
382
" new " + VERSIONCLASS + "());" +
383                 " field.set(foo, o); " +
384                 "} catch (Throwable t) {} " +
385                 "return null;" +
386                 "}";
387
388         log.debug(body);
389         return CtNewMethod.make(body, cc);
390     }
391
392     // --------------------------------------------------------------------------
393
/**
394      * Custom ClassLoader using <tt>CtClass</tt>. This ClassLoader can be used
395      * to define modified classes. It exists mainly for testing purposes.
396      */

397     private static class InstClassLoader extends ClassLoader JavaDoc {
398         /**
399          * Creates new ClassLoader.
400          * @param c the parent classloader.
401          */

402         public InstClassLoader(ClassLoader JavaDoc c) {
403             super(c);
404         }
405
406         public Class JavaDoc loadClass(CtClass cc) throws ClassFormatError JavaDoc {
407             byte[] bytecode;
408             try {
409                 bytecode = cc.toBytecode();
410             } catch (Exception JavaDoc e) {
411                 throw new ClassFormatError JavaDoc(e.getMessage());
412             }
413
414             return loadClass(cc.getName(), bytecode);
415         }
416
417         public Class JavaDoc loadClass(String JavaDoc name, byte[] bytecode) throws ClassFormatError JavaDoc {
418             Class JavaDoc c = defineClass(name, bytecode, 0, bytecode.length);
419             resolveClass(c);
420             return c;
421         }
422
423         public Class JavaDoc loadAndDefine(String JavaDoc name) throws
424                 ClassNotFoundException JavaDoc {
425             String JavaDoc cname = name.replace('.', '/') + ".class";
426             InputStream ins = getResourceAsStream(cname);
427             if (ins == null) {
428                 throw new ClassNotFoundException JavaDoc();
429             }
430
431             byte[] bytecode;
432             try {
433                 bytecode = readStream(ins);
434             } catch (IOException e) {
435                 throw new ClassFormatError JavaDoc(e.getMessage());
436             }
437
438             return loadClass(name, bytecode);
439         }
440
441         protected Class JavaDoc findClass(String JavaDoc name) throws ClassNotFoundException JavaDoc {
442             throw new ClassNotFoundException JavaDoc();
443         }
444     }
445
446     /** Helper method to read inputstream in byte array. */
447     private static byte[] readStream(InputStream fin) throws IOException {
448          byte[][] bufs = new byte[8][];
449          int bufsize = 4096;
450
451          for (int i = 0; i < 8; ++i) {
452              bufs[i] = new byte[bufsize];
453              int size = 0;
454              int len;
455              do {
456                  len = fin.read(bufs[i], size, bufsize - size);
457                  if (len >= 0)
458                      size += len;
459                  else {
460                      byte[] result = new byte[bufsize - 4096 + size];
461                      int s = 0;
462                      for (int j = 0; j < i; ++j) {
463                          System.arraycopy(bufs[j], 0, result, s, s + 4096);
464                          s = s + s + 4096;
465                      }
466
467                      System.arraycopy(bufs[i], 0, result, s, size);
468                      return result;
469                  }
470              } while (size < bufsize);
471              bufsize *= 2;
472          }
473
474          throw new IOException("too much data");
475     }
476
477     private static String JavaDoc fileToClass(String JavaDoc filename) {
478         filename = filename.
479                 replace(File.separatorChar, '.').
480                 substring(0, filename.length() - 6);
481         return filename;
482     }
483
484     /** Prevents class from being instrumented twice. */
485     private static boolean alreadyModified(CtClass ctclass) throws NotFoundException {
486         CtClass[] ifaces = ctclass.getInterfaces();
487         for (int i = 0; i < ifaces.length; i++) {
488             CtClass iface = ifaces[i];
489             if (iface.getName().equals(VERSIONINTERFACE))
490                 return true;
491         }
492         return false;
493     }
494 }
495
Popular Tags