1 package org.andromda.core.anttasks;
2
3 import java.io.BufferedWriter;
4 import java.io.ByteArrayOutputStream;
5 import java.io.File;
6 import java.io.FileInputStream;
7 import java.io.FileNotFoundException;
8 import java.io.FileOutputStream;
9 import java.io.IOException;
10 import java.io.OutputStream;
11 import java.io.OutputStreamWriter;
12 import java.io.Writer;
13 import java.net.MalformedURLException;
14 import java.net.URL;
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.Iterator;
18 import java.util.List;
19 import java.util.Properties;
20
21 import org.andromda.cartridges.interfaces.IAndroMDACartridge;
22 import org.andromda.cartridges.interfaces.OutletDictionary;
23 import org.andromda.cartridges.interfaces.TemplateConfiguration;
24 import org.andromda.cartridges.mgmt.CartridgeDictionary;
25 import org.andromda.cartridges.mgmt.CartridgeFinder;
26
27 import org.andromda.core.common.DbMappingTable;
28 import org.andromda.core.common.RepositoryFacade;
29 import org.andromda.core.common.RepositoryReadException;
30 import org.andromda.core.common.ScriptHelper;
31 import org.andromda.core.common.StringUtilsHelper;
32
33 import org.apache.commons.collections.ExtendedProperties;
34
35 import org.apache.tools.ant.BuildException;
36 import org.apache.tools.ant.DirectoryScanner;
37 import org.apache.tools.ant.Project;
38 import org.apache.tools.ant.taskdefs.MatchingTask;
39
40 import org.apache.velocity.Template;
41 import org.apache.velocity.VelocityContext;
42 import org.apache.velocity.app.VelocityEngine;
43 import org.apache.velocity.runtime.RuntimeConstants;
44 import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
45
46 /***
47 * This class represents the <code><andromda></code> custom task which can
48 * be called from an ant script.
49 *
50 * The <andromda> task facilitates Model Driven Architecture by enabling
51 * the generation of source code, configuration files, and other such artifacts
52 * from a UML model.
53 *
54 * @author <a href="http://www.mbohlen.de">Matthias Bohlen</a>
55 * @author <A HREF="http://www.amowers.com">Anthony Mowers</A>
56 */
57 public class AndroMDAGenTask extends MatchingTask
58 {
59 private static final String DEFAULT_DBMAPPING_TABLE_CLASSNAME =
60 "org.andromda.core.dbmapping.DigesterDbMappingTable";
61
62 /***
63 * the base directory
64 */
65 private File baseDir = null;
66
67 /***
68 * check the last modified date on files. defaults to true
69 */
70 private boolean lastModifiedCheck = true;
71
72 /***
73 * the mappings from java data types to JDBC and SQL datatypes.
74 */
75 private DbMappingTable typeMappings = null;
76
77 /***
78 * the file to get the velocity properties file
79 */
80 private File velocityPropertiesFile = null;
81
82 /***
83 * the VelocityEngine instance to use
84 */
85 private VelocityEngine ve;
86
87 /***
88 * User properties that were specified by nested tags in the ant script.
89 */
90 private ArrayList userProperties = new ArrayList();
91
92 private RepositoryConfiguration repositoryConfiguration = null;
93
94 /***
95 * An optional URL to a model
96 */
97 private URL modelURL = null;
98
99 /***
100 * Dictionary of defined outlets. An outlet is a symbolic alias name
101 * for a physical directory.
102 */
103 private OutletDictionary outletDictionary = new OutletDictionary();
104
105 /***
106 * Temporary list of mappings from the <outlet> subtask.
107 * Will be transferred to the outletDictionary before execution starts.
108 */
109 private ArrayList outletMappingList = new ArrayList();
110
111 /***
112 * Default properties for the Velocity scripting engine.
113 */
114 private Properties velocityProperties;
115
116 /***
117 * Dictionary of installed cartridges, searchable by stereotype.
118 */
119 private CartridgeDictionary cartridgeDictionary;
120
121 /***
122 * <p>
123 * Creates a new <code>AndroMDAGenTask</code> instance.
124 * </p>
125 */
126 public AndroMDAGenTask()
127 {
128 }
129
130 public void setModelURL(URL modelURL)
131 {
132 this.modelURL = modelURL;
133 }
134
135 /***
136 * Adds a mapping for a cartridge outlet to a physical directory.
137 * Example from a build.xml file:
138 * <outlet cartridge="ejb" outlet="beans" dir="${my.beans.dir}" />
139 *
140 * @param om the outlet mapping javabean supplied by Ant
141 */
142 public void addOutlet(OutletMapping om)
143 {
144 outletMappingList.add(om);
145 }
146
147 /***
148 * <p>
149 *
150 * Sets the base directory from which the object model files are read. This
151 * defaults to the base directory of the ant project if not provided.</p>
152 *
153 *@param dir a <code>File</code> with the path to the base directory
154 */
155 public void setBasedir(File dir)
156 {
157 baseDir = dir;
158 }
159
160 /***
161 * <p>
162 *
163 * Reads the configuration file for mappings of Java types to JDBC and SQL
164 * types.</p>
165 *
166 *@param dbMappingConfig XML file with type to database mappings
167 *@throws BuildException if the file is not accessible
168 */
169 public void setTypeMappings(File dbMappingConfig)
170 {
171 try
172 {
173 Class mappingClass =
174 Class.forName(DEFAULT_DBMAPPING_TABLE_CLASSNAME);
175 typeMappings = (DbMappingTable) mappingClass.newInstance();
176
177 typeMappings.read(dbMappingConfig);
178 }
179 catch (IllegalAccessException iae)
180 {
181 throw new BuildException(iae);
182 }
183 catch (ClassNotFoundException cnfe)
184 {
185 throw new BuildException(cnfe);
186 }
187 catch (RepositoryReadException rre)
188 {
189 throw new BuildException(rre);
190 }
191 catch (IOException ioe)
192 {
193 throw new BuildException(ioe);
194 }
195 catch (InstantiationException ie)
196 {
197 throw new BuildException(ie);
198 }
199 }
200
201 /***
202 * <p>
203 *
204 * Allows people to set the path to the <code>velocity.properties</code> file.
205 * </p> <p>
206 *
207 * This file is found relative to the path where the JVM was run. For example,
208 * if <code>build.sh</code> was executed in the <code>./build</code>
209 * directory, then the path would be relative to this directory.</p> <p>
210 *
211 *
212 *@param velocityPropertiesFile a <code>File</code> with the path to the
213 * velocity properties file
214 */
215 public void setVelocityPropertiesFile(File velocityPropertiesFile)
216 {
217 this.velocityPropertiesFile = velocityPropertiesFile;
218 }
219
220 /***
221 * <p>
222 *
223 * Turns on/off last modified checking for generated files. If checking is
224 * turned on, overwritable files are regenerated only when the model is newer
225 * than the file to be generated. By default, it is on.</p>
226 *
227 *@param lastmod set the modified check, yes or no?
228 */
229 public void setLastModifiedCheck(boolean lastmod)
230 {
231 this.lastModifiedCheck = lastmod;
232 }
233
234 /***
235 * <p>
236 *
237 * Add a user property specified as a nested tag in the ant build script.</p>
238 *
239 *@param up the UserProperty that ant already constructed for us
240 */
241 public void addUserProperty(UserProperty up)
242 {
243 userProperties.add(up);
244 }
245
246 /***
247 * <p>
248 *
249 * Starts the generation of source code from an object model.
250 *
251 * This is the main entry point of the application. It is called by ant whenever
252 * the surrounding task is executed (which could be multiple times).</p>
253 *
254 *@throws BuildException if something goes wrong
255 */
256 public void execute() throws BuildException
257 {
258 DirectoryScanner scanner;
259 String[] list;
260 String[] dirs;
261
262 if (baseDir == null)
263 {
264
265
266 baseDir = project.resolveFile(".");
267 }
268
269 if (typeMappings == null)
270 {
271
272
273 }
274
275 initOutletDictionary();
276 initCartridges();
277 initVelocityPropertiesAndEngine();
278
279
280
281 createRepository().createRepository().open();
282
283 if (modelURL == null)
284 {
285
286 scanner = getDirectoryScanner(baseDir);
287
288
289 list = scanner.getIncludedFiles();
290
291 if (list.length > 0)
292 {
293 for (int i = 0; i < list.length; ++i)
294 {
295 URL modelURL = null;
296 File inFile = new File(baseDir, list[i]);
297
298 try
299 {
300 modelURL = inFile.toURL();
301 process(modelURL);
302 }
303 catch (MalformedURLException mfe)
304 {
305 throw new BuildException(
306 "Malformed model file URL: " + modelURL);
307 }
308 }
309 }
310 else
311 {
312 throw new BuildException("Couldn't find any input xmi.");
313 }
314 }
315 else
316 {
317
318 process(modelURL);
319 }
320
321 createRepository().createRepository().close();
322 }
323
324 /***
325 * Initializes the Velocity properties and the Velocity engine itself. Tells
326 * Velocity that the AndroMDA templates can be found using the classpath.
327 *
328 * @see org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
329 * @throws BuildException
330 */
331 private void initVelocityPropertiesAndEngine() throws BuildException
332 {
333 ve = new VelocityEngine();
334
335 boolean hasProperties = false;
336 velocityProperties = new Properties();
337
338 if (velocityPropertiesFile == null)
339 {
340
341
342 velocityPropertiesFile = new File("velocity.properties");
343 }
344
345 FileInputStream fis = null;
346 try
347 {
348
349
350 fis = new FileInputStream(velocityPropertiesFile);
351 velocityProperties.load(fis);
352 hasProperties = true;
353 }
354 catch (FileNotFoundException fnfex)
355 {
356
357
358 }
359 catch (IOException ioex)
360 {
361
362
363 }
364 finally
365 {
366 if (null != fis)
367 {
368 try
369 {
370 fis.close();
371 }
372 catch (IOException ioex)
373 {
374
375 }
376 }
377 }
378
379 try
380 {
381
382 ExtendedProperties ep =
383 ExtendedProperties.convertProperties(velocityProperties);
384
385 ep.addProperty(
386 RuntimeConstants.RESOURCE_LOADER,
387 "andromda.cartridges,file");
388
389 ep.setProperty(
390 "andromda.cartridges."
391 + RuntimeConstants.RESOURCE_LOADER
392 + ".class",
393 ClasspathResourceLoader.class.getName());
394
395
396
397
398
399
400 ep.setProperty(RuntimeConstants.VM_PERM_INLINE_LOCAL, "true");
401
402 ve.setExtendedProperties(ep);
403 ve.init();
404 }
405 catch (Exception e)
406 {
407 log("Error: " + e.toString(), Project.MSG_INFO);
408 throw new BuildException(e);
409 }
410 }
411
412 /***
413 * This method would normally be unnecessary. It is here because of a bug in
414 * ant. Ant calls addOutlet() before the OutletMapping javabean is fully
415 * initialized. So we kept the javabeans in an ArrayList that we have to
416 * copy into the dictionary now.
417 */
418 private void initOutletDictionary()
419 {
420 for (Iterator iter = outletMappingList.iterator(); iter.hasNext();)
421 {
422 OutletMapping om = (OutletMapping) iter.next();
423 outletDictionary.addOutletMapping(
424 om.getCartridge(),
425 om.getOutlet(),
426 om.getDir());
427 }
428 outletMappingList = null;
429 }
430
431 /***
432 * Initialize the cartridge system. Discover all installed cartridges and
433 * register them in the cartridge dictionary.
434 */
435 private void initCartridges() throws BuildException
436 {
437 CartridgeFinder.initClasspath(getClass());
438 try
439 {
440 List cartridges = CartridgeFinder.findCartridges();
441
442 if (cartridges.size() <= 0)
443 {
444 log("Warning: No cartridges found, check configuration!", Project.MSG_INFO);
445 }
446 else
447 {
448 cartridgeDictionary = new CartridgeDictionary();
449 for (Iterator cartridgeIterator = cartridges.iterator();
450 cartridgeIterator.hasNext();
451 )
452 {
453 IAndroMDACartridge cartridge =
454 (IAndroMDACartridge) cartridgeIterator.next();
455 List stereotypes =
456 cartridge.getDescriptor().getSupportedStereotypes();
457 for (Iterator stereotypeIterator = stereotypes.iterator();
458 stereotypeIterator.hasNext();
459 )
460 {
461 String stType = (String) stereotypeIterator.next();
462 cartridgeDictionary.addCartridge(stType, cartridge);
463 }
464 }
465 }
466 }
467 catch (IOException e)
468 {
469 throw new BuildException(e);
470 }
471
472 }
473
474 private void process(URL url) throws BuildException
475 {
476 Context context = new Context();
477
478 try
479 {
480
481 log("Input: " + url, Project.MSG_INFO);
482
483
484 context.repository = createRepository().createRepository();
485 context.repository.open();
486 context.repository.readModel(url);
487
488
489 context.scriptHelper = createRepository().createTransform();
490 context.scriptHelper.setModel(context.repository.getModel());
491 context.scriptHelper.setTypeMappings(typeMappings);
492
493 }
494 catch (FileNotFoundException fnfe)
495 {
496 throw new BuildException("Model file not found: " + modelURL);
497 }
498 catch (IOException ioe)
499 {
500 throw new BuildException(
501 "Exception encountered while processing: " + modelURL);
502 }
503 catch (RepositoryReadException mdre)
504 {
505 throw new BuildException(mdre);
506 }
507
508
509 Collection elements = context.scriptHelper.getModelElements();
510 for (Iterator it = elements.iterator(); it.hasNext();)
511 {
512 processModelElement(context, it.next());
513 }
514 context.repository.close();
515
516 }
517
518 /***
519 * <p>Processes one type (e.g. class, interface or datatype) but possibly
520 * with several templates.</p>
521 *
522 *@param mdr Description of the Parameter
523 *@param modelElement Description of the Parameter
524 *@throws BuildException if something goes wrong
525 */
526 private void processModelElement(Context context, Object modelElement)
527 throws BuildException
528 {
529 String name = context.scriptHelper.getName(modelElement);
530 Collection stereotypeNames =
531 context.scriptHelper.getStereotypeNames(modelElement);
532
533 for (Iterator i = stereotypeNames.iterator(); i.hasNext();)
534 {
535 String stereotypeName = (String) i.next();
536
537 processModelElementStereotype(
538 context,
539 modelElement,
540 stereotypeName);
541 }
542
543 }
544
545 /***
546 * Generate code from a model element, using exactly one of its stereotypes.
547 *
548 * @param context the context for the code generation
549 * @param modelElement the model element
550 * @param stereotypeName the name of the stereotype
551 * @throws BuildException if something goes wrong
552 */
553 private void processModelElementStereotype(
554 Context context,
555 Object modelElement,
556 String stereotypeName)
557 throws BuildException
558 {
559 Collection suitableCartridges =
560 cartridgeDictionary.lookupCartridges(stereotypeName);
561
562
563
564
565
566 if (suitableCartridges == null)
567 {
568 return;
569 }
570
571 for (Iterator iter = suitableCartridges.iterator();
572 iter.hasNext();
573 )
574 {
575 IAndroMDACartridge c = (IAndroMDACartridge) iter.next();
576
577 processModelElementWithCartridge(
578 context,
579 modelElement,
580 c,
581 stereotypeName);
582 }
583 }
584
585 private void processModelElementWithCartridge(
586 Context context,
587 Object modelElement,
588 IAndroMDACartridge cartridge,
589 String stereotypeName)
590 throws BuildException
591 {
592 String name = context.scriptHelper.getName(modelElement);
593 String packageName =
594 context.scriptHelper.getPackageName(modelElement);
595 long modelLastModified = context.repository.getLastModified();
596
597 List templates =
598 cartridge.getDescriptor().getTemplateConfigurations();
599 for (Iterator it = templates.iterator(); it.hasNext();)
600 {
601 TemplateConfiguration tc = (TemplateConfiguration) it.next();
602 if (tc.getStereotype().equals(stereotypeName))
603 {
604 ScriptHelper scriptHelper = context.scriptHelper;
605
606 if (tc.getTransformClass() != null)
607 {
608
609 try
610 {
611 context.scriptHelper =
612 (ScriptHelper) tc
613 .getTransformClass()
614 .newInstance();
615 context.scriptHelper.setModel(
616 context.repository.getModel());
617 context.scriptHelper.setTypeMappings(typeMappings);
618 }
619 catch (IllegalAccessException iae)
620 {
621 throw new BuildException(iae);
622 }
623 catch (InstantiationException ie)
624 {
625 throw new BuildException(ie);
626 }
627 }
628
629 File outFile =
630 tc.getFullyQualifiedOutputFile(
631 name,
632 packageName,
633 outletDictionary);
634
635 if (outFile != null)
636 {
637 try
638 {
639
640
641 boolean writeOutputFile =
642 !outFile.exists() || tc.isOverwrite();
643
644 if (writeOutputFile
645 && (lastModifiedCheck == false
646 || modelLastModified > outFile.lastModified()
647
648
649
650 ))
651 {
652 processModelElementWithOneTemplate(
653 context,
654 modelElement,
655 tc.getSheet(),
656 outFile,
657 tc.isGenerateEmptyFiles());
658 }
659 }
660 catch (ClassTemplateProcessingException e)
661 {
662 outFile.delete();
663 throw new BuildException(e);
664 }
665 }
666
667
668
669 context.scriptHelper = scriptHelper;
670 }
671 }
672 }
673
674 /***
675 * <p>
676 * Processes one type (that is class, interface or datatype) with exactly
677 * one template script.
678 * </p>
679 *
680 * @param context context for code generation
681 * @param modelElement the model element for which code should be
682 * generated
683 * @param styleSheetName name of the Velocity style sheet
684 * @param outFile file to which to write the output
685 * @param generateEmptyFile flag, tells whether to generate empty
686 * files or not.
687 * @throws ClassTemplateProcessingException if something goes wrong
688 */
689 private void processModelElementWithOneTemplate(
690 Context context,
691 Object modelElement,
692 String styleSheetName,
693 File outFile,
694 boolean generateEmptyFile)
695 throws ClassTemplateProcessingException
696 {
697 Writer writer = null;
698 ByteArrayOutputStream content = null;
699
700 ensureDirectoryFor(outFile);
701 String encoding = getTemplateEncoding();
702 try
703 {
704 if (generateEmptyFile)
705 {
706 writer =
707 new BufferedWriter(
708 new OutputStreamWriter(
709 new FileOutputStream(outFile),
710 encoding));
711 } else {
712 content = new ByteArrayOutputStream();
713 writer = new OutputStreamWriter(content, encoding);
714 }
715 }
716 catch (Exception e)
717 {
718 throw new ClassTemplateProcessingException(
719 "Error opening output file " + outFile.getName(),
720 e);
721 }
722
723 try
724 {
725 VelocityContext velocityContext = new VelocityContext();
726
727
728 velocityContext.put("model", context.scriptHelper.getModel());
729 velocityContext.put("transform", context.scriptHelper);
730 velocityContext.put("str", new StringUtilsHelper());
731 velocityContext.put("class", modelElement);
732 velocityContext.put("date", new java.util.Date());
733
734 addUserPropertiesToContext(velocityContext);
735
736
737
738
739
740
741
742
743 Template template = ve.getTemplate(styleSheetName);
744 template.merge(velocityContext, writer);
745
746 writer.flush();
747 writer.close();
748 }
749 catch (Exception e)
750 {
751 try
752 {
753 writer.flush();
754 writer.close();
755 }
756 catch (Exception e2)
757 {
758 }
759
760 throw new ClassTemplateProcessingException(
761 "Error processing velocity script on " + outFile.getName(),
762 e);
763 }
764
765
766
767 if (!generateEmptyFile)
768 {
769 byte[] result = content.toByteArray();
770 if (result.length > 0)
771 {
772 try
773 {
774 OutputStream out = new FileOutputStream(outFile);
775 out.write(result);
776 log("Output: " + outFile, Project.MSG_INFO);
777 }
778 catch (Exception e)
779 {
780 throw new ClassTemplateProcessingException(
781 "Error writing output file " + outFile.getName(),
782 e);
783 }
784 }
785 else
786 {
787 if (outFile.exists())
788 {
789 if (!outFile.delete())
790 {
791 throw new ClassTemplateProcessingException(
792 "Error removing output file " + outFile.getName());
793 }
794 log("Remove: " + outFile, Project.MSG_INFO);
795 }
796 }
797 }
798 else
799 {
800 log("Output: " + outFile, Project.MSG_INFO);
801 }
802 }
803
804 /***
805 * Takes all the UserProperty values that were defined in the ant build.xml
806 * file and adds them to the Velocity context.
807 *
808 *@param context the Velocity context
809 */
810 private void addUserPropertiesToContext(VelocityContext context)
811 {
812 for (Iterator it = userProperties.iterator(); it.hasNext();)
813 {
814 UserProperty up = (UserProperty) it.next();
815 context.put(up.getName(), up.getValue());
816 }
817 }
818
819 /***
820 * Gets the templateEncoding attribute of the AndroMDAGenTask object
821 *
822 *@return The templateEncoding value
823 */
824 private String getTemplateEncoding()
825 {
826
827
828
829
830 String encoding =
831 (String) ve.getProperty(RuntimeConstants.OUTPUT_ENCODING);
832 if (encoding == null
833 || encoding.length() == 0
834 || encoding.equals("8859-1")
835 || encoding.equals("8859_1"))
836 {
837 encoding = "ISO-8859-1";
838 }
839 return encoding;
840 }
841
842 /***
843 * Creates and returns a repsository configuration object.
844 *
845 * This enables an ANT build script to use the <repository> ant subtask
846 * to configure the model repository used by ANDROMDA during code
847 * generation.
848 *
849 * @return RepositoryConfiguration
850 * @throws BuildException
851 */
852 public RepositoryConfiguration createRepository() throws BuildException
853 {
854 if (repositoryConfiguration == null)
855 {
856 repositoryConfiguration = new RepositoryConfiguration();
857 }
858
859 return repositoryConfiguration;
860 }
861
862 /***
863 * <p>
864 *
865 * Creates directories as needed.</p>
866 *
867 *@param targetFile a <code>File</code> whose parent directories need
868 * to exist
869 *@exception BuildException if the parent directories couldn't be created
870 */
871 private void ensureDirectoryFor(File targetFile) throws BuildException
872 {
873 File directory = new File(targetFile.getParent());
874 if (!directory.exists())
875 {
876 if (!directory.mkdirs())
877 {
878 throw new BuildException(
879 "Unable to create directory: "
880 + directory.getAbsolutePath());
881 }
882 }
883 }
884
885 /***
886 * Context used for doing code generation
887 */
888 private static class Context
889 {
890 RepositoryFacade repository = null;
891 ScriptHelper scriptHelper = null;
892 }
893
894 }