View Javadoc

1   package org.apache.torque.task;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InputStreamReader;
28  import java.io.LineNumberReader;
29  import java.io.PrintStream;
30  import java.io.UnsupportedEncodingException;
31  import java.io.Writer;
32  import java.util.ArrayList;
33  import java.util.Date;
34  import java.util.Hashtable;
35  import java.util.Iterator;
36  import java.util.List;
37  import java.util.Map;
38  
39  import org.apache.commons.lang.StringUtils;
40  import org.apache.texen.Generator;
41  import org.apache.texen.ant.TexenTask;
42  import org.apache.tools.ant.BuildException;
43  import org.apache.tools.ant.DirectoryScanner;
44  import org.apache.tools.ant.types.FileSet;
45  import org.apache.torque.engine.EngineException;
46  import org.apache.torque.engine.database.model.Database;
47  import org.apache.torque.engine.database.transform.XmlToAppData;
48  import org.apache.velocity.VelocityContext;
49  import org.apache.velocity.app.VelocityEngine;
50  import org.apache.velocity.context.Context;
51  import org.apache.velocity.exception.MethodInvocationException;
52  import org.apache.velocity.exception.ParseErrorException;
53  import org.apache.velocity.exception.ResourceNotFoundException;
54  import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
55  import org.apache.velocity.runtime.resource.loader.FileResourceLoader;
56  
57  /***
58   * A base torque task that uses either a single XML schema
59   * representing a data model, or a <fileset> of XML schemas.
60   * We are making the assumption that an XML schema representing
61   * a data model contains tables for a <strong>single</strong>
62   * database.
63   *
64   * @author <a href="mailto:jvanzyl@zenplex.com">Jason van Zyl</a>
65   * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
66   */
67  public class TorqueDataModelTask extends TexenTask
68  {
69      /***
70       * XML that describes the database model, this is transformed
71       * into the application model object.
72       */
73      protected String xmlFile;
74  
75      /*** Fileset of XML schemas which represent our data models. */
76      protected List filesets = new ArrayList();
77  
78      /*** Data models that we collect. One from each XML schema file. */
79      protected List dataModels = new ArrayList();
80  
81      /*** Velocity context which exposes our objects in the templates. */
82      protected Context context;
83  
84      /***
85       * Map of data model name to database name.
86       * Should probably stick to the convention of them being the same but
87       * I know right now in a lot of cases they won't be.
88       */
89      protected Hashtable dataModelDbMap;
90  
91      /***
92       * Hashtable containing the names of all the databases
93       * in our collection of schemas.
94       */
95      protected Hashtable databaseNames;
96  
97      //!! This is probably a crappy idea having the sql file -> db map
98      // here. I can't remember why I put it here at the moment ...
99      // maybe I was going to map something else. It can probably
100     // move into the SQL task.
101 
102     /***
103      * Name of the properties file that maps an SQL file
104      * to a particular database.
105      */
106     protected String sqldbmap;
107 
108     /*** The target database(s) we are generating SQL for. */
109     private String targetDatabase;
110 
111     /**</package-summary/html">Target Java package to place the generated files in/ *//package-summary.html">em>* Target Java package to place the generated files in. */
112     private String targetPackage;
113 
114 
115     /***
116      * Set the sqldbmap.
117      *
118      * @param sqldbmap th db map
119      */
120     public void setSqlDbMap(String sqldbmap)
121     {
122         //!! Make all these references files not strings.
123         this.sqldbmap = getProject().resolveFile(sqldbmap).toString();
124     }
125 
126     /***
127      * Get the sqldbmap.
128      *
129      * @return String sqldbmap.
130      */
131     public String getSqlDbMap()
132     {
133         return sqldbmap;
134     }
135 
136     /***
137      * Return the data models that have been processed.
138      *
139      * @return List data models
140      */
141     public List getDataModels()
142     {
143         return dataModels;
144     }
145 
146     /***
147      * Return the data model to database name map.
148      *
149      * @return Hashtable data model name to database name map.
150      */
151     public Hashtable getDataModelDbMap()
152     {
153         return dataModelDbMap;
154     }
155 
156     /***
157      * Get the xml schema describing the application model.
158      *
159      * @return  String xml schema file.
160      */
161     public String getXmlFile()
162     {
163         return xmlFile;
164     }
165 
166     /***
167      * Set the xml schema describing the application model.
168      *
169      * @param xmlFile The new XmlFile value
170      */
171     public void setXmlFile(String xmlFile)
172     {
173         this.xmlFile = getProject().resolveFile(xmlFile).toString();
174     }
175 
176     /***
177      * Adds a set of xml schema files (nested fileset attribute).
178      *
179      * @param set a Set of xml schema files
180      */
181     public void addFileset(FileSet set)
182     {
183         filesets.add(set);
184     }
185 
186     /***
187      * Get the current target database.
188      *
189      * @return String target database(s)
190      */
191     public String getTargetDatabase()
192     {
193         return targetDatabase;
194     }
195 
196     /***
197      * Set the current target database. (e.g. mysql, oracle, ..)
198      *
199      * @param v target database(s)
200      */
201     public void setTargetDatabase(String v)
202     {
203         targetDatabase = v;
204     }
205 
206     /***
207      * Get the current target package.
208      *
209      * @return return target java package.
210      */
211     public String getTargetPackage()
212     {
213         return targetPackage;
214     }
215 
216     /***
217      * Set the current target package. This is where generated java classes will
218      * live.
219      *
220      * @param v target java package.
221      */
222     public void setTargetPackage(String v)
223     {
224         targetPackage = v;
225     }
226 
227     /***
228      * Set up the initial context for generating the SQL from the XML schema.
229      *
230      * @return the context
231      * @throws Exception
232      */
233     public Context initControlContext() throws Exception
234     {
235         XmlToAppData xmlParser;
236 
237         if (xmlFile == null && filesets.isEmpty())
238         {
239             throw new BuildException("You must specify an XML schema or "
240                     + "fileset of XML schemas!");
241         }
242 
243         try
244         {
245             if (xmlFile != null)
246             {
247                 // Transform the XML database schema into
248                 // data model object.
249                 xmlParser = new XmlToAppData(getTargetDatabase(),
250                         getTargetPackage());
251                 Database ad = xmlParser.parseFile(xmlFile);
252                 ad.setFileName(grokName(xmlFile));
253                 dataModels.add(ad);
254             }
255             else
256             {
257                 // Deal with the filesets.
258                 for (int i = 0; i < filesets.size(); i++)
259                 {
260                     FileSet fs = (FileSet) filesets.get(i);
261                     DirectoryScanner ds = fs.getDirectoryScanner(getProject());
262                     File srcDir = fs.getDir(getProject());
263 
264                     String[] dataModelFiles = ds.getIncludedFiles();
265 
266                     // Make a transaction for each file
267                     for (int j = 0; j < dataModelFiles.length; j++)
268                     {
269                         File f = new File(srcDir, dataModelFiles[j]);
270                         xmlParser = new XmlToAppData(getTargetDatabase(),
271                                 getTargetPackage());
272                         Database ad = xmlParser.parseFile(f.toString());
273                         ad.setFileName(grokName(f.toString()));
274                         dataModels.add(ad);
275                     }
276                 }
277             }
278 
279             Iterator i = dataModels.iterator();
280             databaseNames = new Hashtable();
281             dataModelDbMap = new Hashtable();
282 
283             // Different datamodels may state the same database
284             // names, we just want the unique names of databases.
285             while (i.hasNext())
286             {
287                 Database database = (Database) i.next();
288                 databaseNames.put(database.getName(), database.getName());
289                 dataModelDbMap.put(database.getFileName(), database.getName());
290             }
291         }
292         catch (EngineException ee)
293         {
294             throw new BuildException(ee);
295         }
296 
297         context = new VelocityContext();
298 
299         // Place our set of data models into the context along
300         // with the names of the databases as a convenience for now.
301         context.put("dataModels", dataModels);
302         context.put("databaseNames", databaseNames);
303         context.put("targetDatabase", targetDatabase);
304         context.put("targetPackage", targetPackage);
305 
306         return context;
307     }
308 
309     /***
310      * Change type of "now" to java.util.Date
311      *
312      * @see org.apache.texen.ant.TexenTask#populateInitialContext(org.apache.velocity.context.Context)
313      */
314     protected void populateInitialContext(Context context) throws Exception
315     {
316         super.populateInitialContext(context);
317         context.put("now", new Date());
318     }
319 
320     /***
321      * Gets a name to use for the application's data model.
322      *
323      * @param xmlFile The path to the XML file housing the data model.
324      * @return The name to use for the <code>AppData</code>.
325      */
326     private String grokName(String xmlFile)
327     {
328         // This can't be set from the file name as it is an unreliable
329         // method of naming the descriptor. Not everyone uses the same
330         // method as I do in the TDK. jvz.
331 
332         String name = "data-model";
333         int i = xmlFile.lastIndexOf(System.getProperty("file.separator"));
334         if (i != -1)
335         {
336             // Creep forward to the start of the file name.
337             i++;
338 
339             int j = xmlFile.lastIndexOf('.');
340             if (i < j)
341             {
342                 name = xmlFile.substring(i, j);
343             }
344             else
345             {
346                 // Weirdo
347                 name = xmlFile.substring(i);
348             }
349         }
350         return name;
351     }
352 
353     /***
354      * Override Texen's context properties to map the
355      * torque.xxx properties (including defaults set by the
356      * org/apache/torque/defaults.properties) to just xxx.
357      *
358      * <p>
359      * Also, move xxx.yyy properties to xxxYyy as Velocity
360      * doesn't like the xxx.yyy syntax.
361      * </p>
362      *
363      * @param file the file to read the properties from
364      */
365     public void setContextProperties(String file)
366     {
367         super.setContextProperties(file);
368 
369         // Map the torque.xxx elements from the env to the contextProperties
370         Hashtable env = super.getProject().getProperties();
371         for (Iterator i = env.entrySet().iterator(); i.hasNext();)
372         {
373             Map.Entry entry = (Map.Entry) i.next();
374             String key = (String) entry.getKey();
375             if (key.startsWith("torque."))
376             {
377                 String newKey = key.substring("torque.".length());
378                 int j = newKey.indexOf(".");
379                 while (j != -1)
380                 {
381                     newKey =
382                         newKey.substring(0, j)
383                         +  StringUtils.capitalize(newKey.substring(j + 1));
384                     j = newKey.indexOf(".");
385                 }
386 
387                 contextProperties.setProperty(newKey, entry.getValue());
388             }
389         }
390     }
391     /***
392      * This message fragment (telling users to consult the log or
393      * invoke ant with the -debug flag) is appended to rethrown
394      * exception messages.
395      */
396     private final static String ERR_MSG_FRAGMENT = 
397         ". For more information consult the velocity log, or invoke ant " +
398     "with the -debug flag.";
399 
400     /***
401      * This method creates an VelocityEngine instance, parses
402      * every template and creates the corresponding output.
403      * 
404      * Unfortunately the TextenTask.execute() method makes
405      * everything for us but we just want to set our own
406      * VelocityTemplateLoader. 
407      * TODO: change once TEXEN-14 is resolved and out.
408      *  
409      * @see org.apache.texen.ant.TexenTask#execute()
410      */
411     public void execute() throws BuildException 
412     {
413         // Make sure the template path is set.
414         if (templatePath == null && useClasspath == false)
415         {
416             throw new BuildException(
417                 "The template path needs to be defined if you are not using "
418                         + "the classpath for locating templates!");
419         }
420 
421         // Make sure the control template is set.
422         if (controlTemplate == null)
423         {
424             throw new BuildException(
425                     "The control template needs to be defined!");
426         }
427 
428         // Make sure the output directory is set.
429         if (outputDirectory == null)
430         {
431             throw new BuildException(
432                     "The output directory needs to be defined!");
433         }
434 
435         // Make sure there is an output file.
436         if (outputFile == null)
437         {
438             throw new BuildException("The output file needs to be defined!");
439         }
440 
441         VelocityEngine ve = new VelocityEngine();
442 
443         try
444         {
445             // Setup the Velocity Runtime.
446             if (templatePath != null)
447             {
448                 log("Using templatePath: " + templatePath, project.MSG_VERBOSE);
449                 ve.setProperty("torque" + VelocityEngine.FILE_RESOURCE_LOADER_PATH,
450                         templatePath);
451 
452                 // TR: We need our own FileResourceLoader
453                 ve.addProperty(VelocityEngine.RESOURCE_LOADER, "torquefile");
454                 ve.setProperty("torquefile." + VelocityEngine.RESOURCE_LOADER
455                             + ".instance",
456                         new TorqueFileResourceLoader(this));
457             }
458 
459             if (useClasspath)
460             {
461                 log("Using classpath");
462                 // TR: We need our own ClasspathResourceLoader
463                 ve.addProperty(VelocityEngine.RESOURCE_LOADER, "classpath");
464 
465                 ve.setProperty("classpath." + VelocityEngine.RESOURCE_LOADER
466                         + ".instance", new TorqueClasspathResourceLoader(this));
467 
468                 ve.setProperty("classpath." + VelocityEngine.RESOURCE_LOADER
469                         + ".cache", "false");
470 
471                 ve.setProperty("classpath." + VelocityEngine.RESOURCE_LOADER
472                         + ".modificationCheckInterval", "2");
473             }
474 
475             ve.init();
476 
477             // Create the text generator.
478             Generator generator = Generator.getInstance();
479             generator.setVelocityEngine(ve);
480             generator.setOutputPath(outputDirectory);
481             generator.setInputEncoding(inputEncoding);
482             generator.setOutputEncoding(outputEncoding);
483 
484             if (templatePath != null)
485             {
486                 generator.setTemplatePath(templatePath);
487             }
488 
489             // Make sure the output directory exists, if it doesn't
490             // then create it.
491             File file = new File(outputDirectory);
492             if (!file.exists())
493             {
494                 file.mkdirs();
495             }
496 
497             String path = outputDirectory + File.separator + outputFile;
498             log("Generating to file " + path, project.MSG_INFO);
499             Writer writer = generator.getWriter(path, outputEncoding);
500 
501             // The generator and the output path should
502             // be placed in the init context here and
503             // not in the generator class itself.
504             Context c = initControlContext();
505 
506             // Everything in the generator class should be
507             // pulled out and placed in here. What the generator
508             // class does can probably be added to the Velocity
509             // class and the generator class can probably
510             // be removed all together.
511             populateInitialContext(c);
512 
513             // Feed all the options into the initial
514             // control context so they are available
515             // in the control/worker templates.
516             if (contextProperties != null)
517             {
518                 Iterator i = contextProperties.getKeys();
519 
520                 while (i.hasNext())
521                 {
522                     String property = (String) i.next();
523                     String value = contextProperties.getString(property);
524 
525                     // Now lets quickly check to see if what
526                     // we have is numeric and try to put it
527                     // into the context as an Integer.
528                     try
529                     {
530                         c.put(property, new Integer(value));
531                     } catch (NumberFormatException nfe)
532                     {
533                         // Now we will try to place the value into
534                         // the context as a boolean value if it
535                         // maps to a valid boolean value.
536                         String booleanString = contextProperties
537                                 .testBoolean(value);
538 
539                         if (booleanString != null)
540                         {
541                             c.put(property, new Boolean(booleanString));
542                         } else
543                         {
544                             // We are going to do something special
545                             // for properties that have a "file.contents"
546                             // suffix: for these properties will pull
547                             // in the contents of the file and make
548                             // them available in the context. So for
549                             // a line like the following in a properties file:
550                             //
551                             // license.file.contents = license.txt
552                             //
553                             // We will pull in the contents of license.txt
554                             // and make it available in the context as
555                             // $license. This should make texen a little
556                             // more flexible.
557                             if (property.endsWith("file.contents"))
558                             {
559                                 // We need to turn the license file from
560                                 // relative to
561                                 // absolute, and let Ant help :)
562                                 value = org.apache.velocity.util.StringUtils
563                                         .fileContentsToString(project
564                                                 .resolveFile(value)
565                                                 .getCanonicalPath());
566 
567                                 property = property.substring(0, property
568                                         .indexOf("file.contents") - 1);
569                             }
570 
571                             c.put(property, value);
572                         }
573                     }
574                 }
575             }
576 
577             writer.write(generator.parse(controlTemplate, c));
578             writer.flush();
579             writer.close();
580             generator.shutdown();
581             cleanup();
582         } 
583         catch (BuildException e)
584         {
585             throw e;
586         } 
587         catch (MethodInvocationException e)
588         {
589             throw new BuildException("Exception thrown by '"
590                     + e.getReferenceName() + "." + e.getMethodName() + "'"
591                     + ERR_MSG_FRAGMENT, e.getWrappedThrowable());
592         } 
593         catch (ParseErrorException e)
594         {
595             throw new BuildException(
596                     "Velocity syntax error" + ERR_MSG_FRAGMENT, e);
597         } 
598         catch (ResourceNotFoundException e)
599         {
600             throw new BuildException(
601                     "Resource not found" + ERR_MSG_FRAGMENT, 
602                     e);
603         } 
604         catch (Exception e)
605         {
606             throw new BuildException(
607                     "Generation failed" + ERR_MSG_FRAGMENT, 
608                     e);
609         }
610     }
611 
612     /***
613      * This method filters the template and replaces some
614      * unwanted characters. For example it removes leading
615      * spaces in front of velocity commands and replaces
616      * tabs with spaces to prevent bounces in different
617      * code editors with different tab-width-setting.
618      * 
619      * @param resource the input stream to filter
620      * 
621      * @return the filtered input stream.
622      * 
623      * @throws IOException if creating, reading or writing to a stream fails.
624      */
625     protected InputStream filter(InputStream resource) throws IOException
626     {
627         InputStreamReader streamReader;
628         if (inputEncoding != null)
629         {
630             streamReader = new InputStreamReader(resource, inputEncoding);
631         }
632         else
633         {
634             streamReader = new InputStreamReader(resource);
635         }
636         LineNumberReader lineNumberReader = new LineNumberReader(streamReader);
637         String line = null;
638         ByteArrayOutputStream baos = new ByteArrayOutputStream();
639         PrintStream ps = null;
640         if (inputEncoding != null)
641         {
642             ps = new PrintStream(baos, true, inputEncoding);
643         }
644         else
645         {
646             ps = new PrintStream(baos, true);
647         }
648 
649         while ((line = lineNumberReader.readLine()) != null)
650         {
651             // remove leading spaces in front of velocity commands and comments
652             line = line.replaceAll("^//s*#", "#");
653             // replace tabs with spaces to prevent bounces in editors
654             line = line.replaceAll("\t", "    ");
655             ps.println(line);
656         }
657         ps.flush();
658         ps.close();
659 
660         return new ByteArrayInputStream(baos.toByteArray());
661     }
662 
663     
664     /***
665      * A custom classpath resource loader which filters tabs and removes spaces
666      * from lines with velocity commands. 
667      */
668     public static class TorqueClasspathResourceLoader 
669             extends ClasspathResourceLoader
670     {
671         /***
672          * The task in which this resource loader is used.
673          */
674         private TorqueDataModelTask task;
675         
676         /***
677          * Constructor.
678          *
679          * @param task the task in which this resource loader is used.
680          */
681         public TorqueClasspathResourceLoader(TorqueDataModelTask task)
682         {
683             this.task = task;
684         }
685         
686         /***
687          * @see org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader#getResourceStream(java.lang.String)
688          */
689         public synchronized InputStream getResourceStream(String name) 
690                 throws ResourceNotFoundException 
691         {
692             InputStream source = null;
693             try 
694             {
695                 source = super.getResourceStream(name);
696                 return task.filter(source);
697             } 
698             catch (IOException uee)
699             {
700                 task.log(uee.getMessage());
701                 throw new ResourceNotFoundException(uee.getMessage());
702             }
703             finally
704             {
705                 if (source != null)
706                 {
707                     try
708                     {
709                         source.close();
710                     }
711                     catch (IOException e)
712                     {
713                         task.log(e.getMessage());
714                     }
715                 }
716             }
717         }
718     }
719 
720     /***
721      * A custom file resource loader which filters tabs and removes spaces
722      * from lines with velocity commands. 
723      */
724     public static class TorqueFileResourceLoader
725             extends FileResourceLoader
726     {
727         /***
728          * The task in which this resource loader is used.
729          */
730         private TorqueDataModelTask task;
731         
732         /***
733          * Constructor.
734          *
735          * @param task the task in which this resource loader is used.
736          */
737         public TorqueFileResourceLoader(TorqueDataModelTask task)
738         {
739             this.task = task;
740         }
741         
742         /***
743          * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader#getResourceStream(java.lang.String)
744          */
745         public synchronized InputStream getResourceStream(
746                 String name) 
747                 throws ResourceNotFoundException
748         {
749             InputStream source = null;
750             try 
751             {
752                 source = super.getResourceStream(name);
753                 return task.filter(source);
754             } 
755             catch (IOException uee)
756             {
757                 task.log(uee.getMessage());
758                 throw new ResourceNotFoundException(uee.getMessage());
759             }
760             finally
761             {
762                 if (source != null)
763                 {
764                     try
765                     {
766                         source.close();
767                     }
768                     catch (IOException e)
769                     {
770                         task.log(e.getMessage());
771                     }
772                 }
773             }
774         }
775     }
776 }
777 
778