View Javadoc
1   package org.andromda.maven.plugin.andromdapp.script;
2   
3   import java.io.File;
4   import java.net.URL;
5   import javassist.ClassPool;
6   import javassist.CtClass;
7   import javassist.CtField;
8   import javassist.CtMethod;
9   import javassist.LoaderClassPath;
10  import javassist.Modifier;
11  import javassist.NotFoundException;
12  import org.andromda.core.common.ExceptionUtils;
13  import org.apache.commons.lang.StringUtils;
14  
15  /**
16   * This class instruments a given class file in order for it be scripted.  A class modified
17   * by this script generator can have its methods edited and the logic available without having
18   * to redeploy or compile the class.
19   *
20   * @author Chad Brandon
21   */
22  public final class ScriptClassGenerator
23  {
24      /**
25       * The shared instance of this class.
26       */
27      private static ScriptClassGenerator instance;
28  
29      /**
30       * The name of the script wrapper to use.
31       */
32      private String scriptWrapperName;
33  
34      /**
35       * Retrieves an instance of this class and uses the given script wrapper with
36       * the given <code>scriptWrapperName</code>.
37       *
38       * @param scriptWrapperName the fully qualified name of the script wrapper class to use.
39       * @return the instance of this class.
40       */
41      public static final ScriptClassGenerator getInstance(final String scriptWrapperName)
42      {
43          ExceptionUtils.checkEmpty(
44              "scriptWrapperName",
45              scriptWrapperName);
46          instance = new ScriptClassGenerator();
47          instance.scriptWrapperName = scriptWrapperName;
48          return instance;
49      }
50  
51      private ScriptClassGenerator()
52      {
53          // - do not allow instantiation
54      }
55  
56      /**
57       * Modifies the <code>existingClass</code> (basically inserts the script wrapper class into
58       * the class).
59       * @param scriptDirectory the directory in which to find the script.
60       * @param existingClass the class to modify.
61       */
62      public void modifyClass(
63          final String scriptDirectory,
64          final Class existingClass)
65      {
66          try
67          {
68              final String className = existingClass.getName();
69  
70              final ClassPool pool = ClassPool.getDefault();
71              final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
72              if (contextClassLoader != null)
73              {
74                  pool.insertClassPath(new LoaderClassPath(contextClassLoader));
75              }
76              final CtClass ctClass = pool.get(className);
77  
78              // - make sure the class isn't frozen
79              ctClass.defrost();
80  
81              final String scriptWrapperFieldName = "scriptWrapper";
82              try
83              {
84                  ctClass.getField(scriptWrapperFieldName);
85              }
86              catch (Exception exception)
87              {
88                  final CtField scriptWrapper =
89                      new CtField(
90                          convert(
91                              pool,
92                              this.scriptWrapperName),
93                          scriptWrapperFieldName,
94                          ctClass);
95                  scriptWrapper.setModifiers(Modifier.PRIVATE + Modifier.FINAL);
96                  ctClass.addField(
97                      scriptWrapper,
98                      getScriptWrapperInitialization(
99                          scriptDirectory,
100                         className));
101             }
102 
103             final CtMethod[] existingMethods = ctClass.getDeclaredMethods();
104             for (int ctr = 0; ctr < existingMethods.length; ctr++)
105             {
106                 final CtMethod method = existingMethods[ctr];
107                 if (!Modifier.isStatic(method.getModifiers()))
108                 {
109                     final CtClass returnType = method.getReturnType();
110                     String methodBody;
111                     if (returnType.equals(CtClass.voidType))
112                     {
113                         methodBody =
114                                 '{' + constructArgumentString(method) + "scriptWrapper.invoke(\"" + method.getName() +
115                             "\", arguments);}";
116                     }
117                     else
118                     {
119                         if (returnType.isPrimitive())
120                         {
121                             methodBody =
122                                     '{' + constructArgumentString(method) + " return ((" + getWrapperTypeName(returnType) +
123                                 ")scriptWrapper.invoke(\"" + method.getName() + "\", arguments))." +
124                                 returnType.getName() + "Value();}";
125                         }
126                         else
127                         {
128                             methodBody =
129                                     '{' + constructArgumentString(method) + " return (" + method.getReturnType().getName() +
130                                 ")scriptWrapper.invoke(\"" + method.getName() + "\", arguments);}";
131                         }
132                     }
133                     method.setBody(methodBody);
134                 }
135             }
136 
137             final File directory = getClassOutputDirectory(existingClass);
138             ctClass.writeFile(directory.getAbsolutePath());
139         }
140         catch (final Throwable throwable)
141         {
142             throwable.printStackTrace();
143             throw new ScriptClassGeneratorException(throwable);
144         }
145     }
146 
147     /**
148      * Retrieves the output directory which the adapted class will be written to.
149      *
150      * @return the output directory
151      */
152     private File getClassOutputDirectory(final Class existingClass)
153     {
154         final String className = existingClass.getName();
155         final String classResourcePath = '/' + className.replace(
156             '.',
157             '/') + ".class";
158         final URL classResource = existingClass.getResource(classResourcePath);
159         if (classResource == null)
160         {
161             throw new ScriptClassGeneratorException("Could not find the class resource '" + classResourcePath + '\'');
162         }
163         final String file = classResource.getFile().replaceAll(".*(\\\\|//)", "/");
164         return new File(StringUtils.replace(file, classResourcePath, ""));
165     }
166 
167     private String constructArgumentString(final CtMethod method)
168         throws NotFoundException
169     {
170         CtClass[] argumentTypes = method.getParameterTypes();
171         final int argumentNumber = argumentTypes.length;
172         final StringBuilder arguments =
173             new StringBuilder("final Object[] arguments = new Object[" + argumentNumber + "];");
174         for (int ctr = 1; ctr <= argumentNumber; ctr++)
175         {
176             final CtClass argumentType = argumentTypes[ctr - 1];
177             arguments.append("arguments[").append(ctr - 1).append("] = ");
178             if (argumentType.isPrimitive())
179             {
180                 arguments.append("new java.lang.").append(getWrapperTypeName(argumentType)).append("($").append(ctr).append(");");
181             }
182             else
183             {
184                 arguments.append('$').append(ctr).append(';');
185             }
186         }
187         return arguments.toString();
188     }
189 
190     private String getWrapperTypeName(CtClass ctClass)
191     {
192         final String typeName = ctClass.getName();
193         StringBuilder name = new StringBuilder(typeName);
194         if ("int".equalsIgnoreCase(typeName))
195         {
196             name.append("eger");
197         }
198         return StringUtils.capitalize(name.toString());
199     }
200 
201     private String getScriptWrapperInitialization(
202         final String directory,
203         final String className)
204     {
205         return "new " + this.scriptWrapperName + "(this, \"" +
206         new File(
207             directory,
208             className.replace(
209                 '.',
210                 '/')).getAbsolutePath().replace(
211             '\\',
212             '/') + ".java" + "\");";
213     }
214 
215     /**
216      * Converts the given <code>clazz</code> to a CtClass instances.
217      *
218      * @param pool the pool from which to retrieve the CtClass instance.
219      * @param clazz the class to convert.
220      * @return the CtClass instances.
221      * @throws NotFoundException
222      */
223     private CtClass convert(
224         final ClassPool pool,
225         final String className)
226         throws NotFoundException
227     {
228         CtClass ctClass = null;
229         if (className != null)
230         {
231             ctClass = pool.get(className);
232         }
233         return ctClass;
234     }
235 }