/*
* Rapid Beans Framework, SDK, Ant Tasks: XXslt.java
*
* Copyright (C) 2009 Martin Bluemel
*
* Creation Date: 10/29/2005
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation;
* either version 3 of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
* You should have received a copies of the GNU Lesser General Public License and the
* GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
package org.rapidbeans.ant;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.XSLTProcess;
import org.apache.tools.ant.types.FileSet;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* This Ant task supports XSLT based code generation of classes with protected
* regions that allow them to be mixed with manually written parts.
*
* @author Martin Bluemel
*/
public final class XXslt extends XSLTProcess {
private enum PathMode {
absolute, // absolute
wd // relative to working directory
}
/**
* the path mode type.
*/
private PathMode pathmode = PathMode.wd;
/**
* setter for the path mode.
*
* @param argPathmode
* the new value for this property.
*/
public void setPathmode(final String argPathmode) {
this.pathmode = PathMode.valueOf(argPathmode);
}
/**
* the merge flag.
*/
private boolean merge = false;
/**
* setter for merge flag.
*
* @param argMerge
* the new value for this property.
*/
public void setMerge(final boolean argMerge) {
this.merge = argMerge;
}
/**
* the style file.
*/
private File style = null;
/**
* setter for style file.
*
* @param f
* the new value for this property.
*/
public void setStyle(final File f) {
super.setStyle(f.getAbsolutePath());
this.style = f;
}
/**
* the input file.
*/
private File in = null;
/**
* setter for input file.
*
* @param f
* the new value for this property.
*/
public void setIn(final File f) {
super.setIn(f);
this.in = f;
}
/**
* the output file.
*/
private File out = null;
/**
* setter for output file.
*
* @param f
* the new value for this property.
*/
public void setOut(final File f) {
super.setOut(f);
this.out = f;
}
/**
* marks a one line comment.
*/
private String oneLineComment = "//";
/**
* set the one line comment characters.
*
* @param s
* the one line comment characters
*/
public void setOneLineComment(final String s) {
this.oneLineComment = s;
}
/**
* begin comment.
*/
private String sectionBegin = "BEGIN manual code section";
/**
* set the string to mark the begin of a code section.
*
* @param s
* the string to mark the begin of a code section
*/
public void setSectionBegin(final String s) {
this.sectionBegin = s;
}
/**
* end comment.
*/
private String sectionEnd = "END manual code section";
/**
* set the string to mark the end of a code section.
*
* @param s
* the string to mark the end of a code section
*/
public void setSectionEnd(final String s) {
this.sectionEnd = s;
}
/**
* unmatched section begin marker.
*/
private String sectionUnmatchedBegin = ">>> BEGIN unmatched code section";
/**
* set the string to mark the begin of an unmatched code section.
*
* @param s
* the string to mark the begin of an unmatched code section. If
* you take some characters that guarantee the compilation to
* fail then you instantly are aware of unmatched sections.
*/
public void setSectionUnmatchedBegin(final String s) {
this.sectionUnmatchedBegin = s;
}
/**
* unmatched section end marker.
*/
private String sectionUnmatchedEnd = ">>> END unmatched code section";
/**
* set the string to mark the end of an unmatched code section.
*
* @param s
* the string to mark the end of an unmatched code section. If
* you take some characters that guarantee the compilation to
* fail then you instantly are aware of unmatched sections.
*/
public void setSectionUnmatchedEnd(final String s) {
this.sectionUnmatchedEnd = s;
}
/**
* model file set.
*/
private Vector<FileSet> filesets = new Vector<FileSet>();
/**
* Adds a set of files to be deleted.
*
* @param set
* the set of files to be deleted
*/
public void addFileset(final FileSet set) {
this.filesets.addElement(set);
}
/**
* the force flag.
*/
private boolean force;
/**
* setter for the force flag.
*
* @param argForce
* the force flag to set
*/
public void setForce(final boolean argForce) {
super.setForce(argForce);
this.force = argForce;
}
/**
* The execute method has to be implemented from every Ant task.
*/
public void execute() {
File out1 = null;
File out2 = null;
ArrayList<File> infiles = null;
final long lastModifiedOut = this.out.lastModified();
// work on files in the file sets
if (this.filesets.size() > 0) {
try {
this.in = File.createTempFile("xxsltIn", ".xml");
super.setIn(this.in);
infiles = evalFilesets();
long lastModifiedIn = -1L;
for (File file : infiles) {
final long lm = file.lastModified();
if (lm > lastModifiedIn) {
lastModifiedIn = lm;
}
}
if (!this.force && (lastModifiedIn <= lastModifiedOut && this.style.lastModified() <= lastModifiedOut)) {
this.log("nothing to do (multiple input)...", Project.MSG_INFO);
StringBuffer modelFiles = new StringBuffer();
boolean firstRun = true;
for (File file : infiles) {
if (!firstRun) {
modelFiles.append(", ");
}
modelFiles.append(file.getAbsolutePath());
firstRun = false;
}
this.log("neither one of the model files: " + modelFiles.toString() + " nor style sheet "
+ this.style.getAbsolutePath() + " is newer than output file" + this.out.getAbsolutePath()
+ ".", Project.MSG_VERBOSE);
return;
}
concatXmls(infiles, this.in);
} catch (IOException e) {
throw new BuildException(e);
}
} else {
if (!this.force
&& (this.in.lastModified() <= lastModifiedOut && this.style.lastModified() <= lastModifiedOut)) {
this.log("nothing to do (single input)...", Project.MSG_INFO);
this.log(
"neither model file " + this.in.getAbsolutePath() + " nor style sheet "
+ this.style.getAbsolutePath() + " is newer than output file"
+ this.out.getAbsolutePath() + ".", Project.MSG_VERBOSE);
return;
}
}
AntGateway antGateway = new AntGateway(this.getProject(), this.getTaskName());
try {
if (this.merge) {
out1 = new File(this.out.getAbsolutePath() + ".tmp1");
out2 = new File(this.out.getAbsolutePath() + ".tmp2");
super.setOut(out1);
this.log("calling sub task \"xslt\"...", Project.MSG_DEBUG);
this.log(" style = " + this.style.getAbsolutePath(), Project.MSG_DEBUG);
this.log(" in = " + this.in, Project.MSG_DEBUG);
this.log(" out = " + out1.getAbsolutePath(), Project.MSG_DEBUG);
initParams();
super.execute();
this.log("merging file " + this.out.getAbsolutePath() + " with result of XSL generation",
Project.MSG_INFO);
MergeProperties mergeProps = new MergeProperties(this.oneLineComment, this.sectionBegin,
this.sectionEnd, this.sectionUnmatchedBegin, this.sectionUnmatchedEnd);
antGateway.mergeSections(out1, this.out, out2, mergeProps);
this.log("calling sub task \"copy\"...", Project.MSG_VERBOSE);
antGateway.copy(out2, this.out);
} else {
initParams();
super.execute();
}
} catch (RuntimeException e) {
e.printStackTrace();
} finally {
if (out1 != null) {
out1.delete();
}
if (out2 != null) {
out2.delete();
}
if (infiles != null) {
this.in.delete();
}
}
}
/**
* set the params in out style merge.
*/
private void initParams() {
Param param = super.createParam();
param.setName("in");
param.setExpression(this.in.getAbsolutePath());
param = super.createParam();
param.setName("out");
param.setExpression(this.out.getAbsolutePath());
param = super.createParam();
param.setName("style");
param.setExpression(this.style.getAbsolutePath());
param = super.createParam();
param.setName("merge");
param.setExpression(new Boolean(this.merge).toString());
param = super.createParam();
param.setName("force");
param.setExpression(new Boolean(this.force).toString());
param = super.createParam();
param.setName("force");
param.setExpression(new Boolean(this.force).toString());
switch (this.pathmode) {
case wd:
param = super.createParam();
param.setName("root");
param.setExpression(System.getProperty("user.dir"));
break;
default:
break;
}
}
/**
* convert the task's filesets to a Collection of Files.
*
* @return an AraayList with the Files
*/
private ArrayList<File> evalFilesets() {
final ArrayList<File> infiles = new ArrayList<File>();
File file;
for (FileSet fs : this.filesets) {
try {
DirectoryScanner ds = fs.getDirectoryScanner(getProject());
String[] files = ds.getIncludedFiles();
// String[] dirs = ds.getIncludedDirectories();
for (String filename : files) {
file = new File(fs.getDir(this.getProject()), filename);
infiles.add(file);
}
} catch (BuildException be) {
// directory doesn't exist or is not readable
log(be.getMessage(), Project.MSG_WARN);
}
}
return infiles;
}
/**
* concatenates the given XML file collection to one single XML file.
*
* @param files
* the XML files to concatenate
* @param catfile
* the XML file with the concatenated content.
*/
private void concatXmls(final Collection<File> files, final File catfile) {
try {
final DocumentBuilderFactory dbfwr = DocumentBuilderFactory.newInstance();
final DocumentBuilder dbwr = dbfwr.newDocumentBuilder();
final XxsltXmlErrorHandler errorHandler = new XxsltXmlErrorHandler();
errorHandler.setFile(catfile);
dbwr.setErrorHandler(errorHandler);
// create an XML DOM document
// DOMImplementation impl = db.getDOMImplementation();
final Document catdoc = dbwr.newDocument();
// create and append the root node "packae" to the document
Element rootModelNode = catdoc.createElement("model");
catdoc.appendChild(rootModelNode);
for (final File file : files) {
final Element currentPackage = retrieveCurrentPackage(catdoc, rootModelNode, file);
this.log("parsing XML file: " + file.getAbsolutePath() + "...", Project.MSG_VERBOSE);
final Document doc = parseXmlModelFile(file);
currentPackage.appendChild(cloneNode(catdoc, doc.getDocumentElement()));
}
// write the XML DOM document to catfile
Transformer trans = TransformerFactory.newInstance().newTransformer();
Properties oformat = new Properties();
oformat.setProperty(OutputKeys.INDENT, "yes");
trans.setOutputProperties(oformat);
Source src = new DOMSource(catdoc);
Result dest = new StreamResult(catfile);
trans.transform(src, dest);
} catch (TransformerException e) {
throw new BuildException(e);
} catch (ParserConfigurationException e) {
throw new BuildException(e);
}
}
/**
* White space charcters.
*/
private static final char[] WSCHARS = { ' ', '\n', '\t' };
/**
* parse a single XMLModelFile.
*
* @param file
* the XML model file to parse
* @return the XML Document parsed
*/
private Document parseXmlModelFile(final File file) {
LineNumberReader lnr = null;
try {
lnr = new LineNumberReader(new InputStreamReader(new FileInputStream(file)));
final String firstLine = lnr.readLine();
boolean validate = false;
if ((firstLine != null)
&& StringHelper.strip(firstLine, WSCHARS, StringHelper.StripMode.leading).startsWith("<?xml")) {
final String secondLine = lnr.readLine();
if ((secondLine != null)
&& StringHelper.strip(secondLine, WSCHARS, StringHelper.StripMode.leading).startsWith(
"<!DOCTYPE")) {
validate = true;
}
}
final XxsltXmlErrorHandler errorHandler = new XxsltXmlErrorHandler();
errorHandler.setFile(file);
final DocumentBuilderFactory dbfrd = DocumentBuilderFactory.newInstance();
dbfrd.setNamespaceAware(true);
dbfrd.setValidating(validate);
final DocumentBuilder dbrd = dbfrd.newDocumentBuilder();
dbrd.setErrorHandler(errorHandler);
final Document doc = dbrd.parse(new FileInputStream(file));
return doc;
} catch (SAXException e) {
throw new BuildException(e);
} catch (IOException e) {
throw new BuildException(e);
} catch (ParserConfigurationException e) {
throw new BuildException(e);
} finally {
if (lnr != null) {
try {
lnr.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
/**
* copy the given node and it's subnodes into the given XML DOM document.
*
* @param doc
* the document
* @param node
* the node to copy
* @return the node in the document
*/
private Node cloneNode(final Document doc, final Node node) {
Node clone = null;
switch (node.getNodeType()) {
case Node.ELEMENT_NODE:
clone = doc.createElement(node.getNodeName());
Element el = (Element) clone;
NamedNodeMap attributes = node.getAttributes();
Node attrNode;
final int l1 = attributes.getLength();
for (int i = 0; i < l1; i++) {
attrNode = attributes.item(i);
el.setAttribute(attrNode.getNodeName(), attrNode.getNodeValue());
}
break;
case Node.ENTITY_NODE:
clone = doc.createEntityReference(node.getNodeName());
break;
case Node.TEXT_NODE:
clone = doc.createTextNode(node.getTextContent());
break;
default:
break;
}
final NodeList childNodes = node.getChildNodes();
final int l2 = childNodes.getLength();
Node childNode;
for (int i = 0; i < l2; i++) {
childNode = childNodes.item(i);
clone.appendChild(this.cloneNode(doc, childNode));
}
return clone;
}
/**
* retrieve the current package element in the XML DOM tree.
*
* @param doc
* the XML document tree
* @param rootNode
* the root node
* @param file
* the file
* @return the current package element in the XML DOM tree
*/
private Element retrieveCurrentPackage(final Document doc, final Element rootNode, final File file) {
Element currentPackage = rootNode;
String filePath = file.getPath();
final String currentDir = System.getProperty("user.dir");
if (filePath.startsWith(currentDir)) {
filePath = filePath.substring(currentDir.length());
}
final StringTokenizer st = new StringTokenizer(filePath, File.separator);
while (st.hasMoreTokens()) {
final String pkgname = st.nextToken();
if (!pkgname.equals(file.getName())) {
final NodeList subPackages = rootNode.getElementsByTagName("package");
final int len = subPackages.getLength();
Element newSubPackage = null;
for (int i = 0; i < len; i++) {
final Element subPackage = (Element) subPackages.item(i);
if (subPackage.getAttribute("name").equals(pkgname)) {
newSubPackage = subPackage;
break;
}
}
if (newSubPackage == null) {
newSubPackage = doc.createElement("package");
newSubPackage.setAttribute("name", pkgname);
currentPackage.appendChild(newSubPackage);
}
currentPackage = newSubPackage;
}
}
if (currentPackage == rootNode) {
currentPackage = null;
}
return currentPackage;
}
/**
* the XML error handler.
*
* @author Martin Bluemel
*/
class XxsltXmlErrorHandler implements ErrorHandler {
/**
* the default constructor.
*/
public XxsltXmlErrorHandler() {
}
/**
* the XML file currently parsed.
*/
private File file = null;
/**
* warning.
*
* @param e
* the exception
* @throws SAXException
* the exception
*/
public void warning(final SAXParseException e) throws SAXException {
throw new BuildException("XML Parser Warning in file \"" + this.file.getAbsolutePath() + "\", line "
+ e.getLineNumber() + ":\n" + e.getMessage(), e);
}
/**
* error.
*
* @param e
* the exception
* @throws SAXException
* the exception
*/
public void error(final SAXParseException e) throws SAXException {
throw new BuildException("XML Parser Error in file\n " + this.file.getAbsolutePath() + "\", line "
+ e.getLineNumber() + ":\n " + e.getMessage(), e);
}
/**
* fatal error.
*
* @param e
* the exception
* @throws SAXException
* the exception
*/
public void fatalError(final SAXParseException e) throws SAXException {
throw new BuildException("XML Parser Fatal Error in file \"" + this.file.getAbsolutePath() + "\", line "
+ e.getLineNumber() + ":\n" + e.getMessage(), e);
}
// /**
// * @return Returns the file.
// */
// public File getFile() {
// return this.file;
// }
/**
* @param argFile
* The file to set.
*/
public void setFile(final File argFile) {
this.file = argFile;
}
}
}