View Javadoc
1   package org.andromda.translation.ocl.testsuite;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.net.URL;
6   import java.util.HashMap;
7   import java.util.Map;
8   import javassist.CannotCompileException;
9   import javassist.ClassPool;
10  import javassist.CtClass;
11  import javassist.CtField;
12  import javassist.CtMethod;
13  import javassist.LoaderClassPath;
14  import javassist.NotFoundException;
15  import org.andromda.core.common.AndroMDALogger;
16  import org.andromda.core.common.ExceptionUtils;
17  import org.andromda.core.common.ResourceUtils;
18  import org.andromda.core.translation.Expression;
19  import org.andromda.core.translation.TranslationUtils;
20  import org.andromda.core.translation.Translator;
21  import org.andromda.core.translation.TranslatorException;
22  import org.andromda.translation.ocl.BaseTranslator;
23  import org.apache.log4j.Logger;
24  
25  /**
26   * This class allows us to trace the parsing of the expression. It is reflectively extended by Javassist to allow the
27   * inclusion of all "inA" and "outA" methods produced by the SableCC parser. This allows us to dynamically include all
28   * handling code in each method without having to manual code each one. It used used during development of Translators
29   * since it allows you to see the execution of each node.
30   *
31   * @author Chad Brandon
32   */
33  public class TraceTranslator
34          extends BaseTranslator
35  {
36      private static final Logger logger = Logger.getLogger(TraceTranslator.class);
37  
38      private static final String INA_PREFIX = "inA";
39      private static final String OUTA_PREFIX = "outA";
40      private static final String CASE_PREFIX = "case";
41  
42      private Map<CtMethod, String> methods = new HashMap<CtMethod, String>();
43  
44      /**
45       * This is added to the adapted class and then checked to see if it exists to determine if we need to adapt the
46       * class.
47       */
48      private static final String FIELD_ADAPTED = "adapted";
49  
50      private ClassPool pool;
51  
52      /**
53       * Constructs an instance of TraceTranslator.
54       */
55      public TraceTranslator()
56      {
57      }
58  
59      /**
60       * Creates and returns an new Instance of this ExpressionTranslator as a Translator object. The first time this
61       * method is called this class will dynamically be adapted to handle all parser calls.
62       *
63       * @return Translator
64       */
65      public static Translator getInstance()
66      {
67          final String debugMethodName = "TraceTranslator.getInstance";
68          if (logger.isDebugEnabled())
69          {
70              logger.debug("performing " + debugMethodName);
71          }
72          try
73          {
74              TraceTranslator oclTranslator = new TraceTranslator();
75              Translator translator = oclTranslator;
76              if (oclTranslator.needsAdaption())
77              {
78  
79                  if (logger.isInfoEnabled())
80                  {
81                      logger.info(" OCL Translator has not been adapted --> adapting");
82                  }
83                  translator = (Translator) oclTranslator.getAdaptedTranslationClass().newInstance();
84              }
85              return translator;
86          }
87          catch (Exception ex)
88          {
89              String errMsg = "Error performing " + debugMethodName;
90              logger.error(errMsg, ex);
91              throw new TranslatorException(errMsg, ex);
92          }
93      }
94  
95      /**
96       * @see org.andromda.core.translation.Translator#translate(String, String, Object)
97       */
98      public Expression translate(String translationName, String expression, Object contextElement)
99      {
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 }