1 /*
2 * Copyright (c) 2003 The Visigoth Software Society. All rights
3 * reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * 3. The end-user documentation included with the redistribution, if
18 * any, must include the following acknowledgement:
19 * "This product includes software developed by the
20 * Visigoth Software Society (http://www.visigoths.org/)."
21 * Alternately, this acknowledgement may appear in the software itself,
22 * if and wherever such third-party acknowledgements normally appear.
23 *
24 * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
25 * project contributors may be used to endorse or promote products derived
26 * from this software without prior written permission. For written
27 * permission, please contact visigoths@visigoths.org.
28 *
29 * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
30 * nor may "FreeMarker" or "Visigoth" appear in their names
31 * without prior written permission of the Visigoth Software Society.
32 *
33 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
34 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
35 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
36 * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
37 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
38 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
39 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
40 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
41 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
42 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
43 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
44 * SUCH DAMAGE.
45 * ====================================================================
46 *
47 * This software consists of voluntary contributions made by many
48 * individuals on behalf of the Visigoth Software Society. For more
49 * information on the Visigoth Software Society, please see
50 * http://www.visigoths.org/
51 */
52
53 package freemarker.ext.ant;
54
55 import java.io;
56 import java.util;
57
58 import org.w3c.dom;
59 import org.xml.sax.SAXParseException;
60 import javax.xml.parsers.DocumentBuilder;
61 import javax.xml.parsers.DocumentBuilderFactory;
62 import javax.xml.parsers.ParserConfigurationException;
63
64 import org.apache.tools.ant.BuildException;
65 import org.apache.tools.ant.DirectoryScanner;
66 import org.apache.tools.ant.Project;
67 import org.apache.tools.ant.taskdefs.MatchingTask;
68 import freemarker.ext.xml.NodeListModel;
69 import freemarker.ext.dom.NodeModel;
70 import freemarker.template.utility.ClassUtil;
71 import freemarker.template.utility.SecurityUtilities;
72 import freemarker.template;
73
74
75 /**
76 * <p>This is an <a href="http://jakarta.apache.org/ant/" target="_top">Ant</a> task for transforming
77 * XML documents using FreeMarker templates. It uses the adapter class
78 * {@link NodeListModel}. It will read a set of XML documents, and pass them to
79 * the template for processing, building the corresponding output files in the
80 * destination directory.</p>
81 * <p>It makes the following variables available to the template in the data model:</p>
82 * <ul>
83 * <li><tt>document</tt>: <em>Deprecated!</em> The DOM tree of the currently processed XML file wrapped
84 with the legacy {@link freemarker.ext.xml.NodeListModel}.
85 For new projects you should use the <tt>.node</tt> instead, which initially
86 contains the DOM Document wrapped with {@link freemarker.ext.dom.NodeModel}.</li>
87 * <li><tt>properties</tt>: a {@link freemarker.template.SimpleHash} containing
88 * properties of the project that executes the task</li>
89 * <li><tt>userProperties</tt>: a {@link freemarker.template.SimpleHash} containing
90 * user properties of the project that executes the task</li>
91 * <li><tt>project</tt>: the DOM tree of the XML file specified by the
92 * <tt>projectfile</tt>. It will not be available if you didn't specify the
93 * <tt>projectfile</tt> attribute.</li>
94 * <li>further custom models can be instantiated and made available to the
95 * templates using the <tt>models</tt> attribute.</li>
96 * </ul>
97 * <p>It supports the following attributes:</p>
98 * <table border="1" cellpadding="2" cellspacing="0">
99 * <tr>
100 * <th valign="top" align="left">Attribute</th>
101 * <th valign="top" align="left">Description</th>
102 * <th valign="top">Required</th>
103 * </tr>
104 * <tr>
105 * <td valign="top">basedir</td>
106 * <td valign="top">location of the XML files. Defaults to the project's
107 * basedir.</td>
108 * <td align="center" valign="top">No</td>
109 * </tr>
110 * <tr>
111 * <td valign="top">destdir</td>
112 * <td valign="top">location to store the generated files.</td>
113 * <td align="center" valign="top">Yes</td>
114 * </tr>
115 * <tr>
116 * <td valign="top">includes</td>
117 * <td valign="top">comma-separated list of patterns of files that must be
118 * included; all files are included when omitted.</td>
119 * <td valign="top" align="center">No</td>
120 * </tr>
121 * <tr>
122 * <td valign="top">includesfile</td>
123 * <td valign="top">the name of a file that contains
124 * include patterns.</td>
125 * <td valign="top" align="center">No</td>
126 * </tr>
127 * <tr>
128 * <td valign="top">excludes</td>
129 * <td valign="top">comma-separated list of patterns of files that must be
130 * excluded; no files (except default excludes) are excluded when omitted.</td>
131 * <td valign="top" align="center">No</td>
132 * </tr>
133 * <tr>
134 * <td valign="top">excludesfile</td>
135 * <td valign="top">the name of a file that contains
136 * exclude patterns.</td>
137 * <td valign="top" align="center">No</td>
138 * </tr>
139 * <tr>
140 * <td valign="top">defaultexcludes</td>
141 * <td valign="top">indicates whether default excludes should be used
142 * (<code>yes</code> | <code>no</code>); default excludes are used when omitted.</td>
143 * <td valign="top" align="center">No</td>
144 * </tr>
145 * <tr>
146 * <td valign="top">extension</td>
147 * <td valign="top">extension of generated files. Defaults to .html.</td>
148 * <td valign="top" align="center">No</td>
149 * </tr>
150 * <tr>
151 * <td valign="top">template</td>
152 * <td valign="top">name of the FreeMarker template file that will be
153 * applied by default to XML files</td>
154 * <td valign="top" align="center">No</td>
155 * </tr>
156 * <tr>
157 * <td valign="top">templateDir</td>
158 * <td valign="top">location of the FreeMarker template(s) to be used, defaults
159 * to the project's baseDir</td>
160 * <td valign="top" align="center">No</td>
161 * </tr>
162 * <tr>
163 * <td valign="top">projectfile</td>
164 * <td valign="top">path to the project file. The poject file must be an XML file.
165 * If omitted, it will not be available to templates </td>
166 * <td valign="top" align="center">No</td>
167 * </tr>
168 * <tr>
169 * <td valign="top">incremental</td>
170 * <td valign="top">indicates whether all files should be regenerated (no), or
171 * only those that are older than the XML file, the template file, or the
172 * project file (yes). Defaults to yes. </td>
173 * <td valign="top" align="center">No</td>
174 * </tr>
175 * <tr>
176 * <td valign="top">encoding</td>
177 * <td valign="top">The encoding of the output files. Defaults to platform
178 * default encoding.</td>
179 * <td valign="top" align="center">No</td>
180 * </tr>
181 * <tr>
182 * <td valign="top">templateEncoding</td>
183 * <td valign="top">The encoding of the template files. Defaults to platform
184 * default encoding.</td>
185 * <td valign="top" align="center">No</td>
186 * </tr>
187 * <tr>
188 * <td valign="top">validation</td>
189 * <td valign="top">Whether to validate the XML input. Defaults to off.</td>
190 * <td valign="top" align="center">No</td>
191 * </tr>
192 * <tr>
193 * <td valign="top">models</td>
194 * <td valign="top">A list of [name=]className pairs separated by spaces,
195 * commas, or semicolons that specifies further models that should be
196 * available to templates. If name is omitted, the unqualified class name
197 * is used as the name. Every class that is specified must implement the
198 * TemplateModel interface and have a no-args constructor.</td>
199 * <td valign="top" align="center">No</td>
200 * </tr>
201 * </table>
202 *
203 * <p>It supports the following nesed elements:</p>
204 * <table border="1" cellpadding="2" cellspacing="0">
205 * <tr>
206 * <th valign="top" align="left">Element</th>
207 * <th valign="top" align="left">Description</th>
208 * <th valign="top">Required</th>
209 * </tr>
210 * <tr>
211 * <td valign="top">prepareModel</td>
212 * <td valign="top">
213 * This element executes Jython script before the processing of each XML
214 * files, that you can use to modify the data model.
215 * You either enter the Jython script directly nested into this
216 * element, or specify a Jython script file with the <tt>file</tt>
217 * attribute.
218 * The following variables are added to the Jython runtime's local
219 * namespace before the script is invoked:
220 * <ul>
221 * <li><tt>model</tt>: The data model as <code>java.util.HashMap</code>.
222 * You can read and modify the data model with this variable.
223 * <li><tt>doc</tt>: The XML document as <code>org.w3c.dom.Document</code>.
224 * <li><tt>project</tt>: The project document (if used) as
225 * <code>org.w3c.dom.Document</code>.
226 * </ul>
227 * <i>If this element is used, Jython classes (tried with Jython 2.1)
228 * must be available.</i>
229 * </td>
230 * <td valign="top" align="center">No</td>
231 * </tr>
232 * <tr>
233 * <td valign="top">prepareEnvironment</td>
234 * <td valign="top">This element executes Jython script before the processing
235 * of each XML files, that you can use to modify the freemarker environment
236 * ({@link freemarker.core.Environment}). The script is executed after the
237 * <tt>prepareModel</tt> element. The accessible Jython variables are the
238 * same as with the <tt>prepareModel</tt> element, except that there is no
239 * <tt>model</tt> variable, but there is <tt>env</tt> variable, which is
240 * the FreeMarker environment ({@link freemarker.core.Environment}).
241 * <i>If this element is used, Jython classes (tried with Jython 2.1)
242 * must be available.</i>
243 * </td>
244 * <td valign="top" align="center">No</td>
245 * </tr>
246 * </table>
247 *
248 * @author Attila Szegedi
249 * @author Jonathan Revusky, jon@revusky.com
250 * @deprecated <a href="http://fmpp.sourceforge.net">FMPP</a> is a more complete solution.
251 * @version $Id: FreemarkerXmlTask.java,v 1.58.2.1 2006/04/26 11:07:58 revusky Exp $
252 */
253 public class FreemarkerXmlTask
254 extends
255 MatchingTask
256 {
257 private JythonAntTask prepareModel;
258 private JythonAntTask prepareEnvironment;
259 private final DocumentBuilderFactory builderFactory;
260 private DocumentBuilder builder;
261
262 /** the {@link Configuration} used by this task. */
263 private Configuration cfg = new Configuration();
264
265 /** the destination directory */
266 private File destDir;
267
268 /** the base directory */
269 private File baseDir;
270
271 //Where the templates live
272
273 private File templateDir;
274
275 /** the template= attribute */
276 private String templateName;
277
278 /** The template in its parsed form */
279 private Template parsedTemplate;
280
281 /** last modified of the template sheet */
282 private long templateFileLastModified = 0;
283
284 /** the projectFile= attribute */
285 private String projectAttribute = null;
286
287 private File projectFile = null;
288
289 /** The DOM tree of the project wrapped into FreeMarker TemplateModel */
290 private TemplateModel projectTemplate;
291 // The DOM tree wrapped using the freemarker.ext.dom wrapping.
292 private TemplateNodeModel projectNode;
293 private TemplateModel propertiesTemplate;
294 private TemplateModel userPropertiesTemplate;
295
296 /** last modified of the project file if it exists */
297 private long projectFileLastModified = 0;
298
299 /** check the last modified date on files. defaults to true */
300 private boolean incremental = true;
301
302 /** the default output extension is .html */
303 private String extension = ".html";
304
305 private String encoding = SecurityUtilities.getSystemProperty("file.encoding");
306 private String templateEncoding = encoding;
307 private boolean validation = false;
308
309 private String models = "";
310 private final Map modelsMap = new HashMap();
311
312
313
314 /**
315 * Constructor creates the SAXBuilder.
316 */
317 public FreemarkerXmlTask()
318 {
319 builderFactory = DocumentBuilderFactory.newInstance();
320 builderFactory.setNamespaceAware(true);
321 }
322
323 /**
324 * Set the base directory. Defaults to <tt>.</tt>
325 */
326 public void setBasedir(File dir)
327 {
328 baseDir = dir;
329 }
330
331 /**
332 * Set the destination directory into which the generated
333 * files should be copied to
334 * @param dir the name of the destination directory
335 */
336 public void setDestdir(File dir)
337 {
338 destDir = dir;
339 }
340
341 /**
342 * Set the output file extension. <tt>.html</tt> by default.
343 */
344 public void setExtension(String extension)
345 {
346 this.extension = extension;
347 }
348
349 public void setTemplate(String templateName) {
350 this.templateName = templateName;
351 }
352
353 public void setTemplateDir(File templateDir) throws BuildException {
354 this.templateDir = templateDir;
355 try {
356 cfg.setDirectoryForTemplateLoading(templateDir);
357 } catch (Exception e) {
358 throw new BuildException(e);
359 }
360 }
361
362 /**
363 * Set the path to the project XML file
364 */
365 public void setProjectfile(String projectAttribute)
366 {
367 this.projectAttribute = projectAttribute;
368 }
369
370 /**
371 * Turn on/off incremental processing. On by default
372 */
373 public void setIncremental(String incremental)
374 {
375 this.incremental = !(incremental.equalsIgnoreCase("false") || incremental.equalsIgnoreCase("no") || incremental.equalsIgnoreCase("off"));
376 }
377
378 /**
379 * Set encoding for generated files. Defaults to platform default encoding.
380 */
381 public void setEncoding(String encoding)
382 {
383 this.encoding = encoding;
384 }
385
386 public void setTemplateEncoding(String inputEncoding)
387 {
388 this.templateEncoding = inputEncoding;
389 }
390
391 /**
392 * Sets whether to validate the XML input.
393 */
394 public void setValidation(boolean validation)
395 {
396 this.validation = validation;
397 }
398
399 public void setModels(String models)
400 {
401 this.models = models;
402 }
403
404 public void execute() throws BuildException
405 {
406 DirectoryScanner scanner;
407 String[] list;
408
409 if (baseDir == null)
410 {
411 baseDir = getProject().getBaseDir();
412 }
413 if (destDir == null )
414 {
415 String msg = "destdir attribute must be set!";
416 throw new BuildException(msg, getLocation());
417 }
418
419 File templateFile = null;
420
421 if (templateDir == null) {
422 if (templateName != null) {
423 templateFile = new File(templateName);
424 if (!templateFile.isAbsolute()) {
425 templateFile = new File(getProject().getBaseDir(), templateName);
426 }
427 templateDir = templateFile.getParentFile();
428 templateName = templateFile.getName();
429 }
430 else {
431 templateDir = baseDir;
432 }
433 setTemplateDir(templateDir);
434 } else if (templateName != null) {
435 if (new File(templateName).isAbsolute()) {
436 throw new BuildException("Do not specify an absolute location for the template as well as a templateDir");
437 }
438 templateFile = new File(templateDir, templateName);
439 }
440 if (templateFile != null) {
441 templateFileLastModified = templateFile.lastModified();
442 }
443
444 try {
445 if (templateName != null) {
446 parsedTemplate = cfg.getTemplate(templateName, templateEncoding);
447 }
448 }
449 catch (IOException ioe) {
450 throw new BuildException(ioe.toString());
451 }
452 // get the last modification of the template
453 log("Transforming into: " + destDir.getAbsolutePath(), Project.MSG_INFO);
454
455 // projectFile relative to baseDir
456 if (projectAttribute != null && projectAttribute.length() > 0)
457 {
458 projectFile = new File(baseDir, projectAttribute);
459 if (projectFile.isFile())
460 projectFileLastModified = projectFile.lastModified();
461 else
462 {
463 log ("Project file is defined, but could not be located: " +
464 projectFile.getAbsolutePath(), Project.MSG_INFO );
465 projectFile = null;
466 }
467 }
468
469 generateModels();
470
471 // find the files/directories
472 scanner = getDirectoryScanner(baseDir);
473
474 propertiesTemplate = wrapMap(project.getProperties());
475 userPropertiesTemplate = wrapMap(project.getUserProperties());
476
477 builderFactory.setValidating(validation);
478 try
479 {
480 builder = builderFactory.newDocumentBuilder();
481 }
482 catch(ParserConfigurationException e)
483 {
484 throw new BuildException("Could not create document builder", e, getLocation());
485 }
486
487 // get a list of files to work on
488 list = scanner.getIncludedFiles();
489
490
491 for (int i = 0;i < list.length; ++i)
492 {
493 process(baseDir, list[i], destDir);
494 }
495 }
496
497 public void addConfiguredJython(JythonAntTask jythonAntTask) {
498 this.prepareEnvironment = jythonAntTask;
499 }
500
501 public void addConfiguredPrepareModel(JythonAntTask prepareModel) {
502 this.prepareModel = prepareModel;
503 }
504
505 public void addConfiguredPrepareEnvironment(JythonAntTask prepareEnvironment) {
506 this.prepareEnvironment = prepareEnvironment;
507 }
508
509 /**
510 * Process an XML file using FreeMarker
511 */
512 private void process(File baseDir, String xmlFile, File destDir)
513 throws BuildException
514 {
515 File outFile=null;
516 File inFile=null;
517 try
518 {
519 // the current input file relative to the baseDir
520 inFile = new File(baseDir,xmlFile);
521 // the output file relative to basedir
522 outFile = new File(destDir,
523 xmlFile.substring(0,
524 xmlFile.lastIndexOf('.')) + extension);
525
526 // only process files that have changed
527 if (!incremental ||
528 (inFile.lastModified() > outFile.lastModified() ||
529 templateFileLastModified > outFile.lastModified() ||
530 projectFileLastModified > outFile.lastModified()))
531 {
532 ensureDirectoryFor(outFile);
533
534 //-- command line status
535 log("Input: " + xmlFile, Project.MSG_INFO );
536
537 if (projectTemplate == null && projectFile != null) {
538 Document doc = builder.parse(projectFile);
539 projectTemplate = new NodeListModel(builder.parse(projectFile));
540 projectNode = NodeModel.wrap(doc);
541 }
542
543 // Build the file DOM
544 Document docNode = builder.parse(inFile);
545
546 TemplateModel document = new NodeListModel(docNode);
547 TemplateNodeModel docNodeModel = NodeModel.wrap(docNode);
548 HashMap root = new HashMap();
549 root.put("document", document);
550 insertDefaults(root);
551
552 // Process the template and write out
553 // the result as the outFile.
554 Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile), encoding));
555 try
556 {
557 if (parsedTemplate == null) {
558 throw new BuildException("No template file specified in build script or in XML file");
559 }
560 if (prepareModel != null) {
561 Map vars = new HashMap();
562 vars.put("model", root);
563 vars.put("doc", docNode);
564 if (projectNode != null) {
565 vars.put("project", ((NodeModel) projectNode).getNode());
566 }
567 prepareModel.execute(vars);
568 }
569 freemarker.core.Environment env = parsedTemplate.createProcessingEnvironment(root, writer);
570 env.setCurrentVisitorNode(docNodeModel);
571 if (prepareEnvironment != null) {
572 Map vars = new HashMap();
573 vars.put("env", env);
574 vars.put("doc", docNode);
575 if (projectNode != null) {
576 vars.put("project", ((NodeModel) projectNode).getNode());
577 }
578 prepareEnvironment.execute(vars);
579 }
580 env.process();
581 writer.flush();
582 }
583 finally
584 {
585 writer.close();
586 }
587
588 log("Output: " + outFile, Project.MSG_INFO );
589
590 }
591 }
592 catch (SAXParseException spe)
593 {
594 Throwable rootCause = spe;
595 if (spe.getException() != null)
596 rootCause = spe.getException();
597 log("XML parsing error in " + inFile.getAbsolutePath(), Project.MSG_ERR);
598 log("Line number " + spe.getLineNumber());
599 log("Column number " + spe.getColumnNumber());
600 throw new BuildException(rootCause, getLocation());
601 }
602 catch (Throwable e)
603 {
604 if (outFile != null ) {
605 if(!outFile.delete() && outFile.exists()) {
606 log("Failed to delete " + outFile, Project.MSG_WARN);
607 }
608 }
609 e.printStackTrace();
610 throw new BuildException(e, getLocation());
611 }
612 }
613
614 private void generateModels()
615 {
616 StringTokenizer modelTokenizer = new StringTokenizer(models, ",; ");
617 while(modelTokenizer.hasMoreTokens())
618 {
619 String modelSpec = modelTokenizer.nextToken();
620 String name = null;
621 String clazz = null;
622
623 int sep = modelSpec.indexOf('=');
624 if(sep == -1)
625 {
626 // No explicit name - use unqualified class name
627 clazz = modelSpec;
628 int dot = clazz.lastIndexOf('.');
629 if(dot == -1)
630 {
631 // clazz in the default package
632 name = clazz;
633 }
634 else
635 {
636 name = clazz.substring(dot + 1);
637 }
638 }
639 else
640 {
641 name = modelSpec.substring(0, sep);
642 clazz = modelSpec.substring(sep + 1);
643 }
644 try
645 {
646 modelsMap.put(name, ClassUtil.forName(clazz).newInstance());
647 }
648 catch(Exception e)
649 {
650 throw new BuildException(e);
651 }
652 }
653 }
654
655 /**
656 * create directories as needed
657 */
658 private void ensureDirectoryFor( File targetFile ) throws BuildException
659 {
660 File directory = new File( targetFile.getParent() );
661 if (!directory.exists())
662 {
663 if (!directory.mkdirs())
664 {
665 throw new BuildException("Unable to create directory: "
666 + directory.getAbsolutePath(), getLocation());
667 }
668 }
669 }
670
671 private static TemplateModel wrapMap(Map table)
672 {
673 SimpleHash model = new SimpleHash();
674 for (Iterator it = table.entrySet().iterator(); it.hasNext();)
675 {
676 Map.Entry entry = (Map.Entry)it.next();
677 model.put(String.valueOf(entry.getKey()), new SimpleScalar(String.valueOf(entry.getValue())));
678 }
679 return model;
680 }
681
682 protected void insertDefaults(Map root)
683 {
684 root.put("properties", propertiesTemplate);
685 root.put("userProperties", userPropertiesTemplate);
686 if (projectTemplate != null) {
687 root.put("project", projectTemplate);
688 root.put("project_node", projectNode);
689 }
690 if(modelsMap.size() > 0)
691 {
692 for (Iterator it = modelsMap.entrySet().iterator(); it.hasNext();)
693 {
694 Map.Entry entry = (Map.Entry) it.next();
695 root.put(entry.getKey(), entry.getValue());
696 }
697 }
698 }
699
700 }