1 // Copyright 2003 Adam Megacz, see the COPYING file for licensing [GPL]
5 import java.util.zip.*;
12 * Encapsulates a template node (the <template/> element of a
13 * .xwt file, or any child element thereof). Each instance of
14 * Template has a <tt>nodeName</tt> -- this is the resource name of
15 * the file that the template node occurs in, concatenated with the
16 * path from the root element to this node, each step of which is in
17 * the form .n for some integer n. Static nodes use the string "._"
20 * Note that the Template instance corresponding to the
21 * <template/> node carries all the header information -- hence
22 * some of the instance members are not meaningful on non-root
23 * Template instances. We refer to these non-root instances as
24 * <i>anonymous templates</i>.
26 * See the XWT reference for information on the order in which
27 * templates are applied, attributes are put, and scripts are run.
29 public class Template {
31 // Instance Members ///////////////////////////////////////////////////////
33 /** this instance's nodeName */
36 /** the id of the redirect target; only meaningful on a root node */
37 String redirect = null;
39 /** templates that should be preapplied (in the order of application); only meaningful on a root node */
40 private String[] preapply;
42 /** 'linked' form of preapply -- the String references have been resolved into instance references */
43 private Template[] _preapply = null;
45 /** templates that should be postapplied (in the order of application); only meaningful on a root node */
46 private String[] postapply;
48 /** 'linked' form of postapply -- the String references have been resolved into instance references */
49 private Template[] _postapply = null;
51 /** keys to be "put" to instances of this template; elements correspond to those of vals */
52 private String[] keys;
54 /** values to be "put" to instances of this template; elements correspond to those of keys */
55 private Object[] vals;
57 /** array of strings representing the importlist for this template */
58 private String[] importlist;
60 /** child template objects */
61 private Template[] children;
63 /** an array of the names of properties to be preserved when retheming; only meaningful on a root node */
64 private String[] preserve = null;
66 /** the <tt>id</tt> attribute on this node */
67 private String id = "";
69 /** see numUnits(); -1 means that this value has not yet been computed */
70 private int numunits = -1;
72 /** true iff the resolution of this template's preapply/postapply sets changed as a result of the most recent call to retheme() */
73 private boolean changed = false;
75 /** the script on the static node of this template, null if it has already been executed */
76 private JS.CompiledFunction staticscript = null;
78 /** the script on this node */
79 private JS.CompiledFunction script = null;
81 /** during XML parsing, this holds the list of currently-parsed children; null otherwise */
82 private Vec childvect = new Vec();
84 /** during XML parsing, this holds partially-read character data; null otherwise */
85 private StringBuffer content = null;
87 /** line number of the first line of <tt>content</tt> */
88 private int content_start = 0;
90 /** number of lines in <tt>content</tt> */
91 private int content_lines = 0;
93 /** the line number that this element starts on */
94 private int startLine = -1;
96 // Static data/methods ///////////////////////////////////////////////////////////////////
98 /** a template cache so that only one Template object is created for each xwt */
99 private static Hashtable cache = new Hashtable(1000);
101 /** The default importlist; in future revisions this will contain "xwt.*" */
102 public static final String[] defaultImportList = new String[] { };
104 /** returns the appropriate template, resolving and theming as needed */
105 public static Template getTemplate(String name, String[] importlist) {
106 String resolved = Resources.resolve(name + ".xwt", importlist);
107 Template t = resolved == null ? null : (Template)cache.get(resolved.substring(0, resolved.length() - 4));
108 if (t != null) return t;
109 if (resolved == null) return null;
111 // note that Templates in xwar's are instantiated as read in via loadStream() --
112 // the following code only runs when XWT is reading templates from a filesystem.
113 ByteArrayInputStream bais = new ByteArrayInputStream(Resources.getResource(resolved));
114 return buildTemplate(bais, resolved.substring(0, resolved.length() - 4));
117 public static Template buildTemplate(InputStream is, String nodeName) {
118 return buildTemplate(is, nodeName, new TemplateHelper());
121 public static Template buildTemplate(InputStream is, String nodeName, TemplateHelper t) {
123 return new Template(is, nodeName, t);
124 } catch (XML.SchemaException e) {
125 if (Log.on) Log.log(Template.class, "error parsing template " + nodeName);
126 if (Log.on) Log.log(Template.class, e.getMessage());
128 } catch (XML.XMLException e) {
129 if (Log.on) Log.log(Template.class, "error parsing template at " + nodeName + ":" + e.getLine() + "," + e.getCol());
130 if (Log.on) Log.log(Template.class, e.getMessage());
132 } catch (IOException e) {
133 if (Log.on) Log.log(Template.class, "IOException while parsing template " + nodeName + " -- this should never happen");
134 if (Log.on) Log.log(Template.class, e);
140 // Methods to apply templates ////////////////////////////////////////////////////////
142 private Template(String nodeName) {
143 this.nodeName = nodeName;
144 cache.put(nodeName, this);
146 private Template(InputStream is, String nodeName, TemplateHelper th) throws XML.XMLException, IOException {
148 th.parseit(is, this);
151 /** calculates, caches, and returns an integer approximation of how long it will take to apply this template, including pre/post and children */
154 if (numunits != -1) return numunits;
156 for(int i=0; _preapply != null && i<_preapply.length; i++) if (_preapply[i] != null) numunits += _preapply[i].numUnits();
157 for(int i=0; _postapply != null && i<_postapply.length; i++) if (_postapply[i] != null) numunits += _postapply[i].numUnits();
158 if (script != null) numunits += 10;
159 numunits += keys == null ? 0 : keys.length;
160 for(int i=0; children != null && i<children.length; i++) numunits += children[i].numUnits();
164 /** Applies the template to Box b
165 * @param pboxes a vector of all box parents on which to put $-references
166 * @param ptemplates a vector of the nodeNames to recieve private references on the pboxes
168 void apply(Box b, Vec pboxes, Vec ptemplates, JS.Callable callback, int numerator, int denominator, Res resourceRoot) {
170 int original_numerator = numerator;
172 if (pboxes == null) {
174 ptemplates = new Vec();
177 if (id != null && !id.equals(""))
178 for(int i=0; i<pboxes.size(); i++) {
179 Box parent = (Box)pboxes.elementAt(i);
180 String parentNodeName = (String)ptemplates.elementAt(i);
181 parent.put("$" + id, b);
184 if (script != null || (redirect != null && !"self".equals(redirect))) {
185 pboxes.addElement(b);
186 ptemplates.addElement(nodeName);
189 int numids = pboxes.size();
193 for(int i=0; _preapply != null && i<_preapply.length; i++)
194 if (_preapply[i] != null) {
195 _preapply[i].apply(b, null, null, callback, numerator, denominator, resourceRoot);
196 numerator += _preapply[i].numUnits();
199 for (int i=0; children != null && i<children.length; i++) {
200 Box newkid = new Box();
201 children[i].apply(newkid, pboxes, ptemplates, callback, numerator, denominator, resourceRoot);
202 b.put(Integer.MAX_VALUE, newkid);
203 numerator += children[i].numUnits();
206 // whom to redirect to; doesn't take effect until after script runs
208 if (redirect != null && !"self".equals(redirect)) redir = (Box)b.get("$" + redirect);
210 if (script != null) try {
211 script.call(new JS.Array(), new PerInstantiationScope(b, resourceRoot));
213 if (Log.on) Log.log(this, "WARNING: uncaught ecmascript exception: " + e.getMessage());
216 for(int i=0; keys != null && i<keys.length; i++) {
218 if (keys[i] == null) { }
219 else if (keys[i].equals("border") || keys[i].equals("image") &&
220 !vals[i].toString().startsWith("http://") && !vals[i].toString().startsWith("https://")) {
221 String s = Resources.resolve(vals[i].toString() + ".png", importlist);
222 if (s != null) b.put(keys[i], s.substring(0, s.length() - 4));
223 else if (Log.on) Log.log(this, "unable to resolve image " + vals[i].toString() + " referenced in attributes of " + nodeName);
225 else b.put(keys[i], vals[i]);
227 if(Log.on) Log.log(this,"WARNING: uncaught ecmascript exception while putting attr \"" + keys[i] +
228 "\" of " + nodeName + " : " + e.getMessage());
232 if (redirect != null && !"self".equals(redirect)) b.redirect = redir;
234 for(int i=0; _postapply != null && i<_postapply.length; i++)
235 if (_postapply[i] != null) {
236 _postapply[i].apply(b, null, null, callback, numerator, denominator, resourceRoot);
237 numerator += _postapply[i].numUnits();
240 pboxes.setSize(numids);
241 ptemplates.setSize(numids);
243 numerator = original_numerator + numUnits();
245 if (callback != null)
247 JS.Array args = new JS.Array();
248 args.addElement(new Double(numerator));
249 args.addElement(new Double(denominator));
252 if (Log.on) Log.log(this, "WARNING: uncaught ecmascript exception: " + e);
255 if (Thread.currentThread() instanceof ThreadMessage) try {
258 if (Log.on) Log.log(this, "WARNING: uncaught ecmascript exception: " + e);
263 // Theming Logic ////////////////////////////////////////////////////////////
265 /** helper method to recursively gather up the list of keys to be preserved */
266 private void gatherPreserves(Vec v) {
267 for(int i=0; preserve != null && i<preserve.length; i++) v.addElement(preserve[i]);
268 for(int i=0; _preapply != null && i<_preapply.length; i++) if (_preapply[i] != null) _preapply[i].gatherPreserves(v);
269 for(int i=0; _postapply != null && i<_postapply.length; i++) if (_postapply[i] != null) _postapply[i].gatherPreserves(v);
272 /** runs statics, resolves string references to other templates into actual Template instance references, and sets <tt>change</tt> as needed */
273 void link() { link(false); }
275 /** same as link(), except that with a true value, it will force a re-link */
276 private void link(boolean force) {
278 if (staticscript != null) try {
279 JS.Scope s = Static.createStatic(nodeName, false);
280 if (staticscript != null) {
281 JS.CompiledFunction temp = staticscript;
284 // we layer a transparent scope over the Static so that we can catch requests for the xwt object
285 // yet not screw up paths that include a package called xwt (ie xwt.static.org.xwt.foo)
286 JS.Scope varScope = new JS.Scope(s) {
287 public boolean isTransparent() { return true; }
288 public Object get(Object key) {
289 if ("xwt".equals(key)) return XWT.singleton; else return super.get(key);
292 temp.call(new JS.Array(), varScope);
295 if (Log.on) Log.log(this, "WARNING: uncaught ecmascript exception: " + e.getMessage());
298 if (!(force || (preapply != null && _preapply == null) || (postapply != null && _postapply == null))) return;
300 if (preapply != null) {
301 if (_preapply == null) _preapply = new Template[preapply.length];
302 for(int i=0; i<_preapply.length; i++) {
303 Template t = getTemplate(preapply[i], importlist);
304 if (t != _preapply[i]) changed = true;
308 if (postapply != null) {
309 if (_postapply == null) _postapply = new Template[postapply.length];
310 for(int i=0; i<_postapply.length; i++) {
311 Template t = getTemplate(postapply[i], importlist);
312 if (t != _postapply[i]) changed = true;
317 for(int i=0; children != null && i<children.length; i++) children[i].link(force);
321 // XML Parsing /////////////////////////////////////////////////////////////////
323 /** handles XML parsing; builds a Template tree as it goes */
324 static final class TemplateHelper extends XML {
328 /** parse an XML input stream, building a Template tree off of <tt>root</tt> */
329 void parseit(InputStream is, Template root) throws XML.XMLException, IOException {
330 rootNodeHasBeenEncountered = false;
331 templateNodeHasBeenEncountered = false;
332 staticNodeHasBeenEncountered = false;
333 templateNodeHasBeenFinished = false;
334 nameOfHeaderNodeBeingProcessed = null;
336 nodeStack.setSize(0);
337 importlist.setSize(0);
339 postapply.setSize(0);
341 importlist.fromArray(defaultImportList);
344 parse(new InputStreamReader(is));
347 /** parsing state: true iff we have already encountered the <xwt> open-tag */
348 boolean rootNodeHasBeenEncountered = false;
350 /** parsing state: true iff we have already encountered the <template> open-tag */
351 boolean templateNodeHasBeenEncountered = false;
353 /** parsing state: true iff we have already encountered the <static> open-tag */
354 boolean staticNodeHasBeenEncountered = false;
356 /** parsing state: true iff we have already encountered the <template> close-tag */
357 boolean templateNodeHasBeenFinished = false;
359 /** parsing state: If we have encountered the open tag of a header node, but not the close tag, this is the name of
360 * that tag; otherwise, it is null. */
361 String nameOfHeaderNodeBeingProcessed = null;
363 /** stack of Templates whose XML elements we have seen open-tags for but not close-tags */
364 Vec nodeStack = new Vec();
366 /** builds up the list of imports */
367 Vec importlist = new Vec();
369 /** builds up the list of preapplies */
370 Vec preapply = new Vec();
372 /** builds up the list of postapplies */
373 Vec postapply = new Vec();
375 /** the template we're currently working on */
378 public void startElement(XML.Element c) throws XML.SchemaException {
379 if (templateNodeHasBeenFinished) {
380 throw new XML.SchemaException("no elements may appear after the <template> node");
382 } else if (!rootNodeHasBeenEncountered) {
383 if (!"xwt".equals(c.localName)) throw new XML.SchemaException("root element was not <xwt>");
384 if (c.len != 0) throw new XML.SchemaException("root element must not have attributes");
385 rootNodeHasBeenEncountered = true;
388 } else if (!templateNodeHasBeenEncountered) {
389 if (nameOfHeaderNodeBeingProcessed != null) throw new XML.SchemaException("can't nest header nodes");
390 nameOfHeaderNodeBeingProcessed = c.localName;
392 if (c.localName.equals("import")) {
393 if (c.len != 1 || !c.keys[0].equals("name"))
394 throw new XML.SchemaException("<import> node must have exactly one attribute, which must be called 'name'");
395 String importpackage = c.vals[0].toString();
396 if (importpackage.endsWith(".*")) importpackage = importpackage.substring(0, importpackage.length() - 2);
397 importlist.addElement(importpackage);
400 } else if (c.localName.equals("redirect")) {
401 if (c.len != 1 || !c.keys[0].equals("target"))
402 throw new XML.SchemaException("<redirect> node must have exactly one attribute, which must be called 'target'");
403 if (t.redirect != null)
404 throw new XML.SchemaException("the <redirect> header element may not appear more than once");
405 t.redirect = c.vals[0].toString();
406 if(t.redirect.equals("null")) t.redirect = null;
409 } else if (c.localName.equals("preapply")) {
410 if (c.len != 1 || !c.keys[0].equals("name"))
411 throw new XML.SchemaException("<preapply> node must have exactly one attribute, which must be called 'name'");
412 preapply.addElement(c.vals[0]);
415 } else if (c.localName.equals("postapply")) {
416 if (c.len != 1 || !c.keys[0].equals("name"))
417 throw new XML.SchemaException("<postapply> node must have exactly one attribute, which must be called 'name'");
418 postapply.addElement(c.vals[0]);
421 } else if (c.localName.equals("static")) {
422 if (staticNodeHasBeenEncountered)
423 throw new XML.SchemaException("the <static> header node may not appear more than once");
425 throw new XML.SchemaException("the <static> node may not have attributes");
426 staticNodeHasBeenEncountered = true;
429 } else if (c.localName.equals("preserve")) {
430 if (c.len != 1 || !c.keys[0].equals("attributes"))
431 throw new XML.SchemaException("<preserve> node must have exactly one attribute, which must be called 'attributes'");
432 if (t.preserve != null)
433 throw new XML.SchemaException("<preserve> header element may not appear more than once");
435 StringTokenizer tok = new StringTokenizer(c.vals[0].toString(), ",", false);
436 t.preserve = new String[tok.countTokens()];
437 for(int i=0; i<t.preserve.length; i++) t.preserve[i] = tok.nextToken();
440 } else if (c.localName.equals("template")) {
441 // finalize importlist/preapply/postapply, since they can't change from here on
442 t.startLine = getLine();
443 importlist.toArray(t.importlist = new String[importlist.size()]);
444 if (preapply.size() > 0) preapply.copyInto(t.preapply = new String[preapply.size()]);
445 if (postapply.size() > 0) postapply.copyInto(t.postapply = new String[postapply.size()]);
446 importlist.setSize(0); preapply.setSize(0); postapply.setSize(0);
447 templateNodeHasBeenEncountered = true;
450 throw new XML.SchemaException("unrecognized header node \"" + c.localName + "\"");
456 // push the last node we were in onto the stack
457 nodeStack.addElement(t);
459 // instantiate a new node, and set its nodeName/importlist/preapply
460 Template t2 = new Template(t.nodeName + "." + t.childvect.size());
461 t2.importlist = t.importlist;
462 t2.startLine = getLine();
463 if (!c.localName.equals("box")) t2.preapply = new String[] { c.localName };
465 // make the new node the current node
470 // TODO: Sort contents straight from one array to another
471 t.keys = new String[c.len];
472 t.vals = new Object[c.len];
473 System.arraycopy(c.keys, 0, t.keys, 0, c.len);
474 System.arraycopy(c.vals, 0, t.vals, 0, c.len);
475 quickSortAttributes(0, t.keys.length - 1);
477 for(int i=0; i<t.keys.length; i++) {
478 if (t.keys[i].equals("id")) {
479 t.id = t.vals[i].toString().intern();
484 t.keys[i] = t.keys[i].intern();
486 String valString = t.vals[i].toString();
488 if (valString.equals("true")) t.vals[i] = Boolean.TRUE;
489 else if (valString.equals("false")) t.vals[i] = Boolean.FALSE;
490 else if (valString.equals("null")) t.vals[i] = null;
492 boolean hasNonNumeral = false;
493 boolean periodUsed = false;
494 for(int j=0; j<valString.length(); j++)
495 if (j == 0 && valString.charAt(j) == '-') {
496 } else if (valString.charAt(j) == '.' && !periodUsed && j != valString.length() - 1) {
498 } else if (!Character.isDigit(valString.charAt(j))) {
499 hasNonNumeral = true;
502 if (valString.length() > 0 && !hasNonNumeral) t.vals[i] = new Double(valString);
503 else t.vals[i] = valString.intern();
506 // bump thisbox to the front of the pack
507 if (t.keys[i].equals("thisbox")) {
508 t.keys[i] = t.keys[0];
509 t.keys[0] = "thisbox";
510 Object o = t.vals[0];
511 t.vals[0] = t.vals[i];
517 /** simple quicksort, from http://sourceforge.net/snippet/detail.php?type=snippet&id=100240 */
518 private int partitionAttributes(int left, int right) {
520 middle = (left + right) / 2;
521 String s = t.keys[right]; t.keys[right] = t.keys[middle]; t.keys[middle] = s;
522 Object o = t.vals[right]; t.vals[right] = t.vals[middle]; t.vals[middle] = o;
523 for (i = left - 1, j = right; ; ) {
524 while (t.keys[++i].compareTo(t.keys[right]) < 0);
525 while (j > left && t.keys[--j].compareTo(t.keys[right]) > 0);
527 s = t.keys[i]; t.keys[i] = t.keys[j]; t.keys[j] = s;
528 o = t.vals[i]; t.vals[i] = t.vals[j]; t.vals[j] = o;
530 s = t.keys[right]; t.keys[right] = t.keys[i]; t.keys[i] = s;
531 o = t.vals[right]; t.vals[right] = t.vals[i]; t.vals[i] = o;
535 /** simple quicksort, from http://sourceforge.net/snippet/detail.php?type=snippet&id=100240 */
536 private void quickSortAttributes(int left, int right) {
537 if (left >= right) return;
538 int p = partitionAttributes(left, right);
539 quickSortAttributes(left, p - 1);
540 quickSortAttributes(p + 1, right);
543 public void endElement(XML.Element c) throws XML.SchemaException {
544 if (rootNodeHasBeenEncountered && !templateNodeHasBeenEncountered) {
545 if ("static".equals(nameOfHeaderNodeBeingProcessed) && t.content != null) t.staticscript = genscript(true);
546 nameOfHeaderNodeBeingProcessed = null;
548 } else if (templateNodeHasBeenEncountered && !templateNodeHasBeenFinished) {
549 // turn our childvect into a Template[]
550 t.childvect.copyInto(t.children = new Template[t.childvect.size()]);
552 if (t.content != null) t.script = genscript(false);
554 if (nodeStack.size() == 0) {
556 templateNodeHasBeenFinished = true;
559 // add this template as a child of its parent
561 t = (Template)nodeStack.lastElement();
562 nodeStack.setSize(nodeStack.size() - 1);
563 t.childvect.addElement(oldt);
569 private JS.CompiledFunction genscript(boolean isstatic) {
570 JS.CompiledFunction thisscript = null;
572 thisscript = JS.parse(t.nodeName + (isstatic ? "._" : ""), t.content_start, new StringReader(t.content.toString()));
573 } catch (JS.Exn ee) {
574 if (Log.on) Log.log(this, " ERROR: " + ee.getMessage());
576 } catch (IOException ioe) {
577 if (Log.on) Log.log(this, " ERROR: " + ioe.getMessage());
587 public void characters(char[] ch, int start, int length) throws XML.SchemaException {
588 // invoke the no-tab crusade
589 for (int i=0; length >i; i++) if (ch[start+i] == '\t') throw new XML.SchemaException(
590 t.nodeName+ ":" + getLine() + "," + getCol() + ": tabs are not allowed in XWT files");
592 if ("static".equals(nameOfHeaderNodeBeingProcessed) || templateNodeHasBeenEncountered) {
593 if (t.content == null) {
594 t.content_start = getLine();
596 t.content = new StringBuffer();
599 t.content.append(ch, start, length);
602 } else if (nameOfHeaderNodeBeingProcessed != null) {
603 throw new XML.SchemaException("header node <" + nameOfHeaderNodeBeingProcessed + "> cannot have text content");
607 public void whitespace(char[] ch, int start, int length) throws XML.SchemaException {
611 private static class PerInstantiationScope extends JS.Scope {
612 Res resourceRoot = null;
613 public PerInstantiationScope(Scope parentScope, Res resourceRoot) {
615 this.resourceRoot = resourceRoot;
617 public boolean isTransparent() { return true; }
618 public boolean has(Object key) { return false; }
619 public void declare(String s) { super.declare(s); }
620 public Object get(Object key) {
621 // FIXME: access statics here
622 if (Box.SpecialBoxProperty.specialBoxProperties.get(key) == null &&
624 Object ret = resourceRoot.get(key);
625 if (ret != null) return ret;
626 throw new JS.Exn("must declare " + key + " before using it!");
628 return super.get(key);
630 public void put(Object key, Object val) {
631 // FIXME: access statics here
632 if (Box.SpecialBoxProperty.specialBoxProperties.get(key) == null &&
634 throw new JS.Exn("must declare " + key + " before using it!");