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 }