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