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

Added 'type' attribute to <dc:variable/> element. If an optional 'type' attribute is given the result of the XPATH expression is interpreted as this type. Valid values are 'number', 'bool', 'node' and 'nodeset'. All other defaults to 'string' which also is the default if nor type is given.
author Sascha L. Teichmann <teichmann@intevation.de>
date Wed, 02 Jan 2013 15:31:53 +0100
parents 718adea968e2
children bf38ea4cb0f7
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.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;


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;

    /** 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;

        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);
            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();
        }

        /**
         * Handle a \<context\> node.
         */
        protected void context(Node parent, Element current)
        throws SQLException
        {
            log.debug("dc:context");

            NodeList subs = current.getChildNodes();
            int S = subs.getLength();

            // Check only direct children.
            Node stmntNode = null;
            for (int i = 0; i < S; ++i) {
                Node node = subs.item(i);
                String ns;
                if (node.getNodeType() == Node.ELEMENT_NODE
                && node.getLocalName().equals("statement")
                && (ns = node.getNamespaceURI()) != null
                && ns.equals(DC_NAMESPACE_URI)) {
                    stmntNode = node;
                    break;
                }
            }

            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();
                }
            }
        }

        /**
         * 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;
            }

            NodeList subs = current.getChildNodes();
            int S = subs.getLength();

            if (S == 0) {
                log.debug("dc:elements has no children");
                return;
            }

            ResultData rd = connectionsStack.peek().getB();

            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();
                }
            }
        }

        /**
         * 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);
        }

        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;
            }

            NodeList macros = template.getElementsByTagNameNS(
                DC_NAMESPACE_URI, "macro");

            for (int i = 0, N = macros.getLength(); i < N; ++i) {
                Element macro = (Element) macros.item(i);
                if (name.equals(macro.getAttribute("name"))) {
                    NodeList subs = macro.getChildNodes();
                    for (int j = 0, M = subs.getLength(); j < M; ++j) {
                        build(parent, subs.item(j));
                    }
                    return;
                }
            }

            log.warn("no macro '" + name + "' found.");
        }

        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 Object evaluateXPath(String expr, QName returnType) {

            if (log.isDebugEnabled()) {
                log.debug("evaluate: '" + expr + "'");
            }

            try {
                XPath xpath = XPATH_FACTORY.newXPath();
                xpath.setXPathVariableResolver(frames);
                xpath.setXPathFunctionResolver(FunctionResolver.FUNCTIONS);
                return xpath.evaluate(expr, 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);
            }
        }


        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".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>();
    }

    public Builder(Document template) {
        this();
        this.template = template;
        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 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