| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package freemarker.ext.ant; |
| |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStreamWriter; |
| import java.io.Writer; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.StringTokenizer; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| |
| import org.apache.tools.ant.BuildException; |
| import org.apache.tools.ant.DirectoryScanner; |
| import org.apache.tools.ant.Project; |
| import org.apache.tools.ant.taskdefs.MatchingTask; |
| import org.w3c.dom.Document; |
| import org.xml.sax.SAXParseException; |
| |
| import freemarker.ext.dom.NodeModel; |
| import freemarker.ext.xml.NodeListModel; |
| import freemarker.template.Configuration; |
| import freemarker.template.SimpleHash; |
| import freemarker.template.SimpleScalar; |
| import freemarker.template.Template; |
| import freemarker.template.TemplateModel; |
| import freemarker.template.TemplateNodeModel; |
| import freemarker.template._ObjectWrappers; |
| import freemarker.template.utility.ClassUtil; |
| import freemarker.template.utility.SecurityUtilities; |
| |
| |
| /** |
| * <p>This is an <a href="http://jakarta.apache.org/ant/" target="_top">Ant</a> task for transforming |
| * XML documents using FreeMarker templates. It uses the adapter class |
| * {@link NodeListModel}. It will read a set of XML documents, and pass them to |
| * the template for processing, building the corresponding output files in the |
| * destination directory.</p> |
| * <p>It makes the following variables available to the template in the data model:</p> |
| * <ul> |
| * <li>{@code document}: <em>Deprecated!</em> The DOM tree of the currently processed XML file wrapped |
| with the legacy {@link freemarker.ext.xml.NodeListModel}. |
| For new projects you should use the {@code .node} instead, which initially |
| contains the DOM Document wrapped with {@link freemarker.ext.dom.NodeModel}.</li> |
| * <li>{@code properties}: a {@link freemarker.template.SimpleHash} containing |
| * properties of the project that executes the task</li> |
| * <li>{@code userProperties}: a {@link freemarker.template.SimpleHash} containing |
| * user properties of the project that executes the task</li> |
| * <li>{@code project}: the DOM tree of the XML file specified by the |
| * {@code projectfile}. It will not be available if you didn't specify the |
| * {@code projectfile} attribute.</li> |
| * <li>further custom models can be instantiated and made available to the |
| * templates using the {@code models} attribute.</li> |
| * </ul> |
| * <p>It supports the following attributes:</p> |
| * <table style="width: auto; border-collapse: collapse" border="1"> |
| * <caption style="display: none">FreeMarker XML ant task attributes</caption> |
| * <tr> |
| * <th valign="top">Attribute</th> |
| * <th valign="top">Description</th> |
| * <th valign="top">Required</th> |
| * </tr> |
| * <tr> |
| * <td valign="top">basedir</td> |
| * <td valign="top">location of the XML files. Defaults to the project's |
| * basedir.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">destdir</td> |
| * <td valign="top">location to store the generated files.</td> |
| * <td valign="top">Yes</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">includes</td> |
| * <td valign="top">comma-separated list of patterns of files that must be |
| * included; all files are included when omitted.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">includesfile</td> |
| * <td valign="top">the name of a file that contains |
| * include patterns.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">excludes</td> |
| * <td valign="top">comma-separated list of patterns of files that must be |
| * excluded; no files (except default excludes) are excluded when omitted.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">excludesfile</td> |
| * <td valign="top">the name of a file that contains |
| * exclude patterns.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">defaultexcludes</td> |
| * <td valign="top">indicates whether default excludes should be used |
| * (<code>yes</code> | <code>no</code>); default excludes are used when omitted.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">extension</td> |
| * <td valign="top">extension of generated files. Defaults to .html.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">template</td> |
| * <td valign="top">name of the FreeMarker template file that will be |
| * applied by default to XML files</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">templateDir</td> |
| * <td valign="top">location of the FreeMarker template(s) to be used, defaults |
| * to the project's baseDir</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">projectfile</td> |
| * <td valign="top">path to the project file. The poject file must be an XML file. |
| * If omitted, it will not be available to templates </td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">incremental</td> |
| * <td valign="top">indicates whether all files should be regenerated (no), or |
| * only those that are older than the XML file, the template file, or the |
| * project file (yes). Defaults to yes. </td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">encoding</td> |
| * <td valign="top">The encoding of the output files. Defaults to platform |
| * default encoding.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">templateEncoding</td> |
| * <td valign="top">The encoding of the template files. Defaults to platform |
| * default encoding.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">validation</td> |
| * <td valign="top">Whether to validate the XML input. Defaults to off.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">models</td> |
| * <td valign="top">A list of [name=]className pairs separated by spaces, |
| * commas, or semicolons that specifies further models that should be |
| * available to templates. If name is omitted, the unqualified class name |
| * is used as the name. Every class that is specified must implement the |
| * TemplateModel interface and have a no-args constructor.</td> |
| * <td valign="top">No</td> |
| * </tr> |
| * </table> |
| * |
| * <p>It supports the following nesed elements:</p> |
| * |
| * <table style="width: auto; border-collapse: collapse" border="1"> |
| * <caption style="display: none">FreeMarker XML ant task nested elements</caption> |
| * <tr> |
| * <th valign="top">Element</th> |
| * <th valign="top">Description</th> |
| * <th valign="top">Required</th> |
| * </tr> |
| * <tr> |
| * <td valign="top">prepareModel</td> |
| * <td valign="top"> |
| * This element executes Jython script before the processing of each XML |
| * files, that you can use to modify the data model. |
| * You either enter the Jython script directly nested into this |
| * element, or specify a Jython script file with the {@code file} |
| * attribute. |
| * The following variables are added to the Jython runtime's local |
| * namespace before the script is invoked: |
| * <ul> |
| * <li>{@code model}: The data model as <code>java.util.HashMap</code>. |
| * You can read and modify the data model with this variable. |
| * <li>{@code doc}: The XML document as <code>org.w3c.dom.Document</code>. |
| * <li>{@code project}: The project document (if used) as |
| * <code>org.w3c.dom.Document</code>. |
| * </ul> |
| * <i>If this element is used, Jython classes (tried with Jython 2.1) |
| * must be available.</i> |
| * </td> |
| * <td valign="top">No</td> |
| * </tr> |
| * <tr> |
| * <td valign="top">prepareEnvironment</td> |
| * <td valign="top">This element executes Jython script before the processing |
| * of each XML files, that you can use to modify the freemarker environment |
| * ({@link freemarker.core.Environment}). The script is executed after the |
| * {@code prepareModel} element. The accessible Jython variables are the |
| * same as with the {@code prepareModel} element, except that there is no |
| * {@code model} variable, but there is {@code env} variable, which is |
| * the FreeMarker environment ({@link freemarker.core.Environment}). |
| * <i>If this element is used, Jython classes (tried with Jython 2.1) |
| * must be available.</i> |
| * </td> |
| * <td valign="top">No</td> |
| * </tr> |
| * </table> |
| * @deprecated <a href="http://fmpp.sourceforge.net">FMPP</a> is a more complete solution. |
| */ |
| @Deprecated |
| public class FreemarkerXmlTask |
| extends |
| MatchingTask { |
| private JythonAntTask prepareModel; |
| private JythonAntTask prepareEnvironment; |
| private final DocumentBuilderFactory builderFactory; |
| private DocumentBuilder builder; |
| |
| /** the {@link Configuration} used by this task. */ |
| private Configuration cfg = new Configuration(); |
| |
| /** the destination directory */ |
| private File destDir; |
| |
| /** the base directory */ |
| private File baseDir; |
| |
| //Where the templates live |
| |
| private File templateDir; |
| |
| /** the template= attribute */ |
| private String templateName; |
| |
| /** The template in its parsed form */ |
| private Template parsedTemplate; |
| |
| /** last modified of the template sheet */ |
| private long templateFileLastModified = 0; |
| |
| /** the projectFile= attribute */ |
| private String projectAttribute = null; |
| |
| private File projectFile = null; |
| |
| /** The DOM tree of the project wrapped into FreeMarker TemplateModel */ |
| private TemplateModel projectTemplate; |
| // The DOM tree wrapped using the freemarker.ext.dom wrapping. |
| private TemplateNodeModel projectNode; |
| private TemplateModel propertiesTemplate; |
| private TemplateModel userPropertiesTemplate; |
| |
| /** last modified of the project file if it exists */ |
| private long projectFileLastModified = 0; |
| |
| /** check the last modified date on files. defaults to true */ |
| private boolean incremental = true; |
| |
| /** the default output extension is .html */ |
| private String extension = ".html"; |
| |
| private String encoding = SecurityUtilities.getSystemProperty("file.encoding", "utf-8"); |
| private String templateEncoding = encoding; |
| private boolean validation = false; |
| |
| private String models = ""; |
| private final Map modelsMap = new HashMap(); |
| |
| |
| |
| /** |
| * Constructor creates the SAXBuilder. |
| */ |
| public FreemarkerXmlTask() { |
| builderFactory = DocumentBuilderFactory.newInstance(); |
| builderFactory.setNamespaceAware(true); |
| } |
| |
| /** |
| * Set the base directory. Defaults to {@code .} |
| */ |
| public void setBasedir(File dir) { |
| baseDir = dir; |
| } |
| |
| /** |
| * Set the destination directory into which the generated |
| * files should be copied to |
| * @param dir the name of the destination directory |
| */ |
| public void setDestdir(File dir) { |
| destDir = dir; |
| } |
| |
| /** |
| * Set the output file extension. {@code .html} by default. |
| */ |
| public void setExtension(String extension) { |
| this.extension = extension; |
| } |
| |
| public void setTemplate(String templateName) { |
| this.templateName = templateName; |
| } |
| |
| public void setTemplateDir(File templateDir) throws BuildException { |
| this.templateDir = templateDir; |
| try { |
| cfg.setDirectoryForTemplateLoading(templateDir); |
| } catch (Exception e) { |
| throw new BuildException(e); |
| } |
| } |
| |
| /** |
| * Set the path to the project XML file |
| */ |
| public void setProjectfile(String projectAttribute) { |
| this.projectAttribute = projectAttribute; |
| } |
| |
| /** |
| * Turn on/off incremental processing. On by default |
| */ |
| public void setIncremental(String incremental) { |
| this.incremental = !(incremental.equalsIgnoreCase("false") || incremental.equalsIgnoreCase("no") || incremental.equalsIgnoreCase("off")); |
| } |
| |
| /** |
| * Set encoding for generated files. Defaults to platform default encoding. |
| */ |
| public void setEncoding(String encoding) { |
| this.encoding = encoding; |
| } |
| |
| public void setTemplateEncoding(String inputEncoding) { |
| this.templateEncoding = inputEncoding; |
| } |
| |
| /** |
| * Sets whether to validate the XML input. |
| */ |
| public void setValidation(boolean validation) { |
| this.validation = validation; |
| } |
| |
| public void setModels(String models) { |
| this.models = models; |
| } |
| |
| @Override |
| public void execute() throws BuildException { |
| DirectoryScanner scanner; |
| String[] list; |
| |
| if (baseDir == null) { |
| baseDir = getProject().getBaseDir(); |
| } |
| if (destDir == null ) { |
| String msg = "destdir attribute must be set!"; |
| throw new BuildException(msg, getLocation()); |
| } |
| |
| File templateFile = null; |
| |
| if (templateDir == null) { |
| if (templateName != null) { |
| templateFile = new File(templateName); |
| if (!templateFile.isAbsolute()) { |
| templateFile = new File(getProject().getBaseDir(), templateName); |
| } |
| templateDir = templateFile.getParentFile(); |
| templateName = templateFile.getName(); |
| } else { |
| templateDir = baseDir; |
| } |
| setTemplateDir(templateDir); |
| } else if (templateName != null) { |
| if (new File(templateName).isAbsolute()) { |
| throw new BuildException("Do not specify an absolute location for the template as well as a templateDir"); |
| } |
| templateFile = new File(templateDir, templateName); |
| } |
| if (templateFile != null) { |
| templateFileLastModified = templateFile.lastModified(); |
| } |
| |
| try { |
| if (templateName != null) { |
| parsedTemplate = cfg.getTemplate(templateName, templateEncoding); |
| } |
| } catch (IOException ioe) { |
| throw new BuildException(ioe.toString()); |
| } |
| // get the last modification of the template |
| log("Transforming into: " + destDir.getAbsolutePath(), Project.MSG_INFO); |
| |
| // projectFile relative to baseDir |
| if (projectAttribute != null && projectAttribute.length() > 0) { |
| projectFile = new File(baseDir, projectAttribute); |
| if (projectFile.isFile()) |
| projectFileLastModified = projectFile.lastModified(); |
| else { |
| log ("Project file is defined, but could not be located: " + |
| projectFile.getAbsolutePath(), Project.MSG_INFO ); |
| projectFile = null; |
| } |
| } |
| |
| generateModels(); |
| |
| // find the files/directories |
| scanner = getDirectoryScanner(baseDir); |
| |
| propertiesTemplate = wrapMap(project.getProperties()); |
| userPropertiesTemplate = wrapMap(project.getUserProperties()); |
| |
| builderFactory.setValidating(validation); |
| try { |
| builder = builderFactory.newDocumentBuilder(); |
| } catch (ParserConfigurationException e) { |
| throw new BuildException("Could not create document builder", e, getLocation()); |
| } |
| |
| // get a list of files to work on |
| list = scanner.getIncludedFiles(); |
| |
| |
| for (int i = 0; i < list.length; ++i) { |
| process(baseDir, list[i], destDir); |
| } |
| } |
| |
| public void addConfiguredJython(JythonAntTask jythonAntTask) { |
| this.prepareEnvironment = jythonAntTask; |
| } |
| |
| public void addConfiguredPrepareModel(JythonAntTask prepareModel) { |
| this.prepareModel = prepareModel; |
| } |
| |
| public void addConfiguredPrepareEnvironment(JythonAntTask prepareEnvironment) { |
| this.prepareEnvironment = prepareEnvironment; |
| } |
| |
| /** |
| * Process an XML file using FreeMarker |
| */ |
| private void process(File baseDir, String xmlFile, File destDir) |
| throws BuildException { |
| File outFile = null; |
| File inFile = null; |
| try { |
| // the current input file relative to the baseDir |
| inFile = new File(baseDir,xmlFile); |
| // the output file relative to basedir |
| outFile = new File(destDir, |
| xmlFile.substring(0, |
| xmlFile.lastIndexOf('.')) + extension); |
| |
| // only process files that have changed |
| if (!incremental || |
| (inFile.lastModified() > outFile.lastModified() || |
| templateFileLastModified > outFile.lastModified() || |
| projectFileLastModified > outFile.lastModified())) { |
| ensureDirectoryFor(outFile); |
| |
| //-- command line status |
| log("Input: " + xmlFile, Project.MSG_INFO ); |
| |
| if (projectTemplate == null && projectFile != null) { |
| Document doc = builder.parse(projectFile); |
| projectTemplate = new NodeListModel(builder.parse(projectFile)); |
| projectNode = NodeModel.wrap(doc); |
| } |
| |
| // Build the file DOM |
| Document docNode = builder.parse(inFile); |
| |
| TemplateModel document = new NodeListModel(docNode); |
| TemplateNodeModel docNodeModel = NodeModel.wrap(docNode); |
| HashMap root = new HashMap(); |
| root.put("document", document); |
| insertDefaults(root); |
| |
| // Process the template and write out |
| // the result as the outFile. |
| try (Writer writer = new BufferedWriter( |
| new OutputStreamWriter(new FileOutputStream(outFile), encoding))) { |
| if (parsedTemplate == null) { |
| throw new BuildException("No template file specified in build script or in XML file"); |
| } |
| if (prepareModel != null) { |
| Map vars = new HashMap(); |
| vars.put("model", root); |
| vars.put("doc", docNode); |
| if (projectNode != null) { |
| vars.put("project", ((NodeModel) projectNode).getNode()); |
| } |
| prepareModel.execute(vars); |
| } |
| freemarker.core.Environment env = parsedTemplate.createProcessingEnvironment(root, writer); |
| env.setCurrentVisitorNode(docNodeModel); |
| if (prepareEnvironment != null) { |
| Map vars = new HashMap(); |
| vars.put("env", env); |
| vars.put("doc", docNode); |
| if (projectNode != null) { |
| vars.put("project", ((NodeModel) projectNode).getNode()); |
| } |
| prepareEnvironment.execute(vars); |
| } |
| env.process(); |
| writer.flush(); |
| } |
| |
| log("Output: " + outFile, Project.MSG_INFO ); |
| |
| } |
| } catch (SAXParseException spe) { |
| Throwable rootCause = spe; |
| if (spe.getException() != null) |
| rootCause = spe.getException(); |
| log("XML parsing error in " + inFile.getAbsolutePath(), Project.MSG_ERR); |
| log("Line number " + spe.getLineNumber()); |
| log("Column number " + spe.getColumnNumber()); |
| throw new BuildException(rootCause, getLocation()); |
| } catch (Throwable e) { |
| if (outFile != null ) { |
| if (!outFile.delete() && outFile.exists()) { |
| log("Failed to delete " + outFile, Project.MSG_WARN); |
| } |
| } |
| e.printStackTrace(); |
| throw new BuildException(e, getLocation()); |
| } |
| } |
| |
| private void generateModels() { |
| StringTokenizer modelTokenizer = new StringTokenizer(models, ",; "); |
| while (modelTokenizer.hasMoreTokens()) { |
| String modelSpec = modelTokenizer.nextToken(); |
| String name = null; |
| String clazz = null; |
| |
| int sep = modelSpec.indexOf('='); |
| if (sep == -1) { |
| // No explicit name - use unqualified class name |
| clazz = modelSpec; |
| int dot = clazz.lastIndexOf('.'); |
| if (dot == -1) { |
| // clazz in the default package |
| name = clazz; |
| } else { |
| name = clazz.substring(dot + 1); |
| } |
| } else { |
| name = modelSpec.substring(0, sep); |
| clazz = modelSpec.substring(sep + 1); |
| } |
| try { |
| modelsMap.put(name, ClassUtil.forName(clazz).newInstance()); |
| } catch (Exception e) { |
| throw new BuildException(e); |
| } |
| } |
| } |
| |
| /** |
| * create directories as needed |
| */ |
| private void ensureDirectoryFor( File targetFile ) throws BuildException { |
| File directory = new File( targetFile.getParent() ); |
| if (!directory.exists()) { |
| if (!directory.mkdirs()) { |
| throw new BuildException("Unable to create directory: " |
| + directory.getAbsolutePath(), getLocation()); |
| } |
| } |
| } |
| |
| private static TemplateModel wrapMap(Map table) { |
| SimpleHash model = new SimpleHash(_ObjectWrappers.SAFE_OBJECT_WRAPPER); |
| for (Iterator it = table.entrySet().iterator(); it.hasNext(); ) { |
| Map.Entry entry = (Map.Entry) it.next(); |
| model.put(String.valueOf(entry.getKey()), new SimpleScalar(String.valueOf(entry.getValue()))); |
| } |
| return model; |
| } |
| |
| protected void insertDefaults(Map root) { |
| root.put("properties", propertiesTemplate); |
| root.put("userProperties", userPropertiesTemplate); |
| if (projectTemplate != null) { |
| root.put("project", projectTemplate); |
| root.put("project_node", projectNode); |
| } |
| if (modelsMap.size() > 0) { |
| for (Iterator it = modelsMap.entrySet().iterator(); it.hasNext(); ) { |
| Map.Entry entry = (Map.Entry) it.next(); |
| root.put(entry.getKey(), entry.getValue()); |
| } |
| } |
| } |
| |
| } |