2003/09/25 10:10:52
[org.ibex.core.git] / src / org / xwt / Template.java
1 // Copyright 2003 Adam Megacz, see the COPYING file for licensing [GPL]
2 package org.xwt;
3
4 import java.io.*;
5 import java.util.zip.*;
6 import java.util.*;
7 import java.lang.*;
8 import org.xwt.js.*;
9 import org.xwt.util.*;
10
11 /**
12  *  Encapsulates a template node (the <template/> element of a
13  *  .xwt file, or any child element thereof).
14  *
15  *  Note that the Template instance corresponding to the
16  *  <template/> node carries all the header information -- hence
17  *  some of the instance members are not meaningful on non-root
18  *  Template instances. We refer to these non-root instances as
19  *  <i>anonymous templates</i>.
20  *
21  *  See the XWT reference for information on the order in which
22  *  templates are applied, attributes are put, and scripts are run.
23  */
24 public class Template {
25
26     // Instance Members ///////////////////////////////////////////////////////
27
28     String id = null;                  ///< the id of this box
29     String redirect = null;            ///< the id of the redirect target; only meaningful on a root node
30     private String[] keys;             ///< keys to be "put" to instances of this template; elements correspond to those of vals
31     private Object[] vals;             ///< values to be "put" to instances of this template; elements correspond to those of keys
32     private Vec children = new Vec();  ///< during XML parsing, this holds the list of currently-parsed children; null otherwise
33     private int numunits = -1;         ///< see numUnits(); -1 means that this value has not yet been computed
34
35     private JS.CompiledFunction script = null;       ///< the script on this node
36     private String fileName = "unknown";             ///< the filename this node came from; used only for debugging
37     private Vec preapply;                            ///< templates that should be preapplied (in the order of application)
38
39
40     // Instance Members that are only meaningful on root Template //////////////////////////////////////
41
42     private JS.Scope staticScope = null;             ///< the scope in which the static block is executed
43     private JS.CompiledFunction staticscript = null; ///< the script on the static node of this template, null already performed
44
45
46     // Only used during parsing /////////////////////////////////////////////////////////////////
47
48     private StringBuffer content = null; ///< during XML parsing, this holds partially-read character data; null otherwise
49     private int content_start = 0;       ///< line number of the first line of <tt>content</tt>
50     private int content_lines = 0;       ///< number of lines in <tt>content</tt>
51     private int startLine = -1;          ///< the line number that this element starts on
52     final Res r;                         ///< the resource we came from
53
54
55     // Static data/methods ///////////////////////////////////////////////////////////////////
56
57     public static Template getTemplate(Res r) {
58         try {
59             if (r.t != null) return r.t;
60             r.t = new Template(r);
61             new TemplateHelper().parseit(r.getInputStream(), r.t);
62             return r.t;
63         } catch (Exception e) {
64             if (Log.on) Log.log(r.t.fileName, e);
65             return null;
66         }
67     }
68
69     public static Res resolveStringToResource(String str, XWT xwt, boolean permitAbsolute) {
70         // URL
71         if (str.indexOf("://") != -1) {
72             if (permitAbsolute) return str;
73             Log.log("absolute URL " + str + " not permitted here");
74             return null;
75         }
76
77         // root-relative
78         Res ret = xwt.rr;
79         while(str.indexOf('.') != -1) {
80             String path = str.substring(0, str.indexOf('.'));
81             str = str.substring(str.indexOf('.') + 1);
82             ret = (Res)ret.get(path);
83         }
84         ret = (Res)ret.get(str);
85         return ret;
86     }
87
88
89     // Methods to apply templates ////////////////////////////////////////////////////////
90
91     private Template() { this.r = r; }
92
93     /** called before this template is applied or its static object can be externally referenced */
94     JS.Scope getStatic() {
95         if (staticScope == null) staticScope = new JS.Scope(null);
96         if (staticscript == null) return staticScope;
97         JS.CompiledFunction temp = staticscript;
98         staticscript = null;
99         temp.call(new JS.Array(), staticScope);
100         return staticScope;
101     }
102     
103     /** Applies the template to Box b
104      *  @param pboxes a vector of all box parents on which to put $-references
105      *  @param ptemplates a vector of the fileNames to recieve private references on the pboxes
106      */
107     void apply(Box b, JS.Callable c, XWT xwt) { apply(b, c, xwt, null); }
108     void apply(Box b, JS.Callable callback, XWT xwt, PerInstantiationScope parentPis) {
109
110         getStatic();
111
112         if (id != null) parentPis.putDollar(id, this);
113         for(int i=0; i<preapply.size(); i++) {
114             Template t = getTemplate(resolveStringToResource((String)preapply.elementAt(i), xwt));
115             if (t == null) throw new RuntimeException("unable to resolve resource " + preapply.elementAt(i));
116             t.apply(b, callback, numerator, denominator, xwt);
117         }
118
119         PerInstantiationScope pis = new PerInstantiationScope(b, xwt, parentPis);
120         for (int i=0; children != null && i<children.length; i++) {
121             Box kid = new Box();
122             getTemplate(resolveStringToResource((String)children.elementAt(i), xwt)).apply(kid, callback, xwt, pis);
123             b.put(b.numChildren(), kid);
124         }
125
126         if (script != null) script.call(new JS.Array(), pis);
127
128         for(int i=0; keys != null && i<keys.length; i++)
129             if (vals[i] instanceof String && ((String)vals[i]).charAt(0) == '$') b.put(keys[i], pis.get(vals[i]));
130             else b.put(keys[i], vals[i]);
131     }
132
133
134
135     // XML Parsing /////////////////////////////////////////////////////////////////
136
137     /** handles XML parsing; builds a Template tree as it goes */
138     static final class TemplateHelper extends XML {
139
140         TemplateHelper() { }
141
142         private int state;
143         private static final int STATE_INITIAL = 0;
144         private static final int STATE_IN_XWT_NODE = 1;
145         private static final int STATE_IN_TEMPLATE_NODE = 2;
146         private static final int STATE_FINISHED_TEMPLATE_NODE = 3;
147
148         private String nameOfHeaderNodeBeingProcessed;
149
150         Vec nodeStack = new Vec();  ///< stack of Templates whose XML elements we have seen open-tags for but not close-tags
151         Template t = null;          ///< the template we're currently working on
152
153         /** parse an XML input stream, building a Template tree off of <tt>root</tt> */
154         void parseit(InputStream is, Template root) throws XML.XMLException, IOException {
155             state = STATE_INITIAL;
156             nameOfHeaderNodeBeingProcessed = null;
157             nodeStack.setSize(0);
158             t = root;
159             parse(new InputStreamReader(is)); 
160         }
161
162         public void startElement(XML.Element c) throws XML.SchemaException {
163             switch(state) {
164             case STATE_INITIAL:
165                 if (!"xwt".equals(c.localName)) throw new XML.SchemaException("root element was not <xwt>");
166                 if (c.len != 0) throw new XML.SchemaException("root element must not have attributes");
167                 state = STATE_IN_XWT_NODE
168                 return;
169
170             case STATE_IN_XWT_NODE:
171                 if (nameOfHeaderNodeBeingProcessed != null) throw new XML.SchemaException("can't nest header nodes");
172                 nameOfHeaderNodeBeingProcessed = c.localName;
173                 if (c.localName.equals("doc")) {
174                     // FIXME: ignore
175                 } else if (c.localName.equals("static")) {
176                     if (staticscript != null)
177                         throw new XML.SchemaException("the <static> header node may not appear more than once");
178                     if (c.len > 0)
179                         throw new XML.SchemaException("the <static> node may not have attributes");
180                 } else if (c.localName.equals("template")) {
181                     t.startLine = getLine();
182                     state = STATE_IN_TEMPLATE_NODE;
183                 } else {
184                     throw new XML.SchemaException("unrecognized header node \"" + c.localName + "\"");
185                 }
186                 return;
187
188             case STATE_IN_TEMPLATE_NODE:
189                 processBodyElement(c);
190                 return;
191
192             case STATE_FINISHED_TEMPLATE_NODE:
193                 throw new XML.SchemaException("no elements may appear after the <template> node");
194             }
195         }        
196
197         private void processBodyElement(XML.Element c) {
198             // push the last node we were in onto the stack
199             nodeStack.addElement(t);
200             
201             // instantiate a new node, and set its fileName/importlist/preapply
202             Template t2 = new Template(t.fileName);
203             t2.startLine = getLine();
204             if (!c.localName.equals("box"))
205                 t2.preapply.addElement((c.uri == null ? "" : (c.uri + ".")) + c.localName);
206             // make the new node the current node
207             t = t2;
208             
209             t.keys = new String[c.len];
210             t.vals = new Object[c.len];
211             Hash h = new Hash(c.len * 2, 3);
212             for(int i=0; i<c.len; i++) {
213                 if (c.keys[i].endsWith(":image")) {
214                     String uri = c.urimap.get(c.keys[i].substring(0, c.keys[i].indexOf(':')));
215                     c.keys[i] = c.keys[i].substring(c.keys[i].lastIndexOf(':') + 1);
216                     c.vals[i] = uri + "." + c.vals[i];
217                 }                    
218                 if ((c.keys[i].equals("preapply") || c.keys[i].endsWith(":preapply")) && c.localName.equals("template")) {
219                     String uri = "";
220                     if (c.keys[i].endsWith(":preapply")) {
221                         uri = "." + c.urimap.get(c.keys[i].substring(0, c.keys[i].indexOf(':')));
222                         c.keys[i] = c.keys[i].substring(c.keys[i].lastIndexOf(':') + 1);
223                     }
224                     StringTokenizer tok = new StringTokenizer(c.vals[i].toString(), " ");
225                     while(tok.hasMoreTokens()) t.preapply.addElement(uri + tok.nextToken());
226                     continue;
227                 }
228                 h.put(c.keys[i], c.vals[i]);
229             }
230
231             Vec v = new Vec(h.size(), c.keys);
232             v.sort(new Vec.CompareFunc() { public int compare(Object a, Object b) { return ((String)a).compareTo((String)b); } });
233             for(int i=0; i<h.size(); i++) {
234                 if (t.keys[i].equals("thisbox")) {
235                     for(int j=i; j>0; j--) { t.keys[j] = t.keys[j - 1]; t.vals[j] = t.vals[j - 1]; }
236                     t.keys[0] = (String)v.elementAt(i);
237                     t.vals[0] = h.get(t.keys[i]);
238                 } else {
239                     t.keys[i] = (String)v.elementAt(i);
240                     t.vals[i] = h.get(t.keys[i]);
241                 }
242             }
243
244             for(int i=0; i<t.keys.length; i++) {
245                 if (t.keys[i].equals("id")) {
246                     t.id = t.vals[i].toString().intern();
247                     t.keys[i] = null;
248                     continue;
249                 }
250
251                 t.keys[i] = t.keys[i].intern();
252
253                 String valString = t.vals[i].toString();
254                 
255                 if (valString.equals("true")) t.vals[i] = Boolean.TRUE;
256                 else if (valString.equals("false")) t.vals[i] = Boolean.FALSE;
257                 else if (valString.equals("null")) t.vals[i] = null;
258                 else {
259                     boolean hasNonNumeral = false;
260                     boolean periodUsed = false;
261                     for(int j=0; j<valString.length(); j++)
262                         if (j == 0 && valString.charAt(j) == '-') {
263                         } else if (valString.charAt(j) == '.' && !periodUsed && j != valString.length() - 1) {
264                             periodUsed = true;
265                         } else if (!Character.isDigit(valString.charAt(j))) {
266                             hasNonNumeral = true;
267                             break;
268                         }
269                     if (valString.length() > 0 && !hasNonNumeral) t.vals[i] = new Double(valString);
270                     else t.vals[i] = valString.intern();
271                 }
272             }
273         }
274
275         private JS.CompiledFunction parseScript(boolean isstatic) {
276             JS.CompiledFunction thisscript = null;
277             try {
278                 thisscript = JS.parse(t.fileName + (isstatic ? "._" : ""), t.content_start, new StringReader(t.content.toString()));
279             } catch (IOException ioe) {
280                 if (Log.on) Log.log(this, "  ERROR: " + ioe.getMessage());
281                 thisscript = null;
282             }
283             t.content = null;
284             t.content_start = 0;
285             t.content_lines = 0;
286             return thisscript;
287         }
288
289         public void endElement(XML.Element c) throws XML.SchemaException {
290             if (state == STATE_IN_XWT_NODE) {
291                 if ("static".equals(nameOfHeaderNodeBeingProcessed) && t.content != null) t.staticscript = parseScript(true);
292                 nameOfHeaderNodeBeingProcessed = null;
293                 
294             } else if (state == STATE_IN_TEMPLATE_NODE) {
295                 if (t.content != null) t.script = parseScript(false);
296                 if (nodeStack.size() == 0) {
297                     // </template>
298                     state = STATE_FINISHED_TEMPLATE_NODE;
299                     
300                 } else {
301                     // add this template as a child of its parent
302                     Template oldt = t;
303                     t = (Template)nodeStack.lastElement();
304                     nodeStack.setSize(nodeStack.size() - 1);
305                     t.children.addElement(oldt);
306                 }
307             }
308          }
309
310         public void characters(char[] ch, int start, int length) throws XML.SchemaException {
311             // invoke the no-tab crusade
312             for (int i=0; length >i; i++) if (ch[start+i] == '\t') throw new XML.SchemaException(
313                 t.fileName+ ":" + getLine() + "," + getCol() + ": tabs are not allowed in XWT files");
314
315             if ("static".equals(nameOfHeaderNodeBeingProcessed) || state == STATE_IN_TEMPLATE_NODE) {
316                 if (t.content == null) {
317                     t.content_start = getLine();
318                     t.content_lines = 0;
319                     t.content = new StringBuffer();
320                 }
321
322                 t.content.append(ch, start, length);
323                 t.content_lines++;
324
325             } else if (nameOfHeaderNodeBeingProcessed != null) {
326                 throw new XML.SchemaException("header node <" + nameOfHeaderNodeBeingProcessed + "> cannot have text content");
327             }
328         }
329
330         public void whitespace(char[] ch, int start, int length) throws XML.SchemaException { }
331     }
332
333     private static class PerInstantiationScope extends JS.Scope {
334         XWT xwt = null;
335         PerInstantiationScope parentBoxPis = null;
336         void putDollar(String key, Box target) {
337             if (parentBoxPis != null) parentBoxPis.putDollar(key, target);
338             declare("$" + key);
339             put("$" + key, target);
340         }
341         public PerInstantiationScope(Scope parentScope, XWT xwt, PerInstantiationScope parentBoxPis) {
342             super(parentScope);
343             this.parentBoxPis = parentBoxPis;
344             this.xwt = xwt;
345         }
346         public boolean isTransparent() { return true; }
347         public boolean has(Object key) { return false; }
348         public void declare(String s) { super.declare(s); }
349         public Object get(Object key) {
350             if (key.equals("xwt")) return xwt;
351             if (Box.SpecialBoxProperty.specialBoxProperties.get(key) != null) return parentScope.get(key);
352             if (super.has(key)) return super.get(key);
353             Object ret = xwt.rr.get(key);
354             if (ret != null) return ret;
355             throw new JS.Exn("must declare " + key + " before using it!");
356         }
357         public void put(Object key, Object val) {
358             if (Box.SpecialBoxProperty.specialBoxProperties.get(key) == null) parentScope.put(key, val);
359             else if (super.has(key)) super.put(key, val); 
360             else throw new JS.Exn("must declare " + key + " before using it!");
361         }
362     }
363
364 }
365
366