001    package org.andromda.translation.ocl.testsuite;
002    
003    import java.io.File;
004    import java.io.IOException;
005    import java.net.URL;
006    import java.util.HashMap;
007    import java.util.Map;
008    import javassist.CannotCompileException;
009    import javassist.ClassPool;
010    import javassist.CtClass;
011    import javassist.CtField;
012    import javassist.CtMethod;
013    import javassist.LoaderClassPath;
014    import javassist.NotFoundException;
015    import org.andromda.core.common.AndroMDALogger;
016    import org.andromda.core.common.ExceptionUtils;
017    import org.andromda.core.common.ResourceUtils;
018    import org.andromda.core.translation.Expression;
019    import org.andromda.core.translation.TranslationUtils;
020    import org.andromda.core.translation.Translator;
021    import org.andromda.core.translation.TranslatorException;
022    import org.andromda.translation.ocl.BaseTranslator;
023    import org.apache.log4j.Logger;
024    
025    /**
026     * This class allows us to trace the parsing of the expression. It is reflectively extended by Javassist to allow the
027     * inclusion of all "inA" and "outA" methods produced by the SableCC parser. This allows us to dynamically include all
028     * handling code in each method without having to manual code each one. It used used during development of Translators
029     * since it allows you to see the execution of each node.
030     *
031     * @author Chad Brandon
032     */
033    public class TraceTranslator
034            extends BaseTranslator
035    {
036        private static final Logger logger = Logger.getLogger(TraceTranslator.class);
037    
038        private static final String INA_PREFIX = "inA";
039        private static final String OUTA_PREFIX = "outA";
040        private static final String CASE_PREFIX = "case";
041    
042        private Map<CtMethod, String> methods = new HashMap<CtMethod, String>();
043    
044        /**
045         * This is added to the adapted class and then checked to see if it exists to determine if we need to adapt the
046         * class.
047         */
048        private static final String FIELD_ADAPTED = "adapted";
049    
050        private ClassPool pool;
051    
052        /**
053         * Constructs an instance of TraceTranslator.
054         */
055        public TraceTranslator()
056        {
057        }
058    
059        /**
060         * Creates and returns an new Instance of this ExpressionTranslator as a Translator object. The first time this
061         * method is called this class will dynamically be adapted to handle all parser calls.
062         *
063         * @return Translator
064         */
065        public static Translator getInstance()
066        {
067            final String debugMethodName = "TraceTranslator.getInstance";
068            if (logger.isDebugEnabled())
069            {
070                logger.debug("performing " + debugMethodName);
071            }
072            try
073            {
074                TraceTranslator oclTranslator = new TraceTranslator();
075                Translator translator = oclTranslator;
076                if (oclTranslator.needsAdaption())
077                {
078    
079                    if (logger.isInfoEnabled())
080                    {
081                        logger.info(" OCL Translator has not been adapted --> adapting");
082                    }
083                    translator = (Translator) oclTranslator.getAdaptedTranslationClass().newInstance();
084                }
085                return translator;
086            }
087            catch (Exception ex)
088            {
089                String errMsg = "Error performing " + debugMethodName;
090                logger.error(errMsg, ex);
091                throw new TranslatorException(errMsg, ex);
092            }
093        }
094    
095        /**
096         * @see org.andromda.core.translation.Translator#translate(String, String, Object)
097         */
098        public Expression translate(String translationName, String expression, Object contextElement)
099        {
100            if (logger.isInfoEnabled())
101            {
102                logger.info("======================== Tracing Expression ========================");
103                logger.info(TranslationUtils.removeExtraWhitespace(expression));
104                logger.info("======================== ================== ========================");
105            }
106            Expression expressionObj = super.translate(translationName, expression, contextElement);
107            if (logger.isInfoEnabled())
108            {
109                logger.info("========================  Tracing Complete  ========================");
110            }
111            return expressionObj;
112        }
113    
114        /**
115         * Checks to see if this class needs to be adapted If it has the "adapted" field then we know it already has been
116         * adapted.
117         *
118         * @return true/false, true if it needs be be adapted.
119         */
120        protected boolean needsAdaption()
121        {
122            boolean needsAdaption = false;
123            try
124            {
125                this.getClass().getDeclaredField(FIELD_ADAPTED);
126            }
127            catch (NoSuchFieldException ex)
128            {
129                needsAdaption = true;
130            }
131            return needsAdaption;
132        }
133    
134        /**
135         * Creates and returns the adapted translator class.
136         *
137         * @return Class the new Class instance.
138         * @throws NotFoundException
139         * @throws CannotCompileException
140         * @throws IOException
141         */
142        protected Class getAdaptedTranslationClass() throws NotFoundException, CannotCompileException, IOException
143        {
144    
145            Class thisClass = this.getClass();
146            this.pool = TranslatorClassPool.getPool(thisClass.getClassLoader());
147    
148            CtClass ctTranslatorClass = pool.get(thisClass.getName());
149    
150            CtField adaptedField = new CtField(CtClass.booleanType, FIELD_ADAPTED, ctTranslatorClass);
151    
152            ctTranslatorClass.addField(adaptedField);
153    
154            //get the "inA" methods from the analysisClass
155            CtMethod[] analysisMethods = ctTranslatorClass.getMethods();
156    
157            if (analysisMethods != null)
158            {
159    
160                int methodNum = analysisMethods.length;
161    
162                for (int ctr = 0; ctr < methodNum; ctr++)
163                {
164                    CtMethod method = analysisMethods[ctr];
165                    String methodName = method.getName();
166    
167                    if (methodName.startsWith(INA_PREFIX))
168                    {
169                        // add the new overridden "inA" methods
170                        this.methods.put(method, this.getInAMethodBody(method));
171                    } else if (methodName.startsWith(OUTA_PREFIX))
172                    {
173                        // add the new overridden "outA" methods
174                        this.methods.put(method, this.getOutAMethodBody(method));
175                    } else if (methodName.startsWith(CASE_PREFIX))
176                    {
177                        // add the new overridden "case" methods
178                        this.methods.put(method, this.getCaseMethodBody(method));
179                    }
180                }
181    
182                //now add all the methods to the class
183                for (CtMethod method : this.methods.keySet())
184                {
185                    CtMethod newMethod = new CtMethod(method, ctTranslatorClass, null);
186                    String methodBody = this.methods.get(method);
187                    newMethod.setBody(methodBody);
188                    ctTranslatorClass.addMethod(newMethod);
189                }
190    
191            }
192            this.writeAdaptedClass(ctTranslatorClass);
193            return ctTranslatorClass.toClass();
194        }
195    
196        /**
197         * Writes the class to the directory found by the class loader (since the class is a currently existing class)
198         * @param pTranslatorClass
199         */
200        protected void writeAdaptedClass(CtClass pTranslatorClass)
201        {
202            final String methodName = "TraceTranslator.writeAdaptedClass";
203            if (logger.isDebugEnabled())
204            {
205                logger.debug("performing " + methodName);
206            }
207            try
208            {
209                File dir = this.getAdaptedClassOutputDirectory();
210                if (logger.isDebugEnabled())
211                {
212                    final String className = this.getClass().getName();
213                    logger.debug("writing className '" + className + "' to directory --> " + '\'' + dir + '\'');
214                }
215                pTranslatorClass.writeFile(dir.getPath());
216            }
217            catch (Exception ex)
218            {
219                String errMsg = "Error performing " + methodName;
220                logger.error(errMsg, ex);
221                throw new TranslatorException(errMsg, ex);
222            }
223        }
224    
225        /**
226         * Retrieves the output directory which the adapted class will be written to.
227         *
228         * @return AdaptedClassOutputDirectory
229         */
230        protected File getAdaptedClassOutputDirectory()
231        {
232            final String methodName = "TraceTranslator.getAdaptedClassOutputDirectory";
233            Class thisClass = this.getClass();
234            URL classAsResource = ResourceUtils.getClassResource(thisClass.getName());
235            File file = new File(classAsResource.getFile());
236            File dir = file.getParentFile();
237            if (dir == null)
238            {
239                throw new TranslatorException(methodName + " - can not retrieve directory for file '" + file + '\'');
240            }
241            String className = thisClass.getName();
242            int index = className.indexOf('.');
243            String basePackage = null;
244            if (index != -1)
245            {
246                basePackage = className.substring(0, index);
247            }
248            if (basePackage != null)
249            {
250                while (!dir.toString().endsWith(basePackage))
251                {
252                    dir = dir.getParentFile();
253                }
254                dir = dir.getParentFile();
255            }
256            return dir;
257        }
258    
259        /**
260         * Creates and returns the method body for each "caseA" method
261         *
262         * @param method
263         * @return String the <code>case</code> method body
264         */
265        protected String getCaseMethodBody(CtMethod method)
266        {
267            ExceptionUtils.checkNull("method", method);
268            StringBuilder methodBody = new StringBuilder("{");
269            String methodName = method.getName();
270            methodBody.append("String methodName = \"").append(methodName).append("\";");
271            methodBody.append(this.getMethodTrace(method));
272            //add the call of the super class method, so that any methods in sub
273            // classes
274            //can provide functionality
275            methodBody.append("super.").append(methodName).append("($1);");
276            methodBody.append('}');
277            return methodBody.toString();
278        }
279    
280        /**
281         * Creates and returns the method body for each "inA" method
282         *
283         * @param method
284         * @return String the <code>inA</code> method body
285         */
286        protected String getInAMethodBody(CtMethod method)
287        {
288            ExceptionUtils.checkNull("method", method);
289            StringBuilder methodBody = new StringBuilder("{");
290            String methodName = method.getName();
291            methodBody.append("String methodName = \"").append(methodName).append("\";");
292            methodBody.append(this.getMethodTrace(method));
293            //add the call of the super class method, so that any methods in sub
294            // classes
295            //can provide functionality
296            methodBody.append("super.").append(methodName).append("($1);");
297            methodBody.append('}');
298            return methodBody.toString();
299        }
300    
301        /**
302         * Creates and returns the method body for each "inA" method
303         *
304         * @param method
305         * @return String the <code>outA</code> method body.
306         */
307        protected String getOutAMethodBody(CtMethod method)
308        {
309            ExceptionUtils.checkNull("method", method);
310            StringBuilder methodBody = new StringBuilder("{");
311            String methodName = method.getName();
312            methodBody.append("String methodName = \"").append(methodName).append("\";");
313            methodBody.append(this.getMethodTrace(method));
314            //add the call of the super class method, so that any methods in sub
315            // classes
316            //can provide functionality
317            methodBody.append("super.").append(methodName).append("($1);");
318            methodBody.append('}');
319            return methodBody.toString();
320        }
321    
322        /**
323         * Returns the OCL fragment name that must have a matching name in the library translation template in order to be
324         * placed into the Expression translated expression buffer.
325         *
326         * @param method
327         * @return String
328         */
329        protected String getOclFragmentName(CtMethod method)
330        {
331            ExceptionUtils.checkNull("method", method);
332            String fragment = method.getName();
333            String prefix = this.getMethodPrefix(method);
334            int index = fragment.indexOf(prefix);
335            if (index != -1)
336            {
337                fragment = fragment.substring(index + prefix.length(), fragment.length());
338            }
339            return fragment;
340        }
341    
342        /**
343         * Returns the prefix for the method (inA or outA)
344         *
345         * @param method
346         * @return MethodPrefix
347         */
348        protected String getMethodPrefix(CtMethod method)
349        {
350            ExceptionUtils.checkNull("method", method);
351            String mName = method.getName();
352            String prefix = INA_PREFIX;
353            if (mName.startsWith(OUTA_PREFIX))
354            {
355                prefix = OUTA_PREFIX;
356            }
357            return prefix;
358        }
359    
360        /**
361         * Creates the debug statement that will be output for each method
362         *
363         * @param method
364         * @return @throws NotFoundException
365         */
366        protected String getMethodTrace(CtMethod method)
367        {
368            ExceptionUtils.checkNull("method", method);
369            StringBuilder buf = new StringBuilder("if (logger.isInfoEnabled()) {logger.info(\"");
370            buf.append("\" + methodName + \" --> ");
371            //javassist names the arguments $1,$2,$3, etc.
372            buf.append("'\" + org.andromda.core.translation.TranslationUtils.trimToEmpty($1) + \"'\");}");
373            return buf.toString();
374        }
375    
376        /**
377         * Extends the Javassist class pool so that we can define our own ClassLoader to use from which to find, load and
378         * modify and existing class.
379         *
380         * @author Chad Brandon
381         */
382        private static class TranslatorClassPool
383                extends ClassPool
384        {
385    
386            private static final Logger logger = Logger.getLogger(TranslatorClassPool.class);
387    
388            protected TranslatorClassPool()
389            {
390                super(ClassPool.getDefault());
391                if (logger.isInfoEnabled())
392                {
393                    logger.debug("instantiating new TranslatorClassPool");
394                }
395            }
396    
397            /**
398             * Retrieves an instance of this TranslatorClassPool using the loader to find/load any classes.
399             *
400             * @param loader
401             * @return pool
402             */
403            protected static ClassPool getPool(ClassLoader loader)
404            {
405                if (loader == null)
406                {
407                    loader = Thread.currentThread().getContextClassLoader();
408                }
409                TranslatorClassPool pool = new TranslatorClassPool();
410                pool.insertClassPath(new LoaderClassPath(loader));
411                return pool;
412            }
413    
414            /**
415             * Returns a <code>Class</code> object. It calls <code>write()</code> to obtain a class file and then
416             * loads the obtained class file into the JVM. The returned <code>Class</code> object represents the loaded
417             * class.
418             * <p/>
419             * To load a class file, this method uses an internal class loader. Thus, that class file is not loaded by the
420             * system class loader, which should have loaded this <code>AspectClassPool</code> class. The internal class
421             * loader loads only the classes explicitly specified by this method <code>writeAsClass()</code>. The other
422             * classes are loaded by the parent class loader (the system class loader) by delegation. Thus, if a class
423             * <code>X</code> loaded by the internal class loader refers to a class <code>Y</code>, then the class
424             * <code>Y</code> is loaded by the parent class loader.
425             *
426             * @param classname a fully-qualified class name.
427             * @return Class the Class it writes.
428             * @throws NotFoundException
429             * @throws IOException
430             * @throws CannotCompileException
431             */
432            @SuppressWarnings("unused")
433            public Class writeAsClass(String classname) throws NotFoundException, IOException, CannotCompileException
434            {
435                try
436                {
437                    final CtClass ctTranslatorClass = get(classname);
438                    return classLoader.loadClass(classname, ctTranslatorClass.toBytecode());
439                }
440                catch (ClassFormatError e)
441                {
442                    throw new CannotCompileException(e, classname);
443                }
444            }
445    
446            /**
447             * LocalClassLoader which allows us to dynamically construct classes on the fly using Javassist.
448             */
449            static class LocalClassLoader
450                    extends ClassLoader
451            {
452                /**
453                 * Constructs an instance of LocalClassLoader.
454                 *
455                 * @param parent
456                 */
457                public LocalClassLoader(ClassLoader parent)
458                {
459                    super(parent);
460                }
461    
462                /**
463                 * Loads a class.
464                 *
465                 * @param name      the name
466                 * @param classfile the bytes of the class.
467                 * @return Class
468                 * @throws ClassFormatError
469                 */
470                public Class loadClass(String name, byte[] classfile) throws ClassFormatError
471                {
472                    Class c = defineClass(name, classfile, 0, classfile.length);
473                    resolveClass(c);
474                    return c;
475                }
476            }
477    
478            /**
479             * Create the LocalClassLoader and specify the ClassLoader for this class as the parent ClassLoader. This allows
480             * classes defined outside this LocalClassLoader to be loaded (i.e. classes that already exist, and aren't being
481             * dynamically created
482             */
483            private static LocalClassLoader classLoader = new LocalClassLoader(LocalClassLoader.class.getClassLoader());
484        }
485    
486        /**
487         * This method is called by the main method during the build process, to "adapt" the class to the OCL parser.
488         */
489        protected static void adaptClass()
490        {
491            if (logger.isInfoEnabled())
492            {
493                logger.info("adapting class for OCL parser");
494            }
495            TraceTranslator translator = new TraceTranslator();
496            if (translator.needsAdaption())
497            {
498                try
499                {
500                    translator.getAdaptedTranslationClass();
501                }
502                catch (Throwable th)
503                {
504                    logger.error(th);
505                }
506            }
507        }
508    
509        /**
510         * This main method is called during the build process, to "adapt" the class to the OCL parser.
511         *
512         * @param args
513         */
514        public static void main(String[] args)
515        {
516            try
517            {
518                AndroMDALogger.initialize();
519                TraceTranslator.adaptClass();
520            }
521            catch (Throwable th)
522            {
523                logger.error(th);
524            }
525        }
526    
527        /**
528         * Ancestor abstract method which must be implemented even when it does nothing.
529         *
530         * @see org.andromda.translation.ocl.BaseTranslator#postProcess()
531         */
532        @Override
533        public void postProcess()
534        {
535            // Do nothing
536        }
537    }