Mercurial > dive4elements > river
view flys-artifacts/src/main/java/de/intevation/flys/artifacts/datacage/templating/Builder.java @ 5449:504a62887217
Datacage: Compile XPath expressions and reuse them. With the introduction of filters in dc:elements XPath expressions are evalutated very often so compiling them should reduce the overhead significantly.
author | Sascha L. Teichmann <teichmann@intevation.de> |
---|---|
date | Wed, 27 Mar 2013 10:59:55 +0100 |
parents | 3d7e552cc396 |
children | 3b5e1535a459 |
line wrap: on
line source
package de.intevation.flys.artifacts.datacage.templating; import de.intevation.artifacts.common.utils.XMLUtils; import de.intevation.flys.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.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** Handles and evaluate meta-data template against dbs. */ public class Builder { private static Logger log = Logger.getLogger(Builder.class); public static final String CONNECTION_USER = "user"; public static final String CONNECTION_SYSTEM = "system"; public static final String DEFAULT_CONNECTION_NAME = CONNECTION_SYSTEM; public static final Pattern STRIP_LINE_INDENT = Pattern.compile("\\s*\\r?\\n\\s*"); 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 { 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 \<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); } /** * Kind of foreach over results of a statement within a context. */ protected void elements(Node parent, Element current) throws SQLException { log.debug("dc:elements"); if (connectionsStack.isEmpty()) { log.warn("dc:elements without having results"); return; } String filter = current.getAttribute("filter"); if ((filter = filter.trim()).length() == 0) { filter = null; } NodeList subs = current.getChildNodes(); int S = subs.getLength(); if (S == 0) { log.debug("dc:elements has no children"); return; } Pair<Builder.NamedConnection, ResultData> pair = connectionsStack.peek(); ResultData rd = connectionsStack.peek().getB(); ResultData orig = rd; if (filter != null) { ResultData rdCopy = createFilteredResultData(rd, filter); pair.setB(rdCopy); rd = rdCopy; } try { String [] columns = rd.getColumnLabels(); //if (log.isDebugEnabled()) { // log.debug("pushing vars: " // + java.util.Arrays.toString(columns)); //} for (Object [] row: rd.getRows()) { frames.enter(); try { frames.put(columns, row); //if (log.isDebugEnabled()) { // log.debug("current vars: " + frames.dump()); //} for (int i = 0; i < S; ++i) { build(parent, subs.item(i)); } } finally { frames.leave(); } } } finally { if (filter != null) { pair.setB(orig); } } } /** * 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 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(); if ("attribute".equals(localName)) { attribute(parent, (Element)current); } else if ("context".equals(localName)) { context(parent, (Element)current); } else if ("if".equals(localName)) { ifClause(parent, (Element)current); } else if ("choose".equals(localName)) { choose(parent, (Element)current); } else if ("call-macro".equals(localName)) { callMacro(parent, (Element)current); } else if ("macro-body".equals(localName)) { macroBody(parent, (Element)current); } else if ("macro".equals(localName)) { // Simply ignore the definition. } else if ("element".equals(localName)) { element(parent, (Element)current); } else if ("elements".equals(localName)) { elements(parent, (Element)current); } else if ("text".equals(localName)) { text(parent, (Element)current); } else if ("variable".equals(localName)) { variable((Element)current); } else if ("comment".equals(localName) || "statement".equals(localName)) { // ignore comments and statements in output } else if ("convert".equals(localName)) { convert((Element)current); } 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)); } 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; } private static final List<NamedConnection> wrap(Connection connection) { List<NamedConnection> list = new ArrayList<NamedConnection>(1); list.add(new NamedConnection(DEFAULT_CONNECTION_NAME, connection)); return list; } public void build( Connection connection, Node output, Map<String, Object> parameters ) throws SQLException { build(wrap(connection), output, parameters); } 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 :