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}