2003/09/23 08:24:59
[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
25 // FIXME imports
26 public class Template {
27
28     // Instance Members ///////////////////////////////////////////////////////
29
30     /** the id of this box */
31     String id = null;
32
33     /** the id of the redirect target; only meaningful on a root node */
34     String redirect = null;
35
36     /** templates that should be preapplied (in the order of application); only meaningful on a root node */
37     private Template[] preapply;
38
39     /** templates that should be postapplied (in the order of application); only meaningful on a root node */
40     private Template[] postapply;
41
42     /** keys to be "put" to instances of this template; elements correspond to those of vals */
43     private String[] keys;
44
45     /** values to be "put" to instances of this template; elements correspond to those of keys */
46     private Object[] vals;
47
48     /** child template objects */
49     private Template[] children;
50
51     /** see numUnits(); -1 means that this value has not yet been computed */
52     private int numunits = -1;
53
54     /** the scope in which the static block is executed */
55     private JS.Scope staticScope = null;
56
57     /** the script on the static node of this template, null if it has already been executed */
58     private JS.CompiledFunction staticscript = null;
59
60     /** the script on this node */
61     private JS.CompiledFunction script = null;
62
63     /** the filename this node came from; used only for debugging */
64     private String fileName = "unknown";
65
66
67     // Only used during parsing /////////////////////////////////////////////////////////////////
68
69     /** during XML parsing, this holds the list of currently-parsed children; null otherwise */
70     private Vec childvect = new Vec();
71
72     /** during XML parsing, this holds partially-read character data; null otherwise */
73     private StringBuffer content = null;
74
75     /** line number of the first line of <tt>content</tt> */
76     private int content_start = 0;
77
78     /** number of lines in <tt>content</tt> */
79     private int content_lines = 0;
80
81     /** the line number that this element starts on */
82     private int startLine = -1;
83
84
85     // Static data/methods ///////////////////////////////////////////////////////////////////
86
87     private Template(String fileName) { this.fileName = fileName; }
88
89     public static Template getTemplate(Res r) {
90         try {
91             if (r.t != null) return r.t;
92             r.t = new Template(r.getDescriptiveName());
93             new TemplateHelper().parseit(r.getInputStream(), r.t);
94             return r.t;
95         } catch (XML.SchemaException e) {
96             if (Log.on) Log.log(Template.class, "error parsing template " + r.t.fileName);
97             if (Log.on) Log.log(Template.class, e.getMessage());
98             return null;
99         } catch (XML.XMLException e) {
100             if (Log.on) Log.log(Template.class, "error parsing template at " + r.t.fileName + ":" + e.getLine() + "," + e.getCol());
101             if (Log.on) Log.log(Template.class, e.getMessage());
102             return null;
103         } catch (IOException e) {
104             if (Log.on) Log.log(Template.class, "IOException while parsing template " + r.t.fileName + " -- this should never happen");
105             if (Log.on) Log.log(Template.class, e);
106             return null;
107         }
108     }
109
110
111     // Methods to apply templates ////////////////////////////////////////////////////////
112
113     /** calculates, caches, and returns an integer approximation of how long it will take to apply this template,
114      *  including pre/post and children */
115     int numUnits() {
116         if (numunits != -1) return numunits;
117         numunits = 1;
118         for(int i=0; preapply != null && i<preapply.length; i++) numunits += preapply[i].numUnits();
119         for(int i=0; postapply != null && i<postapply.length; i++) numunits += postapply[i].numUnits();
120         if (script != null) numunits += 10;
121         numunits += keys == null ? 0 : keys.length;
122         for(int i=0; children != null && i<children.length; i++) numunits += children[i].numUnits();
123         return numunits;
124     }
125
126     /** called before this template is applied or its static object can be externally referenced */
127     JS.Scope getStatic() {
128         if (staticScope == null) staticScope = new JS.Scope(null);
129         if (staticscript == null) return staticScope;
130         JS.CompiledFunction temp = staticscript;
131         staticscript = null;
132         temp.call(new JS.Array(), staticScope);
133         return staticScope;
134     }
135     
136     /** Applies the template to Box b
137      *  @param pboxes a vector of all box parents on which to put $-references
138      *  @param ptemplates a vector of the fileNames to recieve private references on the pboxes
139      */
140     // FIXME: $-vars not dealt with
141     void apply(Box b, JS.Callable callback, int numerator, int denominator, Res resourceRoot) {
142
143         getStatic();
144         int original_numerator = numerator;
145
146         for(int i=0; preapply != null && i<preapply.length; i++) {
147             preapply[i].apply(b, callback, numerator, denominator, resourceRoot);
148             numerator += preapply[i].numUnits();
149         }
150
151         for (int i=0; children != null && i<children.length; i++) {
152             children[i].apply(new Box(), callback, numerator, denominator, resourceRoot);
153             numerator += children[i].numUnits();
154         }
155
156         // whom to redirect to; doesn't take effect until after script runs
157         Box redir = (redirect != null && !"self".equals(redirect)) ? (Box)b.get("$" + redirect) : null;
158
159         if (script != null) script.call(new JS.Array(), new PerInstantiationScope(b, resourceRoot));
160
161         for(int i=0; keys != null && i<keys.length; i++) b.put(keys[i], vals[i]);
162
163         if (redirect != null && !"self".equals(redirect)) b.redirect = redir;
164
165         for(int i=0; postapply != null && i<postapply.length; i++) {
166             postapply[i].apply(b, callback, numerator, denominator, resourceRoot);
167             numerator += postapply[i].numUnits();
168         }
169
170         numerator = original_numerator + numUnits();
171
172         if (callback != null) try {
173             JS.Array args = new JS.Array();
174             args.addElement(new Double(numerator));
175             args.addElement(new Double(denominator));
176             callback.call(args);
177         } catch (JS.Exn e) { if (Log.on) Log.log(this, "WARNING: uncaught ecmascript exception: " + e); }
178
179         if (Thread.currentThread() instanceof ThreadMessage) XWT.sleep(0);
180     }
181
182
183
184     // XML Parsing /////////////////////////////////////////////////////////////////
185
186     /** handles XML parsing; builds a Template tree as it goes */
187     static final class TemplateHelper extends XML {
188
189         TemplateHelper() { }
190
191         /** parse an XML input stream, building a Template tree off of <tt>root</tt> */
192         void parseit(InputStream is, Template root) throws XML.XMLException, IOException {
193             rootNodeHasBeenEncountered = false;
194             templateNodeHasBeenEncountered = false;
195             staticNodeHasBeenEncountered = false;
196             templateNodeHasBeenFinished = false;
197             nameOfHeaderNodeBeingProcessed = null;
198
199             nodeStack.setSize(0);
200             preapply.setSize(0);
201             postapply.setSize(0);
202
203             t = root;
204             parse(new InputStreamReader(is)); 
205         }
206
207         /** parsing state: true iff we have already encountered the <xwt> open-tag */
208         boolean rootNodeHasBeenEncountered = false;
209
210         /** parsing state: true iff we have already encountered the <template> open-tag */
211         boolean templateNodeHasBeenEncountered = false;
212
213         /** parsing state: true iff we have already encountered the <static> open-tag */
214         boolean staticNodeHasBeenEncountered = false;
215
216         /** parsing state: true iff we have already encountered the <template> close-tag */
217         boolean templateNodeHasBeenFinished = false;
218
219         /** parsing state: If we have encountered the open tag of a header node, but not the close tag, this is the name of
220          *  that tag; otherwise, it is null. */
221         String nameOfHeaderNodeBeingProcessed = null;
222
223         /** stack of Templates whose XML elements we have seen open-tags for but not close-tags */
224         Vec nodeStack = new Vec();
225
226         /** builds up the list of preapplies */
227         Vec preapply = new Vec();
228
229         /** builds up the list of postapplies */
230         Vec postapply = new Vec();
231
232         /** the template we're currently working on */
233         Template t = null;
234
235         public void startElement(XML.Element c) throws XML.SchemaException {
236             if (templateNodeHasBeenFinished) {
237                 throw new XML.SchemaException("no elements may appear after the <template> node");
238
239             } else if (!rootNodeHasBeenEncountered) {
240                 if (!"xwt".equals(c.localName)) throw new XML.SchemaException("root element was not <xwt>");
241                 if (c.len != 0) throw new XML.SchemaException("root element must not have attributes");
242                 rootNodeHasBeenEncountered = true;
243                 return;
244         
245             } else if (!templateNodeHasBeenEncountered) {
246                 if (nameOfHeaderNodeBeingProcessed != null) throw new XML.SchemaException("can't nest header nodes");
247                 nameOfHeaderNodeBeingProcessed = c.localName;
248
249                 if (c.localName.equals("import")) {
250                     if (c.len != 1 || !c.keys[0].equals("name"))
251                         throw new XML.SchemaException("<import> node must have exactly one attribute, which must be called 'name'");
252                     String importpackage = c.vals[0].toString();
253                     if (importpackage.endsWith(".*")) importpackage = importpackage.substring(0, importpackage.length() - 2);
254                     return;
255
256                 } else if (c.localName.equals("redirect")) {
257                     if (c.len != 1 || !c.keys[0].equals("target"))
258                         throw new XML.SchemaException("<redirect> node must have exactly one attribute, which must be called 'target'");
259                     if (t.redirect != null)
260                         throw new XML.SchemaException("the <redirect> header element may not appear more than once");
261                     t.redirect = c.vals[0].toString();
262                     if(t.redirect.equals("null")) t.redirect = null;
263                     return;
264
265                 } else if (c.localName.equals("preapply")) {
266                     if (c.len != 1 || !c.keys[0].equals("name"))
267                         throw new XML.SchemaException("<preapply> node must have exactly one attribute, which must be called 'name'");
268                     preapply.addElement(c.vals[0]);
269                     return;
270
271                 } else if (c.localName.equals("postapply")) {
272                     if (c.len != 1 || !c.keys[0].equals("name"))
273                         throw new XML.SchemaException("<postapply> node must have exactly one attribute, which must be called 'name'");
274                     postapply.addElement(c.vals[0]);
275                     return;
276
277                 } else if (c.localName.equals("static")) {
278                     if (staticNodeHasBeenEncountered)
279                         throw new XML.SchemaException("the <static> header node may not appear more than once");
280                     if (c.len > 0)
281                         throw new XML.SchemaException("the <static> node may not have attributes");
282                     staticNodeHasBeenEncountered = true;
283                     return;
284
285                 } else if (c.localName.equals("template")) {
286                     // finalize importlist/preapply/postapply, since they can't change from here on
287                     t.startLine = getLine();
288                     if (preapply.size() > 0) preapply.copyInto(t.preapply = new Template[preapply.size()]);
289                     if (postapply.size() > 0) postapply.copyInto(t.postapply = new Template[postapply.size()]);
290                     templateNodeHasBeenEncountered = true;
291
292                 } else {
293                     throw new XML.SchemaException("unrecognized header node \"" + c.localName + "\"");
294
295                 }
296
297             } else {
298
299                 // push the last node we were in onto the stack
300                 nodeStack.addElement(t);
301
302                 // instantiate a new node, and set its fileName/importlist/preapply
303                 Template t2 = new Template(t.fileName);
304                 t2.startLine = getLine();
305                 if (!c.localName.equals("box")) t2.preapply = new Template[] { /*c.localName FIXME */ };
306
307                 // make the new node the current node
308                 t = t2;
309
310             }
311
312             // TODO: Sort contents straight from one array to another
313             // FIXME: height must come after image
314             // FIXME: use Vec here
315             t.keys = new String[c.len];
316             t.vals = new Object[c.len];
317             System.arraycopy(c.keys, 0, t.keys, 0, c.len);
318             System.arraycopy(c.vals, 0, t.vals, 0, c.len);
319             quickSortAttributes(0, t.keys.length - 1);
320
321             for(int i=0; i<t.keys.length; i++) {
322                 if (t.keys[i].equals("id")) {
323                     t.id = t.vals[i].toString().intern();
324                     t.keys[i] = null;
325                     continue;
326                 }
327
328                 t.keys[i] = t.keys[i].intern();
329
330                 String valString = t.vals[i].toString();
331                 
332                 if (valString.equals("true")) t.vals[i] = Boolean.TRUE;
333                 else if (valString.equals("false")) t.vals[i] = Boolean.FALSE;
334                 else if (valString.equals("null")) t.vals[i] = null;
335                 else {
336                     boolean hasNonNumeral = false;
337                     boolean periodUsed = false;
338                     for(int j=0; j<valString.length(); j++)
339                         if (j == 0 && valString.charAt(j) == '-') {
340                         } else if (valString.charAt(j) == '.' && !periodUsed && j != valString.length() - 1) {
341                             periodUsed = true;
342                         } else if (!Character.isDigit(valString.charAt(j))) {
343                             hasNonNumeral = true;
344                             break;
345                         }
346                     if (valString.length() > 0 && !hasNonNumeral) t.vals[i] = new Double(valString);
347                     else t.vals[i] = valString.intern();
348                 }
349
350                 // bump thisbox to the front of the pack
351                 if (t.keys[i].equals("thisbox")) {
352                     t.keys[i] = t.keys[0];
353                     t.keys[0] = "thisbox";
354                     Object o = t.vals[0];
355                     t.vals[0] = t.vals[i];
356                     t.vals[i] = o;
357                 }
358             }
359         }
360
361         /** simple quicksort, from http://sourceforge.net/snippet/detail.php?type=snippet&id=100240 */
362         private int partitionAttributes(int left, int right) {
363             int i, j, middle;
364             middle = (left + right) / 2;
365             String s = t.keys[right]; t.keys[right] = t.keys[middle]; t.keys[middle] = s;
366             Object o = t.vals[right]; t.vals[right] = t.vals[middle]; t.vals[middle] = o;
367             for (i = left - 1, j = right; ; ) {
368                 while (t.keys[++i].compareTo(t.keys[right]) < 0);
369                 while (j > left && t.keys[--j].compareTo(t.keys[right]) > 0);
370                 if (i >= j) break;
371                 s = t.keys[i]; t.keys[i] = t.keys[j]; t.keys[j] = s;
372                 o = t.vals[i]; t.vals[i] = t.vals[j]; t.vals[j] = o;
373             }
374             s = t.keys[right]; t.keys[right] = t.keys[i]; t.keys[i] = s;
375             o = t.vals[right]; t.vals[right] = t.vals[i]; t.vals[i] = o;
376             return i;
377         }
378
379         /** simple quicksort, from http://sourceforge.net/snippet/detail.php?type=snippet&id=100240 */
380         private void quickSortAttributes(int left, int right) {
381             if (left >= right) return;
382             int p = partitionAttributes(left, right);
383             quickSortAttributes(left, p - 1);
384             quickSortAttributes(p + 1, right);
385         }
386
387         public void endElement(XML.Element c) throws XML.SchemaException {
388             if (rootNodeHasBeenEncountered && !templateNodeHasBeenEncountered) {
389                 if ("static".equals(nameOfHeaderNodeBeingProcessed) && t.content != null) t.staticscript = genscript(true);
390                 nameOfHeaderNodeBeingProcessed = null;
391
392             } else if (templateNodeHasBeenEncountered && !templateNodeHasBeenFinished) {
393                 // turn our childvect into a Template[]
394                 t.childvect.copyInto(t.children = new Template[t.childvect.size()]);
395                 t.childvect = null;
396                 if (t.content != null) t.script = genscript(false);
397                 
398                 if (nodeStack.size() == 0) {
399                     // </template>
400                     templateNodeHasBeenFinished = true;
401
402                 } else {
403                     // add this template as a child of its parent
404                     Template oldt = t;
405                     t = (Template)nodeStack.lastElement();
406                     nodeStack.setSize(nodeStack.size() - 1);
407                     t.childvect.addElement(oldt);
408                 }
409
410             }
411         }
412
413         private JS.CompiledFunction genscript(boolean isstatic) {
414             JS.CompiledFunction thisscript = null;
415             try {
416                 thisscript = JS.parse(t.fileName + (isstatic ? "._" : ""), t.content_start, new StringReader(t.content.toString()));
417             } catch (IOException ioe) {
418                 if (Log.on) Log.log(this, "  ERROR: " + ioe.getMessage());
419                 thisscript = null;
420             }
421
422             t.content = null;
423             t.content_start = 0;
424             t.content_lines = 0;
425             return thisscript;
426         }
427
428         public void characters(char[] ch, int start, int length) throws XML.SchemaException {
429             // invoke the no-tab crusade
430             for (int i=0; length >i; i++) if (ch[start+i] == '\t') throw new XML.SchemaException(
431                 t.fileName+ ":" + getLine() + "," + getCol() + ": tabs are not allowed in XWT files");
432
433             if ("static".equals(nameOfHeaderNodeBeingProcessed) || templateNodeHasBeenEncountered) {
434                 if (t.content == null) {
435                     t.content_start = getLine();
436                     t.content_lines = 0;
437                     t.content = new StringBuffer();
438                 }
439
440                 t.content.append(ch, start, length);
441                 t.content_lines++;
442
443             } else if (nameOfHeaderNodeBeingProcessed != null) {
444                 throw new XML.SchemaException("header node <" + nameOfHeaderNodeBeingProcessed + "> cannot have text content");
445             }
446         }
447
448         public void whitespace(char[] ch, int start, int length) throws XML.SchemaException {
449         }
450     }
451
452     private static class PerInstantiationScope extends JS.Scope {
453         Res resourceRoot = null;
454         public PerInstantiationScope(Scope parentScope, Res resourceRoot) {
455             super(parentScope);
456             this.resourceRoot = resourceRoot;
457         }
458         public boolean isTransparent() { return true; }
459         public boolean has(Object key) { return false; }
460         public void declare(String s) { super.declare(s); }
461         public Object get(Object key) {
462             // FIXME: access statics here
463             if (Box.SpecialBoxProperty.specialBoxProperties.get(key) == null &&
464                 !super.has(key)) {
465                 Object ret = resourceRoot.get(key);
466                 if (ret != null) return ret;
467                 throw new JS.Exn("must declare " + key + " before using it!");
468             }
469             return super.get(key);
470         }
471         public void put(Object key, Object val) {
472             // FIXME: access statics here
473             if (Box.SpecialBoxProperty.specialBoxProperties.get(key) == null &&
474                 !super.has(key)) {
475                 throw new JS.Exn("must declare " + key + " before using it!");
476             }
477             super.put(key, val);
478         }
479     }
480
481 }
482
483