eval expressions in normal html attributes
[org.ibex.xt-crawshaw.git] / src / java / org / ibex / xt / Template.java
1 package org.ibex.xt;
2
3 import java.io.BufferedReader;
4 import java.io.FileInputStream;
5 import java.io.InputStreamReader;
6 import java.io.Reader;
7 import java.io.StringReader;
8 import java.io.StringWriter;
9 import java.io.Writer;
10 import java.io.IOException;
11 import java.io.FileNotFoundException;
12
13 import java.util.*;
14 import org.ibex.util.*;
15 import org.ibex.js.*;
16
17 public class Template extends JSLeaf.Element {
18     public static Template parse(String path, Template.Scope s) throws FileNotFoundException, IOException {
19         Reader xmlreader = new BufferedReader(new InputStreamReader(new FileInputStream(path)));
20         XML.Document doc = new XML.Document(); // FIXME: switch to Tree.Stream
21         doc.parse(xmlreader);
22         return new Template(doc.getRoot(), s);
23     }
24
25     public static Tree.Leaf wrap(Tree.Leaf leaf, Template.Scope s) throws IOException {
26         if (!(leaf instanceof Tree.Element)) return new Text(leaf);
27
28         Tree.Element e = (Tree.Element)leaf;
29         final String uri = e.getUri();
30
31         if (uri == null) {
32             // do nothing
33         } else if (uri.equals("http://xt.ibex.org/")) {
34             //#switch(e.getLocalName())
35             case "js":          e = new Template.JSTag(e); break;
36             case "foreach":     e = new Template.ForEach(e); break;
37             case "children":    e = new Template.Children(e); break;
38             case "redirect":    e = new Template.Redirect(e); break;
39             case "transaction": e = new Template.Transaction(e, s); break;
40             //#end
41
42         } else if (uri.startsWith("http://xt.ibex.org/")) {
43             throw new JSLeaf.Exn("Unknown XT library: "+uri);
44
45         } else if (uri.startsWith("local:")) {
46             // merge a new template into this tree
47             String path = uri.substring(6) + e.getLocalName() + ".xt";
48             Template t = parse(s.getLocalPath() + path, s);
49
50             List c = e.getChildren();
51             if (c.size() > 0) {
52                 // move all children from e to placeholder
53                 Tree.Element placeholder = findPlaceholder(t);
54                 if (placeholder == null) throw new JSLeaf.Exn(
55                     "<"+e.getQName()+"> attempted to include children into a " +
56                     "template which does not contain an <xt:children /> tag.");
57
58                 placeholder.getChildren().addAll(e.getChildren());
59                 e.getChildren().clear();
60             }
61
62             // merge the attributes of the template and its representative
63             t.setAttributes(new JSLeaf.MergeAttr(e.getAttributes(), t.getAttributes()));
64
65             // remap the parent of the original element
66             if (e.getParent() != null) {
67                 List ch = e.getParent().getChildren();
68                 ch.set(ch.indexOf(e), t);
69             }
70
71             return wrap(t, s);
72         }
73
74         e = new Template.AttributeEval(e);
75
76         // wrap children
77         List c = e.getChildren();
78         for (int i=0; i < c.size(); i++) wrap((Tree.Leaf)c.get(i), s);
79
80         return e;
81     }
82
83     /** Returns the first Template.Children child found. */
84     private static Tree.Element findPlaceholder(Tree.Element e) {
85         if ("http://xt.ibex.org/".equals(e.getUri()) && "children".equals(e.getLocalName()))
86             return e;
87
88         List c = e.getChildren();
89         for (int i=0; i < c.size(); i++) {
90             if (!(c.get(i) instanceof Tree.Element)) continue;
91             Tree.Element ret = findPlaceholder((Tree.Element)c.get(i));
92             if (ret != null) return ret;
93         }
94         return null;
95     }
96
97     private transient Template.Scope tscope;
98     private transient JSScope scope = null;
99
100     public Template(Tree.Element w, Template.Scope t) { super(w); tscope = t; }
101
102     public JSScope scope() { return scope == null ? scope = new JSScope(tscope) : scope; }
103
104
105     /** Processes ${...} blocks in attributes, loads applicable
106      *  attributes into the JS scope and processes global attributes. */
107     public static class AttributeEval extends JSLeaf.Element implements Tree.Attributes {
108         // TODO: hide global attributes from out() function. waiting on util.XMLHelper
109         protected Tree.Attributes a;
110
111         public AttributeEval(Tree.Element wrapped) {
112             super(wrapped);
113             a = wrapped.getAttributes();
114             wrapped.setAttributes(this);
115         }
116
117         public int getIndex(String q) { return a.getIndex(q); }
118         public int getIndex(String u, String k) { return a.getIndex(u, k); }
119         public String getKey(int i) { return a.getKey(i); }
120         public String getVal(int i) { return (String)eval(a.getVal(i)); }
121         public String getUri(int i) { return a.getUri(i); }
122         public String getPrefix(int i) { return a.getPrefix(i); }
123         public String getQName(int i) { return a.getQName(i); }
124         public int attrSize() { return a.attrSize(); }
125
126         public void out(Writer w) throws IOException {
127             try {
128                 // FIXME: questionable abuse of namespaces here
129                 boolean xturi = "http://xt.ibex.org/".equals(getUri());
130                 for(int i=0; i < a.attrSize(); i++) {
131                     if (!xturi && !"http://xt.ibex.org/".equals(a.getUri(i))) continue;
132
133                     //#switch (a.getKey(i))
134                     case "if": if (!"true".equals(eval(a.getVal(i)))) return;
135                     case "declare":
136                         Object d = eval(a.getVal(i));
137                         if (!(d instanceof String)) throw new JSLeaf.Exn(
138                             "attribute '"+getPrefix()+":declare' can only contain a "+
139                             "space seperated list of variable names to declare.");
140                         StringTokenizer st = new StringTokenizer((String)d, " ");
141                         while (st.hasMoreTokens()) scope().declare(st.nextToken());
142                         continue;
143                     //#end
144
145                     scope().declare(a.getKey(i));
146                     scope().put(a.getKey(i), eval(a.getVal(i)));
147                 }
148             } catch (JSExn e) { throw new Exn(e); }
149
150             wrapped.out(w);
151         }
152     }
153
154     public static final class JSTag extends JSLeaf.Element {
155         public JSTag(Tree.Element e) {
156             super(e);
157             List c = getChildren();
158             for (int i=0; i < c.size(); i++)
159                 if (c.get(i) instanceof Tree.Element) throw new JSLeaf.Exn(
160                     "<"+getPrefix()+":js> tags may not have child elements");
161         }
162
163         public void out(Writer w) throws IOException {
164             List c = getChildren();
165             StringWriter s = new StringWriter();
166             for (int i=0; i < c.size(); i++) ((Tree.Leaf)c.get(i)).out(s);
167             exec(s.toString());
168         }
169     }
170
171     public static final class ForEach extends JSLeaf.Element {
172         public ForEach(Tree.Element e) { super(e); }
173
174         public void out(Writer w) throws IOException {
175             try {
176                 JSScope s = scope();
177                 Object varIn = s.get("in"); if (varIn != null) s.undeclare("in");
178                 Object varPut = s.get("put"); if (varPut != null) s.undeclare("put");
179
180                 varIn = exec("return (" + varIn + ");");
181                 if (varIn == null || (varIn instanceof JSArray)) throw new JSLeaf.Exn(
182                     "<"+getPrefix()+":foreach> requires attribute 'in' to specify " +
183                     "the name of a valid js array in the current scope, not in='"+varIn+"'.");
184
185                 if (varPut == null) varPut = "x";
186                 else if (!(varPut instanceof String) || s.get(varPut) != null)
187                     throw new JSLeaf.Exn(
188                     "<"+getPrefix()+":foreach> 'put' attribute requires the name of "+
189                     "an undeclared variable, not put='"+varPut+"'.");
190                 if (scope().get(varPut) != null) throw new JSLeaf.Exn(
191                     "<"+getPrefix()+":foreach> has no 'put' attribute defined and the "+
192                     "default variable 'x' already exists in the current scope.");
193
194                 List c = getChildren();
195
196                 s.declare((String)varPut);
197                 Iterator it = ((JSArray)varIn).toList().iterator(); while (it.hasNext()) {
198                     s.put(varPut, it.next());
199                     for (int i=0; i < c.size(); i++) ((Tree.Leaf)c.get(i)).out(w);
200                 }
201             } catch (JSExn e) { throw new JSLeaf.Exn(e); }
202         }
203     }
204
205     public static final class Children extends JSLeaf.Element {
206         public Children(Tree.Element e) { super(e); }
207     }
208
209     public static final class Redirect extends JSLeaf.Element {
210         public Redirect(Tree.Element e) { super(e); }
211
212         public void out(Writer w) throws IOException {
213             try {
214                 Object p = scope().get("page"); if (p != null) scope().undeclare("page");
215                 if (p == null || !(p instanceof String) || ((String)p).trim().equals(""))
216                     throw new JSLeaf.Exn("<"+getPrefix()+":redirect> requires 'page' "+
217                                             "attribute to be a valid template path");
218                 throw new RedirectSignal((String)p);
219             } catch (JSExn e) { throw new JSLeaf.Exn(e); }
220         }
221     }
222
223     // TODO: finish
224     public static final class Transaction extends JSLeaf.Element {
225         private final Template.Scope scope; // FIXME: HACK. unstatisise all tags, or do this to all
226         public Transaction(Tree.Element e, Template.Scope s) { super(e); scope = s;} // TODO: check kids
227
228         public void out(Writer w) throws IOException {
229             // TODO: <xt:use />
230             List c = getChildren();
231             StringWriter sw = new StringWriter();
232             for (int i=0; i < c.size(); i++) ((Tree.Leaf)c.get(i)).out(sw);
233             JS t = JS.fromReader("input", 0, new StringReader(sw.toString()));
234             t = JS.cloneWithNewParentScope(t, new JSScope(null));
235             scope.transaction(t);
236         }
237     }
238
239     public static final class Text extends JSLeaf {
240         public Text(Tree.Leaf w) { super(w); }
241         public void out(Writer w) throws IOException {
242             // FIXME: make eval() take a writer
243             StringWriter sw = new StringWriter();
244             super.out(sw);
245             w.write((String)eval(sw.toString()));
246         }
247     }
248
249     public abstract static class Scope extends JSScope {
250         public Scope(JSScope j) { super(j); }
251
252         /** Returns the template path for local:/ namespace. */
253         public abstract String getLocalPath();
254
255         /** Registers a new Prevayler transaction. */
256         public abstract void transaction(JS t);
257     }
258
259     public static class Signal extends RuntimeException {}
260     public static class ReturnSignal extends Signal { }
261     public static class RedirectSignal extends Signal {
262         protected String target;
263         public RedirectSignal(String target) { super(); this.target = target; }
264         public String getTarget() { return target; }
265     }
266 }