1 package org.apache.velocity.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.velocity.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 463298 2006-10-12 16:10:32Z henning $
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 if (property.endsWith("file.contents"))
568 {
569 // We need to turn the license file from relative to
570 // absolute, and let Ant help :)
571 value = StringUtils.fileContentsToString(
572 project.resolveFile(value).getCanonicalPath());
573
574 property = property.substring(
575 0, property.indexOf("file.contents") - 1);
576 }
577
578 c.put(property, value);
579 }
580 }
581 }
582 }
583
584 writer.write(generator.parse(controlTemplate, c));
585 writer.flush();
586 writer.close();
587 generator.shutdown();
588 cleanup();
589 }
590 catch( BuildException e)
591 {
592 throw e;
593 }
594 catch( MethodInvocationException e )
595 {
596 throw new BuildException(
597 "Exception thrown by '" + e.getReferenceName() + "." +
598 e.getMethodName() +"'" + ERR_MSG_FRAGMENT,
599 e.getWrappedThrowable());
600 }
601 catch( ParseErrorException e )
602 {
603 throw new BuildException("Velocity syntax error" + ERR_MSG_FRAGMENT ,e);
604 }
605 catch( ResourceNotFoundException e )
606 {
607 throw new BuildException("Resource not found" + ERR_MSG_FRAGMENT,e);
608 }
609 catch( Exception e )
610 {
611 throw new BuildException("Generation failed" + ERR_MSG_FRAGMENT ,e);
612 }
613 }
614
615 /**
616 * <p>Place useful objects into the initial context.</p>
617 *
618 * <p>TexenTask places <code>Date().toString()</code> into the
619 * context as <code>$now</code>. Subclasses who want to vary the
620 * objects in the context should override this method.</p>
621 *
622 * <p><code>$generator</code> is not put into the context in this
623 * method.</p>
624 *
625 * @param context The context to populate, as retrieved from
626 * {@link #initControlContext()}.
627 *
628 * @throws Exception Error while populating context. The {@link
629 * #execute()} method will catch and rethrow as a
630 * <code>BuildException</code>.
631 */
632 protected void populateInitialContext(Context context)
633 throws Exception
634 {
635 context.put("now", new Date().toString());
636 }
637
638 /**
639 * A hook method called at the end of {@link #execute()} which can
640 * be overridden to perform any necessary cleanup activities (such
641 * as the release of database connections, etc.). By default,
642 * does nothing.
643 *
644 * @exception Exception Problem cleaning up.
645 */
646 protected void cleanup()
647 throws Exception
648 {
649 }
650 }