Mercurial > dive4elements > river
diff flys-artifacts/src/main/java/org/dive4elements/river/artifacts/datacage/templating/Builder.java @ 5831:bd047b71ab37
Repaired internal references
author | Sascha L. Teichmann <teichmann@intevation.de> |
---|---|
date | Thu, 25 Apr 2013 12:06:39 +0200 |
parents | flys-artifacts/src/main/java/de/intevation/flys/artifacts/datacage/templating/Builder.java@ebec12def170 |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/flys-artifacts/src/main/java/org/dive4elements/river/artifacts/datacage/templating/Builder.java Thu Apr 25 12:06:39 2013 +0200 @@ -0,0 +1,845 @@ +package org.dive4elements.river.artifacts.datacage.templating; + +import org.dive4elements.artifacts.common.utils.XMLUtils; + +import org.dive4elements.river.utils.Pair; + +import java.sql.Connection; +import java.sql.SQLException; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.namespace.QName; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.apache.log4j.Logger; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.NamedNodeMap; + + +/** Handles and evaluate meta-data template against dbs. */ +public class Builder +{ + private static Logger log = Logger.getLogger(Builder.class); + + public static final Pattern STRIP_LINE_INDENT = + Pattern.compile("\\s*\\r?\\n\\s*"); + + public static final Pattern BRACKET_XPATH = + Pattern.compile("\\{([^}]+)\\}"); + + public static final String DC_NAMESPACE_URI = + "http://www.intevation.org/2011/Datacage"; + + private static final Document EVAL_DOCUMENT = + XMLUtils.newDocument(); + + private static final XPathFactory XPATH_FACTORY = + XPathFactory.newInstance(); + + protected Document template; + + protected Map<String, CompiledStatement> compiledStatements; + + protected Map<String, Element> macros; + + /** Connection to either of the databases. */ + public static class NamedConnection { + + protected String name; + protected Connection connection; + protected boolean cached; + + public NamedConnection() { + } + + public NamedConnection( + String name, + Connection connection + ) { + this(name, connection, true); + } + + public NamedConnection( + String name, + Connection connection, + boolean cached + ) { + this.name = name; + this.connection = connection; + this.cached = cached; + } + } // class NamedConnection + + public class BuildHelper + { + protected Node output; + protected Document owner; + protected StackFrames frames; + protected List<NamedConnection> connections; + protected Map<String, CompiledStatement.Instance> statements; + protected Deque<Pair<NamedConnection, ResultData>> connectionsStack; + protected Deque<NodeList> macroBodies; + protected FunctionResolver functionResolver; + protected Map<String, XPathExpression> expressions; + + + public BuildHelper( + Node output, + List<NamedConnection> connections, + Map<String, Object> parameters + ) { + if (connections.isEmpty()) { + throw new IllegalArgumentException("no connections given."); + } + + this.connections = connections; + connectionsStack = + new ArrayDeque<Pair<NamedConnection, ResultData>>(); + this.output = output; + frames = new StackFrames(parameters); + owner = getOwnerDocument(output); + macroBodies = new ArrayDeque<NodeList>(); + functionResolver = new FunctionResolver(this); + expressions = new HashMap<String, XPathExpression>(); + statements = + new HashMap<String, CompiledStatement.Instance>(); + } + + public void build() throws SQLException { + try { + // XXX: Thread safety is now established by the builder pool. + //synchronized (template) { + for (Node current: rootsToList()) { + build(output, current); + } + //} + } + finally { + closeStatements(); + } + } + + protected void closeStatements() { + for (CompiledStatement.Instance csi: statements.values()) { + csi.close(); + } + statements.clear(); + } + + /** + * Return first statement node in NodeList, respecting + * macros but not doing evaluation (e.g. of <dc:if>s). + */ + private Node findStatementNode(NodeList nodes) { + int S = nodes.getLength(); + + // Check direct children and take special care of macros. + for (int i = 0; i < S; ++i) { + Node node = nodes.item(i); + String ns; + // Regular statement node. + if (node.getNodeType() == Node.ELEMENT_NODE + && node.getLocalName().equals("statement") + && (ns = node.getNamespaceURI()) != null + && ns.equals(DC_NAMESPACE_URI)) { + return node; + } + // Macro node. Descend. + else if (node.getNodeType() == Node.ELEMENT_NODE + && node.getLocalName().equals("call-macro") + && (ns = node.getNamespaceURI()) != null + && ns.equals(DC_NAMESPACE_URI)) { + + String macroName = ((Element)node).getAttribute("name"); + Node inMacroNode = + findStatementNode(getMacroChildren(macroName)); + if (inMacroNode != null) { + return inMacroNode; + } + } + + } + + return null; + } + + /** + * Handle a dc:context node. + */ + protected void context(Node parent, Element current) + throws SQLException + { + log.debug("dc:context"); + + NodeList subs = current.getChildNodes(); + Node stmntNode = findStatementNode(subs); + int S = subs.getLength(); + + if (stmntNode == null) { + log.warn("dc:context: cannot find statement"); + return; + } + + String stmntText = stmntNode.getTextContent(); + + String con = current.getAttribute("connection"); + + String key = con + "-" + stmntText; + + CompiledStatement.Instance csi = statements.get(key); + + if (csi == null) { + CompiledStatement cs = compiledStatements.get(stmntText); + csi = cs.new Instance(); + statements.put(key, csi); + } + + NamedConnection connection = connectionsStack.isEmpty() + ? connections.get(0) + : connectionsStack.peek().getA(); + + if (con.length() > 0) { + for (NamedConnection nc: connections) { + if (con.equals(nc.name)) { + connection = nc; + break; + } + } + } + + ResultData rd = csi.execute( + connection.connection, + frames, + connection.cached); + + // only descent if there are results + if (!rd.isEmpty()) { + connectionsStack.push( + new Pair<NamedConnection, ResultData>(connection, rd)); + try { + for (int i = 0; i < S; ++i) { + build(parent, subs.item(i)); + } + } + finally { + connectionsStack.pop(); + } + } + } + + public boolean hasResult() { + return !connectionsStack.isEmpty() + && !connectionsStack.peek().getB().isEmpty(); + } + + protected ResultData createFilteredResultData(ResultData rd, String filter) { + if (filter == null) return rd; + + List<Object []> rows = rd.getRows(); + String [] columns = rd.getColumnLabels(); + + List<Object []> filtered = new ArrayList<Object[]>(rows.size()); + + for (Object [] row: rows) { + frames.enter(); + try { + frames.put(columns, row); + boolean traverse = filter == null; + + if (!traverse) { + Boolean b = evaluateXPathToBoolean(filter); + traverse = b != null && b; + } + if (traverse) { + filtered.add(row); + } + } + finally { + frames.leave(); + } + } + return new ResultData(rd.getColumnLabels(), filtered); + } + + protected void filter(Node parent, Element current) + throws SQLException + { + String expr = current.getAttribute("expr"); + + if ((expr = expr.trim()).length() == 0) { + expr = null; + } + + NodeList subs = current.getChildNodes(); + int S = subs.getLength(); + if (S == 0) { + log.debug("dc:filter has no children"); + return; + } + + ResultData orig = null; + Pair<Builder.NamedConnection, ResultData> pair = null; + + if (expr != null && !connectionsStack.isEmpty()) { + pair = connectionsStack.peek(); + orig = pair.getB(); + pair.setB(createFilteredResultData(orig, expr)); + } + + try { + for (int i = 0; i < S; ++i) { + build(parent, subs.item(i)); + } + } + finally { + if (orig != null) { + pair.setB(orig); + } + } + } + + /** + * Kind of foreach over results of a statement within a context. + */ + protected void foreach(Node parent, Element current) + throws SQLException + { + log.debug("dc:for-each"); + + if (connectionsStack.isEmpty()) { + log.debug("dc:for-each without having results"); + return; + } + + NodeList subs = current.getChildNodes(); + int S = subs.getLength(); + + if (S == 0) { + log.debug("dc:for-each has no children"); + return; + } + + Pair<Builder.NamedConnection, ResultData> pair = + connectionsStack.peek(); + + ResultData rd = pair.getB(); + + String [] columns = rd.getColumnLabels(); + + for (Object [] row: rd.getRows()) { + frames.enter(); + try { + frames.put(columns, row); + for (int i = 0; i < S; ++i) { + build(parent, subs.item(i)); + } + } + finally { + frames.leave(); + } + } + } + + /** + * Create element. + */ + protected void element(Node parent, Element current) + throws SQLException + { + String attr = expand(current.getAttribute("name")); + + if (log.isDebugEnabled()) { + log.debug("dc:element -> '" + attr + "'"); + } + + if (attr.length() == 0) { + log.warn("no name attribute found"); + return; + } + + Element element = owner.createElement(attr); + + NodeList children = current.getChildNodes(); + for (int i = 0, N = children.getLength(); i < N; ++i) { + build(element, children.item(i)); + } + + parent.appendChild(element); + } + + protected void text(Node parent, Element current) + throws SQLException + { + log.debug("dc:text"); + String value = expand(current.getTextContent()); + parent.appendChild(owner.createTextNode(value)); + } + + /** + * Add attribute to an element + * @see Element + */ + protected void attribute(Node parent, Element current) { + + if (parent.getNodeType() != Node.ELEMENT_NODE) { + log.warn("need element here"); + return; + } + + String name = expand(current.getAttribute("name")); + String value = expand(current.getAttribute("value")); + + Element element = (Element)parent; + + element.setAttribute(name, value); + } + + /** + * Call-Macro node. + * Evaluate child-nodes of the given macro element (not its text). + */ + protected void callMacro(Node parent, Element current) + throws SQLException + { + String name = current.getAttribute("name"); + + if (name.length() == 0) { + log.warn("missing 'name' attribute in 'call-macro'"); + return; + } + + Element macro = macros.get(name); + + if (macro != null) { + macroBodies.push(current.getChildNodes()); + try { + NodeList subs = macro.getChildNodes(); + for (int j = 0, M = subs.getLength(); j < M; ++j) { + build(parent, subs.item(j)); + } + } + finally { + macroBodies.pop(); + } + } + else { + log.warn("no macro '" + name + "' found."); + } + } + + protected void macroBody(Node parent, Element current) + throws SQLException + { + if (!macroBodies.isEmpty()) { + NodeList children = macroBodies.peek(); + for (int i = 0, N = children.getLength(); i < N; ++i) { + build(parent, children.item(i)); + } + } + else { + log.warn("no current macro"); + } + } + + /** Get macro node children, not resolving bodies. */ + protected NodeList getMacroChildren(String name) { + NodeList macros = template.getElementsByTagNameNS( + DC_NAMESPACE_URI, "macro"); + + Element macro = null; + + for (int i = 0, N = macros.getLength(); i < N; ++i) { + Element m = (Element) macros.item(i); + if (name.equals(m.getAttribute("name"))) { + macro = m; + break; + } + } + + if (macro != null) { + return macro.getChildNodes(); + } + return null; + } + + protected void ifClause(Node parent, Element current) + throws SQLException + { + String test = current.getAttribute("test"); + + if (test.length() == 0) { + log.warn("missing 'test' attribute in 'if'"); + return; + } + + Boolean result = evaluateXPathToBoolean(test); + + if (result != null && result.booleanValue()) { + NodeList subs = current.getChildNodes(); + for (int i = 0, N = subs.getLength(); i < N; ++i) { + build(parent, subs.item(i)); + } + } + } + + protected void choose(Node parent, Element current) + throws SQLException + { + Node branch = null; + + NodeList children = current.getChildNodes(); + for (int i = 0, N = children.getLength(); i < N; ++i) { + Node child = children.item(i); + String ns = child.getNamespaceURI(); + if (ns == null + || !ns.equals(DC_NAMESPACE_URI) + || child.getNodeType() != Node.ELEMENT_NODE + ) { + continue; + } + String name = child.getLocalName(); + if ("when".equals(name)) { + Element when = (Element)child; + String test = when.getAttribute("test"); + if (test.length() == 0) { + log.warn("no 'test' attribute found for when"); + continue; + } + + Boolean result = evaluateXPathToBoolean(test); + if (result != null && result.booleanValue()) { + branch = child; + break; + } + + continue; + } + else if ("otherwise".equals(name)) { + branch = child; + // No break here. + } + } + + if (branch != null) { + NodeList subs = branch.getChildNodes(); + for (int i = 0, N = subs.getLength(); i < N; ++i) { + build(parent, subs.item(i)); + } + } + } + + protected XPathExpression getXPathExpression(String expr) + throws XPathExpressionException + { + XPathExpression x = expressions.get(expr); + if (x == null) { + XPath xpath = XPATH_FACTORY.newXPath(); + xpath.setXPathVariableResolver(frames); + xpath.setXPathFunctionResolver(functionResolver); + x = xpath.compile(expr); + expressions.put(expr, x); + } + return x; + } + + protected Object evaluateXPath(String expr, QName returnType) { + + if (log.isDebugEnabled()) { + log.debug("evaluate: '" + expr + "'"); + } + + try { + XPathExpression x = getXPathExpression(expr); + return x.evaluate(EVAL_DOCUMENT, returnType); + } + catch (XPathExpressionException xpee) { + log.error("expression: " + expr, xpee); + } + return null; + } + + protected Boolean evaluateXPathToBoolean(String expr) { + + Object result = evaluateXPath(expr, XPathConstants.BOOLEAN); + + return result instanceof Boolean + ? (Boolean)result + : null; + } + + protected void convert(Element current) { + + String variable = expand(current.getAttribute("var")); + String type = expand(current.getAttribute("type")); + + Object [] result = new Object[1]; + + if (frames.getStore(variable, result)) { + Object object = TypeConverter.convert(result[0], type); + frames.put(variable.toUpperCase(), object); + } + } + + + /** Put <dc:variable> content as variable on stackframes. */ + protected void variable(Element current) { + + String varName = expand(current.getAttribute("name")); + String expr = current.getAttribute("expr"); + String type = current.getAttribute("type"); + + if (varName.length() == 0 || expr.length() == 0) { + log.error("dc:variable 'name' or 'expr' empty."); + } + else { + frames.put( + varName.toUpperCase(), + evaluateXPath(expr, typeToQName(type))); + } + } + + protected String expand(String s) { + Matcher m = CompiledStatement.VAR.matcher(s); + + Object [] result = new Object[1]; + + StringBuffer sb = new StringBuffer(); + while (m.find()) { + String key = m.group(1); + result[0] = null; + if (frames.getStore(key, result)) { + m.appendReplacement( + sb, result[0] != null ? result[0].toString() : ""); + } + else { + m.appendReplacement(sb, "\\${" + key + "}"); + } + } + m.appendTail(sb); + return sb.toString(); + } + + protected void evaluateAttributeValue(Attr attr) { + String value = attr.getValue(); + if (value.indexOf('{') >= 0) { + StringBuffer sb = new StringBuffer(); + Matcher m = BRACKET_XPATH.matcher(value); + while (m.find()) { + String expr = m.group(1); + Object result = evaluateXPath(expr, XPathConstants.STRING); + if (result instanceof String) { + m.appendReplacement(sb, (String)result); + } + else { + m.appendReplacement(sb, ""); + } + } + m.appendTail(sb); + attr.setValue(sb.toString()); + } + } + + protected void build(Node parent, Node current) + throws SQLException + { + String ns = current.getNamespaceURI(); + if (ns != null && ns.equals(DC_NAMESPACE_URI)) { + if (current.getNodeType() != Node.ELEMENT_NODE) { + log.warn("need elements here"); + } + else { + String localName = current.getLocalName(); + Element curr = (Element)current; + if ("attribute".equals(localName)) { + attribute(parent, curr); + } + else if ("context".equals(localName)) { + context(parent, curr); + } + else if ("if".equals(localName)) { + ifClause(parent, curr); + } + else if ("choose".equals(localName)) { + choose(parent, curr); + } + else if ("call-macro".equals(localName)) { + callMacro(parent, curr); + } + else if ("macro-body".equals(localName)) { + macroBody(parent, curr); + } + else if ("macro".equals(localName) + || "comment".equals(localName) + || "statement".equals(localName)) { + // Simply ignore them. + } + else if ("element".equals(localName)) { + element(parent, curr); + } + else if ("for-each".equals(localName)) { + foreach(parent, curr); + } + else if ("filter".equals(localName)) { + filter(parent, curr); + } + else if ("text".equals(localName)) { + text(parent, curr); + } + else if ("variable".equals(localName)) { + variable(curr); + } + else if ("convert".equals(localName)) { + convert(curr); + } + else { + log.warn("unknown '" + localName + "' -> ignore"); + } + } + return; + } + + if (current.getNodeType() == Node.TEXT_NODE) { + String txt = current.getNodeValue(); + if (txt != null && txt.trim().length() == 0) { + return; + } + } + + if (current.getNodeType() == Node.COMMENT_NODE) { + // Ignore XML comments + return; + } + + Node copy = owner.importNode(current, false); + + NodeList children = current.getChildNodes(); + for (int i = 0, N = children.getLength(); i < N; ++i) { + build(copy, children.item(i)); + } + if (copy.getNodeType() == Node.ELEMENT_NODE) { + NamedNodeMap nnm = ((Element)copy).getAttributes(); + for (int i = 0, N = nnm.getLength(); i < N; ++i) { + Node n = nnm.item(i); + if (n.getNodeType() == Node.ATTRIBUTE_NODE) { + evaluateAttributeValue((Attr)n); + } + } + } + parent.appendChild(copy); + } + } // class BuildHelper + + + public Builder() { + compiledStatements = new HashMap<String, CompiledStatement>(); + macros = new HashMap<String, Element>(); + } + + public Builder(Document template) { + this(); + this.template = template; + extractMacros(); + compileStatements(); + } + + protected static QName typeToQName(String type) { + if ("number" .equals(type)) return XPathConstants.NUMBER; + if ("bool" .equals(type)) return XPathConstants.BOOLEAN; + if ("node" .equals(type)) return XPathConstants.NODE; + if ("nodeset".equals(type)) return XPathConstants.NODESET; + return XPathConstants.STRING; + } + + /** Handle <dc:statement> elements. */ + protected void compileStatements() { + + NodeList nodes = template.getElementsByTagNameNS( + DC_NAMESPACE_URI, "statement"); + + for (int i = 0, N = nodes.getLength(); i < N; ++i) { + Element stmntElement = (Element)nodes.item(i); + String stmnt = trimStatement(stmntElement.getTextContent()); + if (stmnt == null || stmnt.length() == 0) { + throw new IllegalArgumentException("found empty statement"); + } + CompiledStatement cs = new CompiledStatement(stmnt); + // For faster lookup store a shortend string into the template. + stmnt = "s" + i; + stmntElement.setTextContent(stmnt); + compiledStatements.put(stmnt, cs); + } + } + + protected void extractMacros() { + NodeList ms = template.getElementsByTagNameNS( + DC_NAMESPACE_URI, "macro"); + + for (int i = 0, N = ms.getLength(); i < N; ++i) { + Element m = (Element)ms.item(i); + macros.put(m.getAttribute("name"), m); + } + } + + protected List<Node> rootsToList() { + + NodeList roots = template.getElementsByTagNameNS( + DC_NAMESPACE_URI, "template"); + + List<Node> elements = new ArrayList<Node>(); + + for (int i = 0, N = roots.getLength(); i < N; ++i) { + NodeList rootChildren = roots.item(i).getChildNodes(); + for (int j = 0, M = rootChildren.getLength(); j < M; ++j) { + Node child = rootChildren.item(j); + if (child.getNodeType() == Node.ELEMENT_NODE) { + elements.add(child); + } + } + } + + return elements; + } + + protected static final String trimStatement(String stmnt) { + if (stmnt == null) return null; + //XXX: Maybe a bit to radical for multiline strings? + return STRIP_LINE_INDENT.matcher(stmnt.trim()).replaceAll(" "); + } + + protected static Document getOwnerDocument(Node node) { + Document document = node.getOwnerDocument(); + return document != null ? document : (Document)node; + } + + public void build( + List<NamedConnection> connections, + Node output, + Map<String, Object> parameters + ) + throws SQLException + { + BuildHelper helper = new BuildHelper(output, connections, parameters); + + helper.build(); + } +} +// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :