001package org.andromda.maven.plugin.andromdapp.script; 002 003import java.io.File; 004import java.net.URL; 005import javassist.ClassPool; 006import javassist.CtClass; 007import javassist.CtField; 008import javassist.CtMethod; 009import javassist.LoaderClassPath; 010import javassist.Modifier; 011import javassist.NotFoundException; 012import org.andromda.core.common.ExceptionUtils; 013import org.apache.commons.lang.StringUtils; 014 015/** 016 * This class instruments a given class file in order for it be scripted. A class modified 017 * by this script generator can have its methods edited and the logic available without having 018 * to redeploy or compile the class. 019 * 020 * @author Chad Brandon 021 */ 022public final class ScriptClassGenerator 023{ 024 /** 025 * The shared instance of this class. 026 */ 027 private static ScriptClassGenerator instance; 028 029 /** 030 * The name of the script wrapper to use. 031 */ 032 private String scriptWrapperName; 033 034 /** 035 * Retrieves an instance of this class and uses the given script wrapper with 036 * the given <code>scriptWrapperName</code>. 037 * 038 * @param scriptWrapperName the fully qualified name of the script wrapper class to use. 039 * @return the instance of this class. 040 */ 041 public static final ScriptClassGenerator getInstance(final String scriptWrapperName) 042 { 043 ExceptionUtils.checkEmpty( 044 "scriptWrapperName", 045 scriptWrapperName); 046 instance = new ScriptClassGenerator(); 047 instance.scriptWrapperName = scriptWrapperName; 048 return instance; 049 } 050 051 private ScriptClassGenerator() 052 { 053 // - do not allow instantiation 054 } 055 056 /** 057 * Modifies the <code>existingClass</code> (basically inserts the script wrapper class into 058 * the class). 059 * @param scriptDirectory the directory in which to find the script. 060 * @param existingClass the class to modify. 061 */ 062 public void modifyClass( 063 final String scriptDirectory, 064 final Class existingClass) 065 { 066 try 067 { 068 final String className = existingClass.getName(); 069 070 final ClassPool pool = ClassPool.getDefault(); 071 final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 072 if (contextClassLoader != null) 073 { 074 pool.insertClassPath(new LoaderClassPath(contextClassLoader)); 075 } 076 final CtClass ctClass = pool.get(className); 077 078 // - make sure the class isn't frozen 079 ctClass.defrost(); 080 081 final String scriptWrapperFieldName = "scriptWrapper"; 082 try 083 { 084 ctClass.getField(scriptWrapperFieldName); 085 } 086 catch (Exception exception) 087 { 088 final CtField scriptWrapper = 089 new CtField( 090 convert( 091 pool, 092 this.scriptWrapperName), 093 scriptWrapperFieldName, 094 ctClass); 095 scriptWrapper.setModifiers(Modifier.PRIVATE + Modifier.FINAL); 096 ctClass.addField( 097 scriptWrapper, 098 getScriptWrapperInitialization( 099 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}