view flys-artifacts/src/main/java/de/intevation/flys/artifacts/datacage/templating/Builder.java @ 5509:627584bc0586

Datacage: Added <dc:filter> element. This allows cleaner way to narrow the datasets. Example: <dc:context> <dc:statement> SELECT DISTINCT name AS hws_name, official AS hws_official, kind_id AS hws_kind FROM hws_lines WHERE river_id = ${river_id} </dc:statement> <dc:if test="dc:has-result()"> <lines> <dc:macro name="hws-lines"> <dc:elements> <hws factory="hwsfactory" name="{$hws_name}"/> </dc:elements> </dc:macro> <dc:filter expr="$hws_official=1"> <dc:if test="dc:has-result()"> <official> <dc:filter expr="$hws_kind=1"> <dc:if test="dc:has-result()"> <Durchlass><dc:call-macro name="hws-lines"></Durchlass> </dc:if> </dc:filter> <dc:filter expr="$hws_kind=2"> <dc:if test="dc:has-result()"> <Damm><dc:call-macro name="hws-lines"></Damm> </dc:if> </dc:filter> <dc:filter expr="$hws_kind=3"> <dc:if test="dc:has-result()"> <Graben><dc:call-macro name="hws-lines"></Graben> </dc:if> </dc:filter> </official> </dc:if> </dc:filter> </lines> </dc:if> </dc:context>
author Sascha L. Teichmann <teichmann@intevation.de>
date Thu, 28 Mar 2013 16:51:15 +0100
parents 773899d00234
children 5800a9497b0b
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 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 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 {
                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 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 = pair.getB();
            ResultData orig = rd;

            if (filter != null) {
                ResultData rdCopy = createFilteredResultData(rd, filter);
                pair.setB(rdCopy);
                rd = rdCopy;
            }
            try {
                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();
                    }
                }
            }
            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 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, "{" + expr + "}");
                    }
                }
                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)curr;
                    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 ("elements".equals(localName)) {
                        elements(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;
    }

    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 :

http://dive4elements.wald.intevation.org