View Javadoc

1   package org.apache.texen.ant;
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.File;
23  import java.io.FileInputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.Writer;
27  import java.util.Date;
28  import java.util.Iterator;
29  import java.util.StringTokenizer;
30  
31  import org.apache.commons.collections.ExtendedProperties;
32  import org.apache.tools.ant.BuildException;
33  import org.apache.tools.ant.Project;
34  import org.apache.tools.ant.Task;
35  import org.apache.velocity.VelocityContext;
36  import org.apache.velocity.app.VelocityEngine;
37  import org.apache.velocity.context.Context;
38  import org.apache.velocity.exception.MethodInvocationException;
39  import org.apache.velocity.exception.ParseErrorException;
40  import org.apache.velocity.exception.ResourceNotFoundException;
41  import org.apache.velocity.runtime.RuntimeConstants;
42  import org.apache.texen.Generator;
43  import org.apache.velocity.util.StringUtils;
44  
45  /**
46   * An ant task for generating output by using Velocity
47   *
48   * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
49   * @author <a href="robertdonkin@mac.com">Robert Burrell Donkin</a>
50   * @version $Id: TexenTask.java 550224 2007-06-24 13:41:05Z wglass $
51   */
52  public class TexenTask
53      extends Task
54  {
55      /**
56       * This message fragment (telling users to consult the log or
57       * invoke ant with the -debug flag) is appended to rethrown
58       * exception messages.
59       */
60      private final static String ERR_MSG_FRAGMENT =
61          ". For more information consult the velocity log, or invoke ant " +
62          "with the -debug flag.";
63  
64      /**
65       * This is the control template that governs the output.
66       * It may or may not invoke the services of worker
67       * templates.
68       */
69      protected String controlTemplate;
70  
71      /**
72       * This is where Velocity will look for templates
73       * using the file template loader.
74       */
75      protected String templatePath;
76  
77      /**
78       * This is where texen will place all the output
79       * that is a product of the generation process.
80       */
81      protected String outputDirectory;
82  
83      /**
84       * This is the file where the generated text
85       * will be placed.
86       */
87      protected String outputFile;
88  
89      /**
90       * This is the encoding for the output file(s).
91       */
92      protected String outputEncoding;
93  
94      /**
95       * This is the encoding for the input file(s)
96       * (templates).
97       */
98      protected String inputEncoding;
99  
100     /**
101      * <p>
102      * These are properties that are fed into the
103      * initial context from a properties file. This
104      * is simply a convenient way to set some values
105      * that you wish to make available in the context.
106      * </p>
107      * <p>
108      * These values are not critical, like the template path
109      * or output path, but allow a convenient way to
110      * set a value that may be specific to a particular
111      * generation task.
112      * </p>
113      * <p>
114      * For example, if you are generating scripts to allow
115      * user to automatically create a database, then
116      * you might want the <code>$databaseName</code>
117      * to be placed
118      * in the initial context so that it is available
119      * in a script that might look something like the
120      * following:
121      * <code><pre>
122      * #!bin/sh
123      *
124      * echo y | mysqladmin create $databaseName
125      * </pre></code>
126      * The value of <code>$databaseName</code> isn't critical to
127      * output, and you obviously don't want to change
128      * the ant task to simply take a database name.
129      * So initial context values can be set with
130      * properties file.
131      */
132     protected ExtendedProperties contextProperties;
133 
134     /**
135      * Property which controls whether the classpath
136      * will be used when trying to locate templates.
137      */
138     protected boolean useClasspath;
139 
140     /**
141      * The LogFile (incl. path) to log to.
142      */
143     protected String logFile;
144 
145     /**
146      *   Property which controls whether the resource
147      *   loader will be told to cache.  Default false
148      */
149 
150     protected String useResourceLoaderCache = "false";
151     /**
152      *
153      */
154     protected String resourceLoaderModificationCheckInterval = "2";
155 
156     /**
157      * [REQUIRED] Set the control template for the
158      * generating process.
159      * @param controlTemplate
160      */
161     public void setControlTemplate (String controlTemplate)
162     {
163         this.controlTemplate = controlTemplate;
164     }
165 
166     /**
167      * Get the control template for the
168      * generating process.
169      * @return The current control template.
170      */
171     public String getControlTemplate()
172     {
173         return controlTemplate;
174     }
175 
176     /**
177      * [REQUIRED] Set the path where Velocity will look
178      * for templates using the file template
179      * loader.
180      * @param templatePath
181      * @throws Exception
182      */
183 
184     public void setTemplatePath(String templatePath) throws Exception
185     {
186         StringBuffer resolvedPath = new StringBuffer();
187         StringTokenizer st = new StringTokenizer(templatePath, ",");
188         while ( st.hasMoreTokens() )
189         {
190             // resolve relative path from basedir and leave
191             // absolute path untouched.
192             File fullPath = project.resolveFile(st.nextToken());
193             resolvedPath.append(fullPath.getCanonicalPath());
194             if ( st.hasMoreTokens() )
195             {
196                 resolvedPath.append(",");
197             }
198         }
199         this.templatePath = resolvedPath.toString();
200 
201         System.out.println(templatePath);
202      }
203 
204     /**
205      * Get the path where Velocity will look
206      * for templates using the file template
207      * loader.
208      * @return The template path.
209      */
210     public String getTemplatePath()
211     {
212         return templatePath;
213     }
214 
215     /**
216      * [REQUIRED] Set the output directory. It will be
217      * created if it doesn't exist.
218      * @param outputDirectory
219      */
220     public void setOutputDirectory(File outputDirectory)
221     {
222         try
223         {
224             this.outputDirectory = outputDirectory.getCanonicalPath();
225         }
226         catch (java.io.IOException ioe)
227         {
228             throw new BuildException(ioe);
229         }
230     }
231 
232     /**
233      * Get the output directory.
234      * @return The output directory.
235      */
236     public String getOutputDirectory()
237     {
238         return outputDirectory;
239     }
240 
241     /**
242      * [REQUIRED] Set the output file for the
243      * generation process.
244      * @param outputFile
245      */
246     public void setOutputFile(String outputFile)
247     {
248         this.outputFile = outputFile;
249     }
250 
251     /**
252      * Set the output encoding.
253      * @param outputEncoding
254      */
255     public void setOutputEncoding(String outputEncoding)
256     {
257         this.outputEncoding = outputEncoding;
258     }
259 
260     /**
261      * Set the input (template) encoding.
262      * @param inputEncoding
263      */
264     public void setInputEncoding(String inputEncoding)
265     {
266         this.inputEncoding = inputEncoding;
267     }
268 
269     /**
270      * Get the output file for the
271      * generation process.
272      * @return The output file.
273      */
274     public String getOutputFile()
275     {
276         return outputFile;
277     }
278 
279     /**
280      * Sets the log file.
281      * @param log
282      */
283     public void setLogFile(String log)
284     {
285         this.logFile = log;
286     }
287 
288     /**
289      * Gets the log file.
290      * @return The log file.
291      */
292     public String getLogFile()
293     {
294         return this.logFile;
295     }
296 
297     /**
298      * Set the context properties that will be
299      * fed into the initial context be the
300      * generating process starts.
301      * @param file
302      */
303     public void setContextProperties( String file )
304     {
305         String[] sources = StringUtils.split(file,",");
306         contextProperties = new ExtendedProperties();
307 
308         // Always try to get the context properties resource
309         // from a file first. Templates may be taken from a JAR
310         // file but the context properties resource may be a
311         // resource in the filesystem. If this fails than attempt
312         // to get the context properties resource from the
313         // classpath.
314         for (int i = 0; i < sources.length; i++)
315         {
316             ExtendedProperties source = new ExtendedProperties();
317 
318             try
319             {
320                 // resolve relative path from basedir and leave
321                 // absolute path untouched.
322                 File fullPath = project.resolveFile(sources[i]);
323                 log("Using contextProperties file: " + fullPath);
324                 source.load(new FileInputStream(fullPath));
325             }
326             catch (IOException e)
327             {
328                 ClassLoader classLoader = this.getClass().getClassLoader();
329 
330                 try
331                 {
332                     InputStream inputStream = classLoader.getResourceAsStream(sources[i]);
333 
334                     if (inputStream == null)
335                     {
336                         throw new BuildException("Context properties file " + sources[i] +
337                             " could not be found in the file system or on the classpath!");
338                     }
339                     else
340                     {
341                         source.load(inputStream);
342                     }
343                 }
344                 catch (IOException ioe)
345                 {
346                     source = null;
347                 }
348             }
349 
350             if (source != null)
351             {
352                 for (Iterator j = source.getKeys(); j.hasNext(); )
353                 {
354                     String name = (String) j.next();
355                     String value = StringUtils.nullTrim(source.getString(name));
356                     contextProperties.setProperty(name,value);
357                 }
358             }
359         }
360     }
361 
362     /**
363      * Get the context properties that will be
364      * fed into the initial context be the
365      * generating process starts.
366      * @return The current context properties.
367      */
368     public ExtendedProperties getContextProperties()
369     {
370         return contextProperties;
371     }
372 
373     /**
374      * Set the use of the classpath in locating templates
375      *
376      * @param useClasspath true means the classpath will be used.
377      */
378     public void setUseClasspath(boolean useClasspath)
379     {
380         this.useClasspath = useClasspath;
381     }
382 
383     /**
384      * @param useResourceLoaderCache
385      */
386     public void setUseResourceLoaderCache(String useResourceLoaderCache)
387     {
388         this.useResourceLoaderCache = useResourceLoaderCache;
389     }
390 
391     /**
392      * @param resourceLoaderModificationCheckInterval
393      */
394     public void setResourceLoaderModificationCheckInterval(String resourceLoaderModificationCheckInterval)
395     {
396         this.resourceLoaderModificationCheckInterval = resourceLoaderModificationCheckInterval;
397     }
398     /**
399      * Creates a VelocityContext.
400      *
401      * @return new Context
402      * @throws Exception the execute method will catch
403      *         and rethrow as a <code>BuildException</code>
404      */
405     public Context initControlContext()
406         throws Exception
407     {
408         return new VelocityContext();
409     }
410 
411     /**
412      * Execute the input script with Velocity
413      *
414      * @throws BuildException
415      * BuildExceptions are thrown when required attributes are missing.
416      * Exceptions thrown by Velocity are rethrown as BuildExceptions.
417      */
418     public void execute ()
419         throws BuildException
420     {
421         // Make sure the template path is set.
422         if (templatePath == null && useClasspath == false)
423         {
424             throw new BuildException(
425                 "The template path needs to be defined if you are not using " +
426                 "the classpath for locating templates!");
427         }
428 
429         // Make sure the control template is set.
430         if (controlTemplate == null)
431         {
432             throw new BuildException("The control template needs to be defined!");
433         }
434 
435         // Make sure the output directory is set.
436         if (outputDirectory == null)
437         {
438             throw new BuildException("The output directory needs to be defined!");
439         }
440 
441         // Make sure there is an output file.
442         if (outputFile == null)
443         {
444             throw new BuildException("The output file needs to be defined!");
445         }
446 
447         VelocityEngine ve = new VelocityEngine();
448 
449         try
450         {
451             // Setup the Velocity Runtime.
452             if (templatePath != null)
453             {
454                 log("Using templatePath: " + templatePath, Project.MSG_VERBOSE);
455                 ve.setProperty(
456                     RuntimeConstants.FILE_RESOURCE_LOADER_PATH, templatePath);
457             }
458 
459             if (useClasspath)
460             {
461                 log("Using classpath");
462                 ve.addProperty(
463                     VelocityEngine.RESOURCE_LOADER, "classpath");
464 
465                 ve.setProperty(
466                     "classpath." + VelocityEngine.RESOURCE_LOADER + ".class",
467                         "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
468 
469                 ve.setProperty(
470                     "classpath." + VelocityEngine.RESOURCE_LOADER +
471                         ".cache", useResourceLoaderCache);
472 
473                 ve.setProperty(
474                     "classpath." + VelocityEngine.RESOURCE_LOADER +
475                         ".modificationCheckInterval", resourceLoaderModificationCheckInterval);
476             }
477 
478             if (this.logFile != null)
479             {
480                 ve.setProperty(RuntimeConstants.RUNTIME_LOG, this.logFile);
481             }
482 
483             ve.init();
484 
485             // Create the text generator.
486             Generator generator = Generator.getInstance();
487             generator.setVelocityEngine(ve);
488             generator.setOutputPath(outputDirectory);
489             generator.setInputEncoding(inputEncoding);
490             generator.setOutputEncoding(outputEncoding);
491 
492             if (templatePath != null)
493             {
494                 generator.setTemplatePath(templatePath);
495             }
496 
497             // Make sure the output directory exists, if it doesn't
498             // then create it.
499             File file = new File(outputDirectory);
500             if (! file.exists())
501             {
502                 file.mkdirs();
503             }
504 
505             String path = outputDirectory + File.separator + outputFile;
506             log("Generating to file " + path, Project.MSG_INFO);
507             Writer writer = generator.getWriter(path, outputEncoding);
508 
509             // The generator and the output path should
510             // be placed in the init context here and
511             // not in the generator class itself.
512             Context c = initControlContext();
513 
514             // Everything in the generator class should be
515             // pulled out and placed in here. What the generator
516             // class does can probably be added to the Velocity
517             // class and the generator class can probably
518             // be removed all together.
519             populateInitialContext(c);
520 
521             // Feed all the options into the initial
522             // control context so they are available
523             // in the control/worker templates.
524             if (contextProperties != null)
525             {
526                 Iterator i = contextProperties.getKeys();
527 
528                 while (i.hasNext())
529                 {
530                     String property = (String) i.next();
531                     String value = StringUtils.nullTrim(contextProperties.getString(property));
532 
533                     // Now lets quickly check to see if what
534                     // we have is numeric and try to put it
535                     // into the context as an Integer.
536                     try
537                     {
538                         c.put(property, new Integer(value));
539                     }
540                     catch (NumberFormatException nfe)
541                     {
542                         // Now we will try to place the value into
543                         // the context as a boolean value if it
544                         // maps to a valid boolean value.
545                         String booleanString =
546                             contextProperties.testBoolean(value);
547 
548                         if (booleanString != null)
549                         {
550                             c.put(property, Boolean.valueOf(booleanString));
551                         }
552                         else
553                         {
554                             // We are going to do something special
555                             // for properties that have a ".file.contents"
556                             // suffix: for these properties will pull
557                             // in the contents of the file and make
558                             // them available in the context. So for
559                             // a line like the following in a properties file:
560                             //
561                             // license.file.contents = license.txt
562                             //
563                             // We will pull in the contents of license.txt
564                             // and make it available in the context as
565                             // $license. This should make texen a little
566                             // more flexible.
567                             final String FILE_CONTENTS = ".file.contents";
568                             if (property.endsWith(FILE_CONTENTS))
569                             {
570                                 // We need to turn the license file from relative to
571                                 // absolute, and let Ant help :)
572                                 value = StringUtils.fileContentsToString(
573                                     project.resolveFile(value).getCanonicalPath());
574 
575                                 property = property.substring(
576                                     0, property.indexOf(FILE_CONTENTS));
577                             }
578 
579                             c.put(property, value);
580                         }
581                     }
582                 }
583             }
584 
585             writer.write(generator.parse(controlTemplate, c));
586             writer.flush();
587             writer.close();
588             generator.shutdown();
589             cleanup();
590         }
591         catch( BuildException e)
592         {
593             throw e;
594         }
595         catch( MethodInvocationException e )
596         {
597             throw new BuildException(
598                 "Exception thrown by '" + e.getReferenceName() + "." +
599                     e.getMethodName() +"'" + ERR_MSG_FRAGMENT,
600                         e.getWrappedThrowable());
601         }
602         catch( ParseErrorException e )
603         {
604             throw new BuildException("Velocity syntax error" + ERR_MSG_FRAGMENT ,e);
605         }
606         catch( ResourceNotFoundException e )
607         {
608             throw new BuildException("Resource not found" + ERR_MSG_FRAGMENT,e);
609         }
610         catch( Exception e )
611         {
612             throw new BuildException("Generation failed" + ERR_MSG_FRAGMENT ,e);
613         }
614     }
615 
616     /**
617      * <p>Place useful objects into the initial context.</p>
618      *
619      * <p>TexenTask places <code>Date().toString()</code> into the
620      * context as <code>$now</code>.  Subclasses who want to vary the
621      * objects in the context should override this method.</p>
622      *
623      * <p><code>$generator</code> is not put into the context in this
624      * method.</p>
625      *
626      * @param context The context to populate, as retrieved from
627      * {@link #initControlContext()}.
628      *
629      * @throws Exception Error while populating context.  The {@link
630      * #execute()} method will catch and rethrow as a
631      * <code>BuildException</code>.
632      */
633     protected void populateInitialContext(Context context)
634         throws Exception
635     {
636         context.put("now", new Date().toString());
637     }
638 
639     /**
640      * A hook method called at the end of {@link #execute()} which can
641      * be overridden to perform any necessary cleanup activities (such
642      * as the release of database connections, etc.).  By default,
643      * does nothing.
644      *
645      * @exception Exception Problem cleaning up.
646      */
647     protected void cleanup()
648         throws Exception
649     {
650     }
651 }