comparison flys-artifacts/src/main/java/org/dive4elements/river/artifacts/datacage/templating/Builder.java @ 5831:bd047b71ab37

Repaired internal references
author Sascha L. Teichmann <teichmann@intevation.de>
date Thu, 25 Apr 2013 12:06:39 +0200
parents flys-artifacts/src/main/java/de/intevation/flys/artifacts/datacage/templating/Builder.java@ebec12def170
children
comparison
equal deleted inserted replaced
5830:160f53ee0870 5831:bd047b71ab37
1 package org.dive4elements.river.artifacts.datacage.templating;
2
3 import org.dive4elements.artifacts.common.utils.XMLUtils;
4
5 import org.dive4elements.river.utils.Pair;
6
7 import java.sql.Connection;
8 import java.sql.SQLException;
9
10 import java.util.ArrayDeque;
11 import java.util.ArrayList;
12 import java.util.Deque;
13 import java.util.HashMap;
14 import java.util.List;
15 import java.util.Map;
16
17 import java.util.regex.Matcher;
18 import java.util.regex.Pattern;
19
20 import javax.xml.namespace.QName;
21
22 import javax.xml.xpath.XPath;
23 import javax.xml.xpath.XPathConstants;
24 import javax.xml.xpath.XPathExpression;
25 import javax.xml.xpath.XPathExpressionException;
26 import javax.xml.xpath.XPathFactory;
27
28 import org.apache.log4j.Logger;
29
30 import org.w3c.dom.Attr;
31 import org.w3c.dom.Document;
32 import org.w3c.dom.Element;
33 import org.w3c.dom.Node;
34 import org.w3c.dom.NodeList;
35 import org.w3c.dom.NamedNodeMap;
36
37
38 /** Handles and evaluate meta-data template against dbs. */
39 public class Builder
40 {
41 private static Logger log = Logger.getLogger(Builder.class);
42
43 public static final Pattern STRIP_LINE_INDENT =
44 Pattern.compile("\\s*\\r?\\n\\s*");
45
46 public static final Pattern BRACKET_XPATH =
47 Pattern.compile("\\{([^}]+)\\}");
48
49 public static final String DC_NAMESPACE_URI =
50 "http://www.intevation.org/2011/Datacage";
51
52 private static final Document EVAL_DOCUMENT =
53 XMLUtils.newDocument();
54
55 private static final XPathFactory XPATH_FACTORY =
56 XPathFactory.newInstance();
57
58 protected Document template;
59
60 protected Map<String, CompiledStatement> compiledStatements;
61
62 protected Map<String, Element> macros;
63
64 /** Connection to either of the databases. */
65 public static class NamedConnection {
66
67 protected String name;
68 protected Connection connection;
69 protected boolean cached;
70
71 public NamedConnection() {
72 }
73
74 public NamedConnection(
75 String name,
76 Connection connection
77 ) {
78 this(name, connection, true);
79 }
80
81 public NamedConnection(
82 String name,
83 Connection connection,
84 boolean cached
85 ) {
86 this.name = name;
87 this.connection = connection;
88 this.cached = cached;
89 }
90 } // class NamedConnection
91
92 public class BuildHelper
93 {
94 protected Node output;
95 protected Document owner;
96 protected StackFrames frames;
97 protected List<NamedConnection> connections;
98 protected Map<String, CompiledStatement.Instance> statements;
99 protected Deque<Pair<NamedConnection, ResultData>> connectionsStack;
100 protected Deque<NodeList> macroBodies;
101 protected FunctionResolver functionResolver;
102 protected Map<String, XPathExpression> expressions;
103
104
105 public BuildHelper(
106 Node output,
107 List<NamedConnection> connections,
108 Map<String, Object> parameters
109 ) {
110 if (connections.isEmpty()) {
111 throw new IllegalArgumentException("no connections given.");
112 }
113
114 this.connections = connections;
115 connectionsStack =
116 new ArrayDeque<Pair<NamedConnection, ResultData>>();
117 this.output = output;
118 frames = new StackFrames(parameters);
119 owner = getOwnerDocument(output);
120 macroBodies = new ArrayDeque<NodeList>();
121 functionResolver = new FunctionResolver(this);
122 expressions = new HashMap<String, XPathExpression>();
123 statements =
124 new HashMap<String, CompiledStatement.Instance>();
125 }
126
127 public void build() throws SQLException {
128 try {
129 // XXX: Thread safety is now established by the builder pool.
130 //synchronized (template) {
131 for (Node current: rootsToList()) {
132 build(output, current);
133 }
134 //}
135 }
136 finally {
137 closeStatements();
138 }
139 }
140
141 protected void closeStatements() {
142 for (CompiledStatement.Instance csi: statements.values()) {
143 csi.close();
144 }
145 statements.clear();
146 }
147
148 /**
149 * Return first statement node in NodeList, respecting
150 * macros but not doing evaluation (e.g. of <dc:if>s).
151 */
152 private Node findStatementNode(NodeList nodes) {
153 int S = nodes.getLength();
154
155 // Check direct children and take special care of macros.
156 for (int i = 0; i < S; ++i) {
157 Node node = nodes.item(i);
158 String ns;
159 // Regular statement node.
160 if (node.getNodeType() == Node.ELEMENT_NODE
161 && node.getLocalName().equals("statement")
162 && (ns = node.getNamespaceURI()) != null
163 && ns.equals(DC_NAMESPACE_URI)) {
164 return node;
165 }
166 // Macro node. Descend.
167 else if (node.getNodeType() == Node.ELEMENT_NODE
168 && node.getLocalName().equals("call-macro")
169 && (ns = node.getNamespaceURI()) != null
170 && ns.equals(DC_NAMESPACE_URI)) {
171
172 String macroName = ((Element)node).getAttribute("name");
173 Node inMacroNode =
174 findStatementNode(getMacroChildren(macroName));
175 if (inMacroNode != null) {
176 return inMacroNode;
177 }
178 }
179
180 }
181
182 return null;
183 }
184
185 /**
186 * Handle a dc:context node.
187 */
188 protected void context(Node parent, Element current)
189 throws SQLException
190 {
191 log.debug("dc:context");
192
193 NodeList subs = current.getChildNodes();
194 Node stmntNode = findStatementNode(subs);
195 int S = subs.getLength();
196
197 if (stmntNode == null) {
198 log.warn("dc:context: cannot find statement");
199 return;
200 }
201
202 String stmntText = stmntNode.getTextContent();
203
204 String con = current.getAttribute("connection");
205
206 String key = con + "-" + stmntText;
207
208 CompiledStatement.Instance csi = statements.get(key);
209
210 if (csi == null) {
211 CompiledStatement cs = compiledStatements.get(stmntText);
212 csi = cs.new Instance();
213 statements.put(key, csi);
214 }
215
216 NamedConnection connection = connectionsStack.isEmpty()
217 ? connections.get(0)
218 : connectionsStack.peek().getA();
219
220 if (con.length() > 0) {
221 for (NamedConnection nc: connections) {
222 if (con.equals(nc.name)) {
223 connection = nc;
224 break;
225 }
226 }
227 }
228
229 ResultData rd = csi.execute(
230 connection.connection,
231 frames,
232 connection.cached);
233
234 // only descent if there are results
235 if (!rd.isEmpty()) {
236 connectionsStack.push(
237 new Pair<NamedConnection, ResultData>(connection, rd));
238 try {
239 for (int i = 0; i < S; ++i) {
240 build(parent, subs.item(i));
241 }
242 }
243 finally {
244 connectionsStack.pop();
245 }
246 }
247 }
248
249 public boolean hasResult() {
250 return !connectionsStack.isEmpty()
251 && !connectionsStack.peek().getB().isEmpty();
252 }
253
254 protected ResultData createFilteredResultData(ResultData rd, String filter) {
255 if (filter == null) return rd;
256
257 List<Object []> rows = rd.getRows();
258 String [] columns = rd.getColumnLabels();
259
260 List<Object []> filtered = new ArrayList<Object[]>(rows.size());
261
262 for (Object [] row: rows) {
263 frames.enter();
264 try {
265 frames.put(columns, row);
266 boolean traverse = filter == null;
267
268 if (!traverse) {
269 Boolean b = evaluateXPathToBoolean(filter);
270 traverse = b != null && b;
271 }
272 if (traverse) {
273 filtered.add(row);
274 }
275 }
276 finally {
277 frames.leave();
278 }
279 }
280 return new ResultData(rd.getColumnLabels(), filtered);
281 }
282
283 protected void filter(Node parent, Element current)
284 throws SQLException
285 {
286 String expr = current.getAttribute("expr");
287
288 if ((expr = expr.trim()).length() == 0) {
289 expr = null;
290 }
291
292 NodeList subs = current.getChildNodes();
293 int S = subs.getLength();
294 if (S == 0) {
295 log.debug("dc:filter has no children");
296 return;
297 }
298
299 ResultData orig = null;
300 Pair<Builder.NamedConnection, ResultData> pair = null;
301
302 if (expr != null && !connectionsStack.isEmpty()) {
303 pair = connectionsStack.peek();
304 orig = pair.getB();
305 pair.setB(createFilteredResultData(orig, expr));
306 }
307
308 try {
309 for (int i = 0; i < S; ++i) {
310 build(parent, subs.item(i));
311 }
312 }
313 finally {
314 if (orig != null) {
315 pair.setB(orig);
316 }
317 }
318 }
319
320 /**
321 * Kind of foreach over results of a statement within a context.
322 */
323 protected void foreach(Node parent, Element current)
324 throws SQLException
325 {
326 log.debug("dc:for-each");
327
328 if (connectionsStack.isEmpty()) {
329 log.debug("dc:for-each without having results");
330 return;
331 }
332
333 NodeList subs = current.getChildNodes();
334 int S = subs.getLength();
335
336 if (S == 0) {
337 log.debug("dc:for-each has no children");
338 return;
339 }
340
341 Pair<Builder.NamedConnection, ResultData> pair =
342 connectionsStack.peek();
343
344 ResultData rd = pair.getB();
345
346 String [] columns = rd.getColumnLabels();
347
348 for (Object [] row: rd.getRows()) {
349 frames.enter();
350 try {
351 frames.put(columns, row);
352 for (int i = 0; i < S; ++i) {
353 build(parent, subs.item(i));
354 }
355 }
356 finally {
357 frames.leave();
358 }
359 }
360 }
361
362 /**
363 * Create element.
364 */
365 protected void element(Node parent, Element current)
366 throws SQLException
367 {
368 String attr = expand(current.getAttribute("name"));
369
370 if (log.isDebugEnabled()) {
371 log.debug("dc:element -> '" + attr + "'");
372 }
373
374 if (attr.length() == 0) {
375 log.warn("no name attribute found");
376 return;
377 }
378
379 Element element = owner.createElement(attr);
380
381 NodeList children = current.getChildNodes();
382 for (int i = 0, N = children.getLength(); i < N; ++i) {
383 build(element, children.item(i));
384 }
385
386 parent.appendChild(element);
387 }
388
389 protected void text(Node parent, Element current)
390 throws SQLException
391 {
392 log.debug("dc:text");
393 String value = expand(current.getTextContent());
394 parent.appendChild(owner.createTextNode(value));
395 }
396
397 /**
398 * Add attribute to an element
399 * @see Element
400 */
401 protected void attribute(Node parent, Element current) {
402
403 if (parent.getNodeType() != Node.ELEMENT_NODE) {
404 log.warn("need element here");
405 return;
406 }
407
408 String name = expand(current.getAttribute("name"));
409 String value = expand(current.getAttribute("value"));
410
411 Element element = (Element)parent;
412
413 element.setAttribute(name, value);
414 }
415
416 /**
417 * Call-Macro node.
418 * Evaluate child-nodes of the given macro element (not its text).
419 */
420 protected void callMacro(Node parent, Element current)
421 throws SQLException
422 {
423 String name = current.getAttribute("name");
424
425 if (name.length() == 0) {
426 log.warn("missing 'name' attribute in 'call-macro'");
427 return;
428 }
429
430 Element macro = macros.get(name);
431
432 if (macro != null) {
433 macroBodies.push(current.getChildNodes());
434 try {
435 NodeList subs = macro.getChildNodes();
436 for (int j = 0, M = subs.getLength(); j < M; ++j) {
437 build(parent, subs.item(j));
438 }
439 }
440 finally {
441 macroBodies.pop();
442 }
443 }
444 else {
445 log.warn("no macro '" + name + "' found.");
446 }
447 }
448
449 protected void macroBody(Node parent, Element current)
450 throws SQLException
451 {
452 if (!macroBodies.isEmpty()) {
453 NodeList children = macroBodies.peek();
454 for (int i = 0, N = children.getLength(); i < N; ++i) {
455 build(parent, children.item(i));
456 }
457 }
458 else {
459 log.warn("no current macro");
460 }
461 }
462
463 /** Get macro node children, not resolving bodies. */
464 protected NodeList getMacroChildren(String name) {
465 NodeList macros = template.getElementsByTagNameNS(
466 DC_NAMESPACE_URI, "macro");
467
468 Element macro = null;
469
470 for (int i = 0, N = macros.getLength(); i < N; ++i) {
471 Element m = (Element) macros.item(i);
472 if (name.equals(m.getAttribute("name"))) {
473 macro = m;
474 break;
475 }
476 }
477
478 if (macro != null) {
479 return macro.getChildNodes();
480 }
481 return null;
482 }
483
484 protected void ifClause(Node parent, Element current)
485 throws SQLException
486 {
487 String test = current.getAttribute("test");
488
489 if (test.length() == 0) {
490 log.warn("missing 'test' attribute in 'if'");
491 return;
492 }
493
494 Boolean result = evaluateXPathToBoolean(test);
495
496 if (result != null && result.booleanValue()) {
497 NodeList subs = current.getChildNodes();
498 for (int i = 0, N = subs.getLength(); i < N; ++i) {
499 build(parent, subs.item(i));
500 }
501 }
502 }
503
504 protected void choose(Node parent, Element current)
505 throws SQLException
506 {
507 Node branch = null;
508
509 NodeList children = current.getChildNodes();
510 for (int i = 0, N = children.getLength(); i < N; ++i) {
511 Node child = children.item(i);
512 String ns = child.getNamespaceURI();
513 if (ns == null
514 || !ns.equals(DC_NAMESPACE_URI)
515 || child.getNodeType() != Node.ELEMENT_NODE
516 ) {
517 continue;
518 }
519 String name = child.getLocalName();
520 if ("when".equals(name)) {
521 Element when = (Element)child;
522 String test = when.getAttribute("test");
523 if (test.length() == 0) {
524 log.warn("no 'test' attribute found for when");
525 continue;
526 }
527
528 Boolean result = evaluateXPathToBoolean(test);
529 if (result != null && result.booleanValue()) {
530 branch = child;
531 break;
532 }
533
534 continue;
535 }
536 else if ("otherwise".equals(name)) {
537 branch = child;
538 // No break here.
539 }
540 }
541
542 if (branch != null) {
543 NodeList subs = branch.getChildNodes();
544 for (int i = 0, N = subs.getLength(); i < N; ++i) {
545 build(parent, subs.item(i));
546 }
547 }
548 }
549
550 protected XPathExpression getXPathExpression(String expr)
551 throws XPathExpressionException
552 {
553 XPathExpression x = expressions.get(expr);
554 if (x == null) {
555 XPath xpath = XPATH_FACTORY.newXPath();
556 xpath.setXPathVariableResolver(frames);
557 xpath.setXPathFunctionResolver(functionResolver);
558 x = xpath.compile(expr);
559 expressions.put(expr, x);
560 }
561 return x;
562 }
563
564 protected Object evaluateXPath(String expr, QName returnType) {
565
566 if (log.isDebugEnabled()) {
567 log.debug("evaluate: '" + expr + "'");
568 }
569
570 try {
571 XPathExpression x = getXPathExpression(expr);
572 return x.evaluate(EVAL_DOCUMENT, returnType);
573 }
574 catch (XPathExpressionException xpee) {
575 log.error("expression: " + expr, xpee);
576 }
577 return null;
578 }
579
580 protected Boolean evaluateXPathToBoolean(String expr) {
581
582 Object result = evaluateXPath(expr, XPathConstants.BOOLEAN);
583
584 return result instanceof Boolean
585 ? (Boolean)result
586 : null;
587 }
588
589 protected void convert(Element current) {
590
591 String variable = expand(current.getAttribute("var"));
592 String type = expand(current.getAttribute("type"));
593
594 Object [] result = new Object[1];
595
596 if (frames.getStore(variable, result)) {
597 Object object = TypeConverter.convert(result[0], type);
598 frames.put(variable.toUpperCase(), object);
599 }
600 }
601
602
603 /** Put <dc:variable> content as variable on stackframes. */
604 protected void variable(Element current) {
605
606 String varName = expand(current.getAttribute("name"));
607 String expr = current.getAttribute("expr");
608 String type = current.getAttribute("type");
609
610 if (varName.length() == 0 || expr.length() == 0) {
611 log.error("dc:variable 'name' or 'expr' empty.");
612 }
613 else {
614 frames.put(
615 varName.toUpperCase(),
616 evaluateXPath(expr, typeToQName(type)));
617 }
618 }
619
620 protected String expand(String s) {
621 Matcher m = CompiledStatement.VAR.matcher(s);
622
623 Object [] result = new Object[1];
624
625 StringBuffer sb = new StringBuffer();
626 while (m.find()) {
627 String key = m.group(1);
628 result[0] = null;
629 if (frames.getStore(key, result)) {
630 m.appendReplacement(
631 sb, result[0] != null ? result[0].toString() : "");
632 }
633 else {
634 m.appendReplacement(sb, "\\${" + key + "}");
635 }
636 }
637 m.appendTail(sb);
638 return sb.toString();
639 }
640
641 protected void evaluateAttributeValue(Attr attr) {
642 String value = attr.getValue();
643 if (value.indexOf('{') >= 0) {
644 StringBuffer sb = new StringBuffer();
645 Matcher m = BRACKET_XPATH.matcher(value);
646 while (m.find()) {
647 String expr = m.group(1);
648 Object result = evaluateXPath(expr, XPathConstants.STRING);
649 if (result instanceof String) {
650 m.appendReplacement(sb, (String)result);
651 }
652 else {
653 m.appendReplacement(sb, "");
654 }
655 }
656 m.appendTail(sb);
657 attr.setValue(sb.toString());
658 }
659 }
660
661 protected void build(Node parent, Node current)
662 throws SQLException
663 {
664 String ns = current.getNamespaceURI();
665 if (ns != null && ns.equals(DC_NAMESPACE_URI)) {
666 if (current.getNodeType() != Node.ELEMENT_NODE) {
667 log.warn("need elements here");
668 }
669 else {
670 String localName = current.getLocalName();
671 Element curr = (Element)current;
672 if ("attribute".equals(localName)) {
673 attribute(parent, curr);
674 }
675 else if ("context".equals(localName)) {
676 context(parent, curr);
677 }
678 else if ("if".equals(localName)) {
679 ifClause(parent, curr);
680 }
681 else if ("choose".equals(localName)) {
682 choose(parent, curr);
683 }
684 else if ("call-macro".equals(localName)) {
685 callMacro(parent, curr);
686 }
687 else if ("macro-body".equals(localName)) {
688 macroBody(parent, curr);
689 }
690 else if ("macro".equals(localName)
691 || "comment".equals(localName)
692 || "statement".equals(localName)) {
693 // Simply ignore them.
694 }
695 else if ("element".equals(localName)) {
696 element(parent, curr);
697 }
698 else if ("for-each".equals(localName)) {
699 foreach(parent, curr);
700 }
701 else if ("filter".equals(localName)) {
702 filter(parent, curr);
703 }
704 else if ("text".equals(localName)) {
705 text(parent, curr);
706 }
707 else if ("variable".equals(localName)) {
708 variable(curr);
709 }
710 else if ("convert".equals(localName)) {
711 convert(curr);
712 }
713 else {
714 log.warn("unknown '" + localName + "' -> ignore");
715 }
716 }
717 return;
718 }
719
720 if (current.getNodeType() == Node.TEXT_NODE) {
721 String txt = current.getNodeValue();
722 if (txt != null && txt.trim().length() == 0) {
723 return;
724 }
725 }
726
727 if (current.getNodeType() == Node.COMMENT_NODE) {
728 // Ignore XML comments
729 return;
730 }
731
732 Node copy = owner.importNode(current, false);
733
734 NodeList children = current.getChildNodes();
735 for (int i = 0, N = children.getLength(); i < N; ++i) {
736 build(copy, children.item(i));
737 }
738 if (copy.getNodeType() == Node.ELEMENT_NODE) {
739 NamedNodeMap nnm = ((Element)copy).getAttributes();
740 for (int i = 0, N = nnm.getLength(); i < N; ++i) {
741 Node n = nnm.item(i);
742 if (n.getNodeType() == Node.ATTRIBUTE_NODE) {
743 evaluateAttributeValue((Attr)n);
744 }
745 }
746 }
747 parent.appendChild(copy);
748 }
749 } // class BuildHelper
750
751
752 public Builder() {
753 compiledStatements = new HashMap<String, CompiledStatement>();
754 macros = new HashMap<String, Element>();
755 }
756
757 public Builder(Document template) {
758 this();
759 this.template = template;
760 extractMacros();
761 compileStatements();
762 }
763
764 protected static QName typeToQName(String type) {
765 if ("number" .equals(type)) return XPathConstants.NUMBER;
766 if ("bool" .equals(type)) return XPathConstants.BOOLEAN;
767 if ("node" .equals(type)) return XPathConstants.NODE;
768 if ("nodeset".equals(type)) return XPathConstants.NODESET;
769 return XPathConstants.STRING;
770 }
771
772 /** Handle <dc:statement> elements. */
773 protected void compileStatements() {
774
775 NodeList nodes = template.getElementsByTagNameNS(
776 DC_NAMESPACE_URI, "statement");
777
778 for (int i = 0, N = nodes.getLength(); i < N; ++i) {
779 Element stmntElement = (Element)nodes.item(i);
780 String stmnt = trimStatement(stmntElement.getTextContent());
781 if (stmnt == null || stmnt.length() == 0) {
782 throw new IllegalArgumentException("found empty statement");
783 }
784 CompiledStatement cs = new CompiledStatement(stmnt);
785 // For faster lookup store a shortend string into the template.
786 stmnt = "s" + i;
787 stmntElement.setTextContent(stmnt);
788 compiledStatements.put(stmnt, cs);
789 }
790 }
791
792 protected void extractMacros() {
793 NodeList ms = template.getElementsByTagNameNS(
794 DC_NAMESPACE_URI, "macro");
795
796 for (int i = 0, N = ms.getLength(); i < N; ++i) {
797 Element m = (Element)ms.item(i);
798 macros.put(m.getAttribute("name"), m);
799 }
800 }
801
802 protected List<Node> rootsToList() {
803
804 NodeList roots = template.getElementsByTagNameNS(
805 DC_NAMESPACE_URI, "template");
806
807 List<Node> elements = new ArrayList<Node>();
808
809 for (int i = 0, N = roots.getLength(); i < N; ++i) {
810 NodeList rootChildren = roots.item(i).getChildNodes();
811 for (int j = 0, M = rootChildren.getLength(); j < M; ++j) {
812 Node child = rootChildren.item(j);
813 if (child.getNodeType() == Node.ELEMENT_NODE) {
814 elements.add(child);
815 }
816 }
817 }
818
819 return elements;
820 }
821
822 protected static final String trimStatement(String stmnt) {
823 if (stmnt == null) return null;
824 //XXX: Maybe a bit to radical for multiline strings?
825 return STRIP_LINE_INDENT.matcher(stmnt.trim()).replaceAll(" ");
826 }
827
828 protected static Document getOwnerDocument(Node node) {
829 Document document = node.getOwnerDocument();
830 return document != null ? document : (Document)node;
831 }
832
833 public void build(
834 List<NamedConnection> connections,
835 Node output,
836 Map<String, Object> parameters
837 )
838 throws SQLException
839 {
840 BuildHelper helper = new BuildHelper(output, connections, parameters);
841
842 helper.build();
843 }
844 }
845 // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org