view flys-artifacts/src/main/java/de/intevation/flys/artifacts/datacage/templating/Builder.java @ 5779:ebec12def170

Datacage: Add a pool of builders to make it multi threadable. XML DOM is not thread safe. Therefore the old implementation only allowed one thread to use the builder at a time. As the complexity of the configuration has increased over time this has become a bottleneck of the whole application because it took quiet some time to build a result. Furthermore the builder code path is visited very frequent. So many concurrent requests were piled up resulting in long waits for the users. To mitigate this problem a round robin pool of builders is used now. Each of the pooled builders has an independent copy of the XML template and can be run in parallel. The number of builders is determined by the system property 'flys.datacage.pool.size'. It defaults to 4.
author Sascha L. Teichmann <teichmann@intevation.de>
date Sun, 21 Apr 2013 12:48:09 +0200
parents 55529f586962
children
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.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 :

http://dive4elements.wald.intevation.org