![JAR search and dependency download from the Maven repository](/logo.png)
net.pwall.xml.TemplateProcessor Maven / Gradle / Ivy
/*
* @(#) TemplateProcessor.java
*
* xtj XML Templating for Java
* Copyright (c) 2015, 2016, 2020 Peter Wall
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.pwall.xml;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import net.pwall.el.DoubleCoercionException;
import net.pwall.el.Expression;
import net.pwall.el.ExpressionException;
import net.pwall.el.Functions;
import net.pwall.el.IntCoercionException;
import net.pwall.el.Parser;
import net.pwall.html.HTMLFormatter;
import net.pwall.json.JSON;
import net.pwall.json.JSONException;
import net.pwall.util.Strings;
import net.pwall.util.UserError;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
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.w3c.dom.Text;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;
/**
* XML Template Processor for Java
*
* @author Peter Wall
*/
public class TemplateProcessor {
// TODO list: (* means completed)
//*1. Reconsider use of exceptions - make TemplateException a subclass of UserError?
//*2. Implement
// 3. Consider with contents - allocate element itself to a variable to allow access
// to elements contained within it? (Less important since 4 below completed)
//*4. In Expression - allow JavaScript object syntax?
// e.g.
// 5. Consider passing context object on each call - avoids potential error in unwinding
// 6. Update namespace version number to 1.0 (or greater?)
// 7. Consider a "for" attribute (or "while") to go with "if"
// 8. Block use of to modify an existing variable?
// 9. Is the best name for this functionality?
//10. Add logging
//*11. (access index within collection)
//*12. xt:if attribute must be applied to inner elements (case, param, ...)
// 13. xt:element - create element with dynamic name (xt:attribute, xt:content/xt:data)
// 14. xt:expect (with default=) in macros - declare parameters expected
// 15. Manage namespaces on xml output
// 16. Change name= to variable= ?
public static final String applicationName;
public static final String applicationVersion;
public static final String applicationHeading;
static {
Package pkg = TemplateProcessor.class.getPackage();
String implementationTitle = pkg.getImplementationTitle();
applicationName = implementationTitle != null ? implementationTitle : "xtj";
applicationVersion = pkg.getImplementationVersion();
applicationHeading = applicationVersion != null ? applicationName + " V" + applicationVersion : applicationName;
}
public static final String defaultNamespace = "http://pwall.net/xml/xt/1.0";
public static final String jstlFunctionsURL = "http://java.sun.com/jsp/jstl/functions";
private static final String templateElementName = "template";
private static final String macroElementName = "macro";
private static final String errorElementName = "error";
private static final String doctypeElementName = "doctype";
private static final String includeElementName = "include";
private static final String setElementName = "set";
private static final String ifElementName = "if";
private static final String switchElementName = "switch";
private static final String caseElementName = "case";
private static final String forElementName = "for";
private static final String callElementName = "call";
private static final String paramElementName = "param";
private static final String commentElementName = "comment";
private static final String copyElementName = "copy";
private static final String interceptElementName = "intercept";
private static final String whitespaceAttrName = "whitespace";
private static final String outputAttrName = "output";
private static final String outputXML = "xml";
private static final String outputHTML = "html";
private static final String prefixAttrName = "prefix";
private static final String textAttrName = "text";
private static final String systemAttrName = "system";
private static final String publicAttrName = "public";
private static final String nameAttrName = "name";
private static final String documentAttrName = "document";
private static final String valueAttrName = "value";
private static final String testAttrName = "test";
private static final String collectionAttrName = "collection";
private static final String fromAttrName = "from";
private static final String toAttrName = "to";
private static final String byAttrName = "by";
private static final String elementAttrName = "element";
private static final String optionAttrName = "option";
private static final String ifAttrName = "if";
private static final String hrefAttrName = "href";
private static final String indexAttrName = "index";
private static final String prefixTrue = "true";
private static final String prefixFalse = "false";
private static final String prefixYes = "yes";
private static final String prefixNo = "no";
private static final String whitespaceNone = "none";
private static final String whitespaceAll = "all";
private static final String whitespaceIndent = "indent";
private static final String optionInclude = "include";
private static final String versionSwitch = "-version";
private static final String nojstlSwitch = "-nojstl";
private static final String templateSwitch = "-template";
private static final String xmlSwitch = "-xml";
private static final String jsonSwitch = "-json";
private static final String propSwitch = "-prop";
private static final String outSwitch = "-out";
private static final String dSwitch = "-D";
private static final Map documentMap = new HashMap<>();
private Document dom;
private final Parser parser;
private TemplateContext context;
private String namespace;
private String whitespace;
private boolean prefixXML;
public TemplateProcessor() {
dom = null;
parser = Expression.getDefaultParser();
context = new TemplateContext(null, null);
namespace = defaultNamespace;
whitespace = null;
prefixXML = false;
}
public TemplateProcessor(Document dom, URL url) {
this();
setTemplate(dom, url);
}
public TemplateProcessor(URL url) {
this(getDocument(url), url);
}
public static TemplateProcessor from(File file) throws FileNotFoundException {
if (!file.exists())
throw new FileNotFoundException(file.getAbsolutePath());
try {
return new TemplateProcessor(new URL("file://" + file.getAbsoluteFile()));
}
catch (MalformedURLException e) {
throw new RuntimeException("Unexpected MalformedURLException", e);
}
}
public static TemplateProcessor from(String filename) throws FileNotFoundException {
File file = new File(filename);
return from(file);
}
public Document getDom() {
return dom;
}
public void setTemplate(Document dom, URL url) {
this.dom = Objects.requireNonNull(dom);
context = new TemplateContext(context, dom.getDocumentElement());
context.setURL(url);
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getWhitespace() {
return whitespace;
}
public void setWhitespace(String whitespace) throws TemplateException {
if (!(whitespaceNone.equalsIgnoreCase(whitespace) ||
whitespaceAll.equalsIgnoreCase(whitespace) ||
whitespaceIndent.equalsIgnoreCase(whitespace)))
throw new TemplateException("Illegal whitespace option - " + whitespace);
this.whitespace = whitespace;
}
public boolean isPrefixXML() {
return prefixXML;
}
public void setPrefixXML(boolean prefixXML) {
this.prefixXML = prefixXML;
}
public void setPrefixXML(String prefixXML) throws TemplateException {
if (!(prefixTrue.equalsIgnoreCase(prefixXML) || prefixFalse.equalsIgnoreCase(prefixXML) ||
prefixYes.equalsIgnoreCase(prefixXML) || prefixNo.equalsIgnoreCase(prefixXML)))
throw new TemplateException("Illegal prefix option - " + prefixXML);
setPrefixXML(prefixTrue.equalsIgnoreCase(prefixXML) || prefixYes.equalsIgnoreCase(prefixXML));
}
public void setVariable(String identifier, Object object) {
context.setVariable(identifier, object);
}
public void setVariables(Map map) {
for (Map.Entry entry : map.entrySet())
context.setVariable(entry.getKey(), entry.getValue());
}
public void addNamespace(String uri, Object impl) {
context.addNamespace(uri, impl);
}
public void process(OutputStream os) throws TemplateException {
if (context == null)
throw new IllegalStateException("No template specified");
Element documentElement = dom.getDocumentElement();
if (XML.matchNS(documentElement, templateElementName, namespace)) {
String whitespaceOption = substAttr(documentElement, whitespaceAttrName);
if (!isEmpty(whitespaceOption))
setWhitespace(whitespaceOption);
String outputAttr = substAttr(documentElement, outputAttrName);
if (!isEmpty(outputAttr)) {
if (outputAttr.equalsIgnoreCase(outputXML)) {
applyPrefixAttr(documentElement, documentElement.getAttribute(prefixAttrName));
processXML(os);
}
else if (outputAttr.equalsIgnoreCase(outputHTML))
processHTML(os);
else
throw new TemplateException(documentElement, outputAttrName,
"Illegal " + outputAttrName + ": " + outputAttr);
}
else
processXML(os);
}
else
processByOutputAttr(os, documentElement, documentElement.getAttributeNS(namespace, outputAttrName),
documentElement.getAttributeNS(namespace, prefixAttrName));
}
private void processByOutputAttr(OutputStream os, Element documentElement, String outputAttrValue,
String prefixAttrValue) throws TemplateException {
if (outputAttrValue != null) {
try {
String substValue = subst(outputAttrValue);
if (!isEmpty(substValue)) {
if (substValue.equalsIgnoreCase(outputXML)) {
applyPrefixAttr(documentElement, prefixAttrValue);
processXML(os);
}
else if (substValue.equalsIgnoreCase(outputHTML)) {
processHTML(os);
}
else
throw new TemplateException(documentElement, outputAttrName,
"Illegal " + outputAttrName + ": " + substValue);
}
}
catch (ExpressionException eee) {
throw new TemplateException(documentElement, outputAttrName,
"Error in expression substitution" + '\n' + eee.getMessage());
}
}
else
processXML(os);
}
private void applyPrefixAttr(Element documentElement, String prefixAttrValue) {
if (prefixAttrValue != null) {
try {
String substValue = subst(prefixAttrValue);
if (!isEmpty(substValue))
setPrefixXML(substValue);
}
catch (ExpressionException eee) {
throw new TemplateException(documentElement, prefixAttrName,
"Error in expression substitution" + '\n' + eee.getMessage());
}
}
}
public void processToSAX(SAXInterface saxHandler) {
if (context == null)
throw new IllegalStateException("No template specified");
try {
saxHandler.startDocument();
Element documentElement = dom.getDocumentElement();
if (XML.matchNS(documentElement, templateElementName, namespace))
processElementContents(documentElement, saxHandler, false);
else
processElement(documentElement, saxHandler);
saxHandler.endDocument();
}
catch (SAXException saxe) {
throw new RuntimeException("Unexpected SAX exception", saxe);
}
}
public Document processToDOM() {
SAX2DOMForXtj sax2dom = new SAX2DOMForXtj();
processToSAX(sax2dom);
return sax2dom.getDocument();
}
public void processXML(OutputStream os) throws TemplateException {
if (context == null)
throw new IllegalStateException("No template specified");
try (XMLFormatterForXTJ formatter = new XMLFormatterForXTJ(os)) {
if (whitespaceNone.equalsIgnoreCase(whitespace))
formatter.setWhitespace(XMLFormatter.Whitespace.NONE);
else if (whitespaceAll.equalsIgnoreCase(whitespace))
formatter.setWhitespace(XMLFormatter.Whitespace.ALL);
else if (whitespaceIndent.equalsIgnoreCase(whitespace))
formatter.setWhitespace(XMLFormatter.Whitespace.INDENT);
if (prefixXML)
formatter.prefix();
processToSAX(formatter);
}
catch (IOException ioe) {
throw new RuntimeException("Unexpected I/O exception", ioe);
}
catch (SAXException saxe) {
throw new RuntimeException("Unexpected SAX exception", saxe);
}
}
public void processHTML(OutputStream os) throws TemplateException {
if (context == null)
throw new IllegalStateException("No template specified");
try (HTMLFormatterForXTJ formatter = new HTMLFormatterForXTJ(os)) {
if (whitespaceNone.equalsIgnoreCase(whitespace))
formatter.setWhitespace(HTMLFormatter.Whitespace.NONE);
else if (whitespaceAll.equalsIgnoreCase(whitespace))
formatter.setWhitespace(HTMLFormatter.Whitespace.ALL);
else if (whitespaceIndent.equalsIgnoreCase(whitespace))
formatter.setWhitespace(HTMLFormatter.Whitespace.INDENT);
processToSAX(formatter);
}
catch (IOException ioe) {
throw new RuntimeException("Unexpected I/O exception", ioe);
}
}
private void processElement(Element element, SAXInterface formatter) throws TemplateException {
if (isIncluded(element)) {
if (XML.matchNS(element, errorElementName, namespace))
processError(element);
else if (XML.matchNS(element, doctypeElementName, namespace))
processDoctype(element, formatter);
else if (XML.matchNS(element, includeElementName, namespace))
processInclude(element, formatter);
else if (XML.matchNS(element, setElementName, namespace))
processSet(element, formatter);
else if (XML.matchNS(element, ifElementName, namespace))
processIf(element, formatter);
else if (XML.matchNS(element, switchElementName, namespace))
processSwitch(element, formatter);
else if (XML.matchNS(element, forElementName, namespace))
processFor(element, formatter);
else if (XML.matchNS(element, callElementName, namespace))
processCall(element, formatter);
else if (XML.matchNS(element, commentElementName, namespace))
processComment(element, formatter);
else if (XML.matchNS(element, copyElementName, namespace))
processCopy(element, formatter);
else
outputElement(element, formatter);
}
}
private boolean isIncluded(Element element) throws TemplateException {
// i.e. not excluded by xt:if=""
Attr ifAttr = element.getAttributeNodeNS(namespace, ifAttrName);
if (ifAttr != null) {
String test = ifAttr.getValue();
try {
String substTest = subst(test);
if (!isEmpty(substTest) && !parser.parseExpression(substTest, context).asBoolean())
return false;
}
catch (ExpressionException eee) {
throw new TemplateException(element, ifAttr.getName(),
"Error in \"if\" attribute - " + test + '\n' +eee.getMessage());
}
}
return true;
}
private void processElementContents(Element element, SAXInterface formatter, boolean trim)
throws TemplateException {
NodeList childNodes = element.getChildNodes();
int start = 0;
int end = childNodes.getLength();
if (trim) {
while (start < end && XML.isCommentOrEmpty(childNodes.item(start)))
start++;
while (start < end && XML.isCommentOrEmpty(childNodes.item(end - 1)))
end--;
}
for (int i = start, n = end; i < n; i++) {
Node childNode = childNodes.item(i);
if (childNode.getNodeType() == Node.ELEMENT_NODE &&
XML.matchNS((Element)childNode, macroElementName, namespace))
context.addMacro((Element)childNode);
}
for (int i = start, n = end; i < n; i++) {
Node childNode = childNodes.item(i);
if (childNode.getNodeType() == Node.ELEMENT_NODE) {
if (!XML.matchNS((Element)childNode, macroElementName, namespace))
processElement((Element)childNode, formatter);
}
else if (childNode.getNodeType() == Node.TEXT_NODE) {
Text text = (Text)childNode;
String data = text.getData();
if (trim) {
if (i == start)
data = XML.trimLeading(data);
if (i == n - 1)
data = XML.trimTrailing(data);
}
outputText(text, data, formatter);
}
}
}
private void processElementContentsNewContext(Element element, SAXInterface formatter, boolean trim)
throws TemplateException {
context = new TemplateContext(context, element);
processElementContents(element, formatter, trim);
context = context.getParent();
}
private void processError(Element element) throws TemplateException {
String text = substAttr(element, textAttrName);
throw new TemplateException(element, !isEmpty(text) ? text : "Error element");
}
private void processDoctype(Element element, LexicalHandler formatter) throws TemplateException {
String name = substAttr(element, nameAttrName);
if (isEmpty(name))
throw new TemplateException(element, "Name missing");
String systemAttr = substAttr(element, systemAttrName);
String publicAttr = substAttr(element, publicAttrName);
try {
formatter.startDTD(name, isEmpty(publicAttr) ? null : publicAttr, isEmpty(systemAttr) ? null : systemAttr);
// TODO implement doctype internal subset? Otherwise check element empty?
formatter.endDTD();
}
catch (SAXException saxe) {
throw new RuntimeException("Unexpected SAX exception", saxe);
}
}
private void processInclude(Element element, SAXInterface formatter) throws TemplateException {
String href = substAttr(element, hrefAttrName);
if (isEmpty(href))
throw new TemplateException(element, "HRef missing");
URL url = context.getURL();
URL includeURL;
Document included;
try {
includeURL = url == null ? new URL(href) : new URL(url, href);
included = getDocument(includeURL);
}
catch (Exception e) {
throw new TemplateException(element, "Error on include - " + href);
}
// TODO check element is empty - or allow elements for included code
Element documentElement = included.getDocumentElement();
context = new TemplateContext(context, documentElement);
context.setURL(includeURL);
if (XML.matchNS(documentElement, templateElementName, namespace)) {
// TODO process attributes on included template?
// TODO consider forcing specification of variables used in included template
processElementContents(documentElement, formatter, false);
}
else
processElement(documentElement, formatter);
context = context.getParent();
}
private void processSet(Element element, @SuppressWarnings("unused") SAXInterface formatter)
throws TemplateException {
String name = substAttr(element, nameAttrName);
if (!Expression.isValidIdentifier(name))
throw new TemplateException(element, "Name missing or invalid");
if (!isEmpty(element.getAttribute(documentAttrName)))
throw new TemplateException(element, "Can't handle ");
// TODO should be able to do this in Java
String value = substAttr(element, valueAttrName);
context.setVariable(name, evaluate(value, element, valueAttrName));
if (!XML.isElementEmpty(element))
throw new TemplateException(element, "Illegal content");
// TODO allow content - if it contains elements, store as an ElementWrapper;
// otherwise parse as JSON
}
private void processIf(Element element, SAXInterface formatter) throws TemplateException {
String test = substAttr(element, testAttrName);
if (isEmpty(test))
throw new TemplateException(element, "Test must be specified");
boolean testResult;
try {
testResult = parser.parseExpression(test, context).asBoolean();
}
catch (ExpressionException e) {
throw new TemplateException(element, testAttrName, "Error in test - " + test + '\n' + e.getMessage());
}
if (testResult)
processElementContentsNewContext(element, formatter, true);
}
private void processSwitch(Element element, SAXInterface formatter) throws TemplateException {
NodeList childNodes = element.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element childElement = (Element) node;
if (isIncluded(childElement)) {
if (XML.matchNS(childElement, caseElementName, namespace)) {
String test = substAttr(childElement, testAttrName);
boolean testResult = true;
if (!isEmpty(test)) {
try {
testResult = parser.parseExpression(test, context).asBoolean();
}
catch (ExpressionException e) {
throw new TemplateException(childElement, testAttrName,
"Error in test - " + test + '\n' + e.getMessage());
}
}
if (testResult) {
processElementContentsNewContext(childElement, formatter, true);
break;
// note - doesn't check switch contents following successful case
}
}
else
throw new TemplateException(childElement, "Illegal element within ");
}
}
else if (!XML.isCommentOrEmpty(node))
throw new TemplateException(element, "Illegal content within ");
}
}
private void processFor(Element element, SAXInterface formatter)
throws TemplateException {
// TODO document not yet handled
String name = substAttr(element, nameAttrName);
if (!isEmpty(name) && !Expression.isValidIdentifier(name))
throw new TemplateException(element, nameAttrName, "Illegal name in ");
String coll = substAttr(element, collectionAttrName);
String from = substAttr(element, fromAttrName);
String to = substAttr(element, toAttrName);
String by = substAttr(element, byAttrName);
String index = substAttr(element, indexAttrName);
if (!isEmpty(index) && !Expression.isValidIdentifier(index))
throw new TemplateException(element, indexAttrName, "Illegal index in ");
if (!isEmpty(coll)) {
if (!isEmpty(from) || !isEmpty(to) || !isEmpty(by))
throw new TemplateException(element, " has illegal combination of attributes");
processForCollection(element, formatter, name, coll, index);
}
else if (!isEmpty(from) || !isEmpty(to) || !isEmpty(by)) {
if (!isEmpty(index))
throw new TemplateException(element, " has illegal combination of attributes");
Object fromObject = isEmpty(from) ? null : evaluate(from, element, fromAttrName);
Object toObject = isEmpty(to) ? null : evaluate(to, element, toAttrName);
Object byObject = isEmpty(by) ? null : evaluate(by, element, byAttrName);
if (isFloating(fromObject) || isFloating(toObject) || isFloating(byObject))
processForSequenceFloat(element, formatter, name, fromObject, toObject, byObject);
else
processForSequenceInt(element, formatter, name, fromObject, toObject, byObject);
}
else
throw new TemplateException(element, " must specify iteration type");
}
private void processForSequenceInt(Element element, SAXInterface formatter, String name, Object from, Object to,
Object by) throws TemplateException {
// note - "to" value is exclusive; from="0" to="4" will perform 0,1,2,3
int fromValue = from == null ? 0 : intValue(from, element, fromAttrName, " from value invalid");
int toValue = to == null ? 0 : intValue(to, element, toAttrName, " to value invalid");
int byValue = by == null ? 1 : intValue(by, element, byAttrName, " by value invalid");
if (byValue <= 0)
throw new TemplateException(element, byAttrName, " by value invalid");
if (fromValue != toValue) {
context = new TemplateContext(context, element);
if (fromValue < toValue) {
do {
if (!isEmpty(name))
context.setVariable(name, fromValue);
processElementContents(element, formatter, true);
fromValue += byValue;
} while (fromValue < toValue);
}
else {
do {
if (!isEmpty(name))
context.setVariable(name, fromValue);
processElementContents(element, formatter, true);
fromValue -= byValue;
} while (fromValue > toValue);
}
context = context.getParent();
}
}
private int intValue(Object obj, Element elem, String attrName, String msg) throws TemplateException {
try {
return Expression.asInt(obj);
}
catch (IntCoercionException e) {
throw new TemplateException(elem, attrName, msg);
}
}
private void processForSequenceFloat(Element element, SAXInterface formatter, String name, Object from,
Object to, Object by) throws TemplateException {
// note - "to" value is exclusive; from="0" to="4" will perform 0,1,2,3
double fromValue = from == null ? 0.0 : doubleValue(from, element, fromAttrName, " from value invalid");
double toValue = to == null ? 0.0 : doubleValue(to, element, toAttrName, " to value invalid");
double byValue = by == null ? 1.0 : doubleValue(by, element, byAttrName, " by value invalid");
if (byValue <= 0.0)
throw new TemplateException(element, byAttrName, " by value invalid");
if (fromValue != toValue) {
context = new TemplateContext(context, element);
if (fromValue < toValue) {
do {
if (!isEmpty(name))
context.setVariable(name, fromValue);
processElementContents(element, formatter, true);
fromValue += byValue;
} while (fromValue < toValue);
}
else {
do {
if (!isEmpty(name))
context.setVariable(name, fromValue);
processElementContents(element, formatter, true);
fromValue -= byValue;
} while (fromValue > toValue);
}
context = context.getParent();
}
}
private double doubleValue(Object obj, Element elem, String attrName, String msg) throws TemplateException {
try {
return Expression.asDouble(obj);
}
catch (DoubleCoercionException e) {
throw new TemplateException(elem, attrName, msg);
}
}
private void processForCollection(Element element, SAXInterface formatter, String name, String coll,
String index) throws TemplateException {
Object collObject = evaluate(coll, element, collectionAttrName);
if (collObject != null) {
context = new TemplateContext(context, element);
if (collObject instanceof Map, ?>) {
int i = 0;
for (Object obj : ((Map, ?>)collObject).values()) { // should this be keys?
if (!isEmpty(name))
context.setVariable(name, obj);
if (!isEmpty(index))
context.setVariable(index, i);
processElementContents(element, formatter, true);
i++;
}
}
else if (collObject instanceof Iterable>) {
int i = 0;
for (Object obj : (Iterable>)collObject) {
if (!isEmpty(name))
context.setVariable(name, obj);
if (!isEmpty(index))
context.setVariable(index, i);
processElementContents(element, formatter, true);
i++;
}
}
else if (collObject instanceof Object[]) {
Object[] array = (Object[])collObject;
for (int i = 0, n = array.length; i < n; i++) {
Object obj = array[i];
if (!isEmpty(name))
context.setVariable(name, obj);
if (!isEmpty(index))
context.setVariable(index, i);
processElementContents(element, formatter, true);
}
}
else
throw new TemplateException(element, " collection must be capable of iteration");
context = context.getParent();
}
}
private void processCall(Element element, SAXInterface formatter) throws TemplateException {
String name = substAttr(element, nameAttrName);
Element macro = context.getMacro(name);
if (macro == null)
throw new TemplateException(element, "macro name incorrect - " + name);
context = new TemplateContext(context, element);
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node childNode = childNodes.item(i);
if (childNode.getNodeType() == Node.ELEMENT_NODE) {
Element childElement = (Element)childNode;
if (isIncluded(childElement)) {
if (XML.matchNS(childElement, paramElementName, namespace)) {
name = substAttr(childElement, nameAttrName);
if (!Expression.isValidIdentifier(name))
throw new TemplateException(childElement, "Name missing or invalid");
String value = substAttr(childElement, valueAttrName);
if (isEmpty(value))
throw new TemplateException(childElement, "Value missing");
try {
context.setVariable(name, // must be outer context
parser.parseExpression(value, context).evaluate());
}
catch (ExpressionException e) {
throw new TemplateException(childElement, valueAttrName,
"Error in value - " + value + '\n' + e.getMessage());
}
}
else
throw new TemplateException(childElement, "Illegal element within ");
}
}
else if (!XML.isCommentOrEmpty(childNode))
throw new TemplateException(element, "Illegal content within ");
}
processElementContents(macro, formatter, true);
context = context.getParent();
}
private void processComment(@SuppressWarnings("unused") Element element,
@SuppressWarnings("unused") SAXInterface formatter) {
// TODO complete this
}
private void processCopy(Element element, SAXInterface formatter) throws TemplateException {
String elementName = substAttr(element, elementAttrName);
if (isEmpty(elementName))
throw new TemplateException(element, " element missing");
// TODO if element not specified, process contents of (skipping s)??
context = new TemplateContext(context, element);
Object obj = evaluate(elementName, element, elementAttrName);
if (!(obj instanceof ElementWrapper))
throw new TemplateException(element, elementName, " must specify element");
Element elementToCopy = ((ElementWrapper)obj).getElement();
boolean include = false;
String opt = substAttr(element, optionAttrName);
if (!isEmpty(opt)) {
if (optionInclude.equals(opt))
include = true;
else
throw new TemplateException(element, optionAttrName, " option not recognised - " + opt);
}
List intercepts = new ArrayList<>();
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node childNode = childNodes.item(i);
if (childNode instanceof Element) {
Element childElement = (Element)childNode;
if (isIncluded(childElement)) {
context = new TemplateContext(context, element);
if (XML.matchNS(childElement, interceptElementName, namespace)) {
elementName = substAttr(childElement, elementAttrName);
if (isEmpty(elementName))
throw new TemplateException(element, " element missing");
String name = substAttr(childElement, nameAttrName);
if (!isEmpty(name) && !Expression.isValidIdentifier(name))
throw new TemplateException(childElement, nameAttrName, "Invalid name on ");
intercepts.add(new Intercept(elementName, childElement, name));
}
else
throw new TemplateException(element, "Illegal element within ");
context = context.getParent();
}
}
else if (!XML.isCommentOrEmpty(childNode))
throw new TemplateException(element, "Illegal content within ");
}
if (include)
copyElement(elementToCopy, intercepts, formatter);
else
copyElementContents(elementToCopy, intercepts, formatter);
context = context.getParent();
}
private void copyElement(Element element, List intercepts, SAXInterface formatter)
throws TemplateException {
for (Intercept intercept : intercepts) {
if (element.getTagName().equals(intercept.getTagName())) {
Element replacement = intercept.getReplacement();
context = new TemplateContext(context, replacement);
String name = intercept.getName();
if (!isEmpty(name))
context.setVariable(name, new ElementWrapper(element));
processElementContents(replacement, formatter, true);
context = context.getParent();
return;
}
}
AttributesImpl attrs = new AttributesImpl();
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attr = (Attr)attributes.item(i);
attrs.addAttribute(attr.getNamespaceURI(), attr.getLocalName(), attr.getNodeName(), "CDATA",
attr.getValue());
}
try {
formatter.startElement(element.getNamespaceURI(), element.getLocalName(), element.getNodeName(), attrs);
copyElementContents(element, intercepts, formatter);
formatter.endElement(element.getNamespaceURI(), element.getLocalName(), element.getNodeName());
}
catch (SAXException saxe) {
throw new RuntimeException("Unexpected SAX exception", saxe);
}
}
private void copyElementContents(Element element, List intercepts, SAXInterface formatter)
throws TemplateException {
context = new TemplateContext(context, element);
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node childNode = childNodes.item(i);
if (childNode instanceof Element)
copyElement((Element)childNode, intercepts, formatter);
else if (childNode instanceof CDATASection) {
try {
formatter.startCDATA();
outputData(((Text)childNode).getData(), formatter);
formatter.endCDATA();
}
catch (SAXException saxe) {
throw new RuntimeException("Unexpected SAX exception", saxe);
}
}
else if (childNode instanceof Text)
outputData(((Text)childNode).getData(), formatter);
}
context = context.getParent();
}
private void outputElement(Element element, SAXInterface formatter) throws TemplateException {
AttributesImpl attrs = new AttributesImpl();
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attr = (Attr)attributes.item(i);
if (!namespace.equals(attr.getNamespaceURI())) {
String value = attr.getValue();
try {
String substValue = subst(value);
if (!isEmpty(substValue))
attrs.addAttribute(attr.getNamespaceURI(), attr.getLocalName(), attr.getNodeName(), "CDATA",
substValue);
}
catch (ExpressionException eee) {
throw new TemplateException(element, attr.getName(), "Error in expression substitution - " + value);
}
}
}
try {
formatter.startElement(element.getNamespaceURI(), element.getLocalName(), element.getNodeName(), attrs);
processElementContents(element, formatter, false);
formatter.endElement(element.getNamespaceURI(), element.getLocalName(), element.getNodeName());
}
catch (SAXException saxe) {
throw new RuntimeException("Unexpected SAX exception", saxe);
}
}
private void outputText(Text text, String data, SAXInterface formatter) throws TemplateException {
try {
String substData = subst(data);
outputData(substData, formatter);
}
catch (ExpressionException eee) {
throw new TemplateException(text, "Error in expression substitution" + '\n' + eee.getMessage());
}
}
private void outputData(String data, SAXInterface formatter) {
try {
formatter.characters(data.toCharArray(), 0, data.length());
}
catch (SAXException saxe) {
throw new RuntimeException("Unexpected SAX exception", saxe);
}
}
private Object evaluate(String str, Element element, String attrName) throws TemplateException {
try {
return parser.parseExpression(str, context).evaluate();
}
catch (ExpressionException eee) {
throw new TemplateException(element, attrName, "Error in expression evaluation" + '\n' + eee.getMessage());
}
}
private String substAttr(Element element, String attrName) throws TemplateException {
try {
return subst(element.getAttribute(attrName));
}
catch (ExpressionException eee) {
throw new TemplateException(element, attrName,
"Error in expression substitution" + '\n' + eee.getMessage());
}
}
private String subst(String str) throws ExpressionException {
return str == null ? null : parser.substitute(str, context);
}
private static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
private static boolean isFloating(Object obj) {
return obj instanceof Double || obj instanceof Float;
}
public static void main(String[] args) {
try {
TemplateProcessor processor = new TemplateProcessor();
File currentDir = new File(".");
URL baseURL = new URL("file://" + currentDir.getAbsoluteFile());
File out = null;
boolean jstlFunctions = true;
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (arg.equals(versionSwitch)) {
System.err.println(applicationHeading);
if (args.length == 1)
System.exit(0);
}
else if (arg.equals(nojstlSwitch))
jstlFunctions = false;
else if (arg.equals(templateSwitch)) {
if (processor.getDom() != null)
throw new UserError("Duplicate " + templateSwitch);
URL template = getArgURL(args, ++i, baseURL, templateSwitch);
processor.setTemplate(getDocument(template), template);
}
else if (arg.equals(xmlSwitch)) {
String ident = getArgIdent(args, ++i, xmlSwitch);
URL xmlURL = getArgURL(args, ++i, baseURL, xmlSwitch);
try {
Element element = getDocument(xmlURL).getDocumentElement();
processor.setVariable(ident, new ElementWrapper(element));
}
catch (TemplateException te) {
throw new UserError(xmlSwitch + " content invalid - " + args[i]);
}
catch (Exception e) {
throw new UserError("Error reading xml - " + args[i]);
}
}
else if (arg.equals(jsonSwitch)) {
String ident = getArgIdent(args, ++i, jsonSwitch);
URL jsonURL = getArgURL(args, ++i, baseURL, jsonSwitch);
try {
processor.setVariable(ident, JSON.parse(getURLReader(jsonURL)));
}
catch (JSONException je) {
throw new UserError(jsonSwitch + " content invalid - " + args[i]);
}
catch (Exception e) {
throw new UserError("Error reading json - " + args[i]);
}
}
else if (arg.equals(propSwitch)) {
String ident = getArgIdent(args, ++i, propSwitch);
URL propURL = getArgURL(args, ++i, baseURL, propSwitch);
try {
Properties properties = new Properties();
properties.load(getURLReader(propURL));
processor.setVariable(ident, properties);
}
catch (Exception e) {
throw new UserError("Error reading properties - " + args[i]);
}
}
else if (arg.equals(outSwitch)) {
if (out != null)
throw new UserError("Duplicate " + outSwitch);
out = new File(getArg(args, ++i, outSwitch + " with no pathname"));
}
else if (arg.startsWith(dSwitch) && arg.length() > dSwitch.length()) {
int j = arg.indexOf('=');
String lhs = j < 0 ? arg.substring(dSwitch.length()) :
arg.substring(dSwitch.length(), j);
String rhs = j < 0 ? null : arg.substring(j + 1);
if (!Expression.isValidIdentifier(lhs))
throw new UserError(dSwitch + " identifier invalid - " + lhs);
processor.setVariable(lhs, rhs == null ? Boolean.TRUE : parseArg(rhs));
}
else
throw new UserError("Unrecognised argument - " + arg);
}
if (processor.getDom() == null)
throw new UserError("No " + templateSwitch);
if (jstlFunctions)
processor.addNamespace(jstlFunctionsURL, new Functions());
if (out != null) {
try (OutputStream os = new FileOutputStream(out)) {
processor.process(os);
}
catch (IOException ioe) {
throw new RuntimeException("Error writing output file", ioe);
}
}
else
processor.process(System.out);
}
catch (TemplateException te) {
System.err.println();
String xpath = te.getXPath();
if (xpath != null)
System.err.println("XPath: " + xpath);
System.err.println(te.getMessage());
}
catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
private static String getArg(String[] args, int index, String msg) {
if (index >= args.length)
throw new UserError(msg);
String result = args[index];
if (result.startsWith("-"))
throw new UserError(msg);
return result;
}
private static String getArgIdent(String[] args, int index, String name) {
String ident = getArg(args, index, name + " with no identifier");
if (!Expression.isValidIdentifier(ident))
throw new UserError(name + " identifier invalid - " + ident);
return ident;
}
private static URL getArgURL(String[] args, int index, URL baseURL, String name) {
String arg = getArg(args, index, name + " with no URL");
try {
return new URL(baseURL, arg);
}
catch (Exception e) {
throw new UserError(name + " URL invalid - " + arg);
}
}
private static Reader getURLReader(URL url) {
try {
if ("file".equals(url.getProtocol())) // workaround for Windows
return new FileReader(url.getPath());
URLConnection urlConnection = url.openConnection();
InputStream stream = urlConnection.getInputStream();
String contentTypeHeader = urlConnection.getContentType();
if (contentTypeHeader != null) {
String[] params = Strings.split(contentTypeHeader, ';');
for (int i = 1; i < params.length; i++) {
String param = params[i];
int j = param.indexOf('=');
if (j > 0) {
String lhs = param.substring(0, j).trim();
String rhs = param.substring(j + 1).trim();
if (lhs.equalsIgnoreCase("charset"))
return new InputStreamReader(stream, rhs);
}
}
}
return new InputStreamReader(stream);
}
catch (IOException e) {
throw new UserError("Error reading " + url);
}
}
private static Object parseArg(String arg) {
if (arg.equalsIgnoreCase("null"))
return null;
if (arg.equalsIgnoreCase("true"))
return Boolean.TRUE;
if (arg.equalsIgnoreCase("false"))
return Boolean.FALSE;
try {
return Integer.decode(arg);
}
catch (NumberFormatException e) {
// ignore
}
try {
return Long.decode(arg);
}
catch (NumberFormatException e) {
// ignore
}
try {
return Double.valueOf(arg);
}
catch (NumberFormatException e) {
// ignore
}
return arg;
}
private static synchronized Document getDocument(URL url) throws TemplateException {
String urlString = url.toString();
Document document = documentMap.get(urlString);
if (document == null) {
try {
InputSource is = new InputSource();
if ("file".equals(url.getProtocol())) // workaround for Windows
is.setByteStream(new FileInputStream(url.getPath()));
else
is.setSystemId(urlString);
document = XML.getDocumentBuilderNS().parse(is);
}
catch (IOException e) {
throw new TemplateException("I/O error reading URL - " + urlString);
}
catch (SAXException e) {
throw new TemplateException("Parsing error reading URL - " + urlString);
}
catch (Exception e) {
throw new TemplateException("Unexpected error reading URL - " + urlString);
}
documentMap.put(urlString, document);
}
return document;
}
public static class ElementWrapper {
private final Element element;
private List elems;
private Map attrs;
private String text;
public ElementWrapper(Element element) {
this.element = element;
elems = null;
attrs = null;
text = null;
}
public Element getElement() {
return element;
}
@SuppressWarnings("unused")
public String getTagName() {
return element.getTagName();
}
@SuppressWarnings("unused")
public List getElems() {
if (elems == null) {
elems = new ArrayList<>();
NodeList children = element.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
if (child instanceof Element)
elems.add(new ElementWrapper((Element)child));
}
}
return elems;
}
@SuppressWarnings("unused")
public Map getAttrs() {
if (attrs == null) {
attrs = new HashMap<>();
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attr = (Attr)attributes.item(i);
attrs.put(attr.getName(), attr.getValue());
}
}
return attrs;
}
@SuppressWarnings("unused")
public String getText() {
if (text == null) {
StringBuilder sb = new StringBuilder();
appendData(sb, element);
text = sb.toString();
}
return text;
}
private void appendData(StringBuilder sb, Node node) {
if (node instanceof Text)
sb.append(((Text)node).getData());
else if (node instanceof Element) {
NodeList children = node.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++)
appendData(sb, children.item(i));
}
}
}
public static class Intercept {
private final String tagName;
private final Element replacement;
private final String name;
public Intercept(String tagName, Element replacement, String name) {
this.tagName = tagName;
this.replacement = replacement;
this.name = name;
}
public String getTagName() {
return tagName;
}
public Element getReplacement() {
return replacement;
}
public String getName() {
return name;
}
}
public static class HTMLFormatterForXTJ extends HTMLFormatter implements SAXInterface {
public HTMLFormatterForXTJ(OutputStream os) {
super(os);
}
}
public static class XMLFormatterForXTJ extends XMLFormatter implements SAXInterface {
public XMLFormatterForXTJ(OutputStream os) {
super(os);
}
}
// public static class SAX2DOMForXtj extends SAX2DOM implements SAXInterface {
// public SAX2DOMForXtj() throws ParserConfigurationException {
// super(true);
// }
// }
public static class SAX2DOMForXtj implements SAXInterface {
private final Document document;
private final List nodeStack;
private boolean inCDATA;
public SAX2DOMForXtj() {
document = XML.newDocument();
nodeStack = new ArrayList<>();
nodeStack.add(document);
inCDATA = false;
}
public Document getDocument() {
return document;
}
private Node topNode() {
return nodeStack.get(nodeStack.size() - 1);
}
@Override
public void startDocument() {
}
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) {
Element element;
if (uri == null)
element = document.createElement(localName);
else
element = document.createElementNS(uri, qName);
topNode().appendChild(element);
nodeStack.add(element);
if (atts != null) {
for (int i = 0, n = atts.getLength(); i < n; i++) {
String attUri = atts.getURI(i);
if (attUri == null)
element.setAttribute(atts.getLocalName(i), atts.getValue(i));
else
element.setAttributeNS(attUri, atts.getQName(i), atts.getValue(i));
}
}
}
@Override
public void endElement(String uri, String localName, String qName) {
nodeStack.remove(nodeStack.size() - 1);
}
@Override
public void characters(char[] ch, int start, int length) {
if (topNode() instanceof Element) {
if (inCDATA)
topNode().appendChild(document.createCDATASection(new String(ch, start, length)));
else
topNode().appendChild(document.createTextNode(new String(ch, start, length)));
}
}
@Override
public void comment(char[] ch, int start, int length) {
if (topNode() instanceof Element)
topNode().appendChild(document.createComment(new String(ch, start, length)));
}
@Override
public void startCDATA() {
inCDATA = true;
}
@Override
public void endCDATA() {
inCDATA = false;
}
@Override
public void endDocument() {
}
@Override
public void startDTD(String name, String publicId, String systemId) {
}
@Override
public void endDTD() {
}
@Override
public void startEntity(String name) {
}
@Override
public void endEntity(String name) {
}
@Override
public void startPrefixMapping(String prefix, String uri) {
}
@Override
public void endPrefixMapping(String prefix) {
}
@Override
public void ignorableWhitespace(char[] ch, int start, int length) {
characters(ch, start, length);
}
@Override
public void processingInstruction(String target, String data) {
}
@Override
public void setDocumentLocator(Locator locator) {
}
@Override
public void skippedEntity(String name) {
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy