first alpha release
authoradam <adam@megacz.com>
Mon, 27 Sep 2004 07:51:02 +0000 (07:51 +0000)
committeradam <adam@megacz.com>
Mon, 27 Sep 2004 07:51:02 +0000 (07:51 +0000)
darcs-hash:20040927075102-5007d-25cda76e3993229931e6922232874cdd068f41d2.gz

src/org/ibex/xml/JSRewriter.java [deleted file]
src/org/ibex/xt/Node.java [new file with mode: 0644]
src/org/ibex/xt/Prevalence.java [new file with mode: 0644]
src/org/ibex/xt/Servlet.java [moved from src/org/ibex/xml/Servlet.java with 55% similarity]
src/org/ibex/xt/Template.java [new file with mode: 0644]

diff --git a/src/org/ibex/xml/JSRewriter.java b/src/org/ibex/xml/JSRewriter.java
deleted file mode 100644 (file)
index d87afb3..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.ibex.xml;
-import org.ibex.js.*;
-import org.ibex.util.*;
-import org.ibex.io.*;
-import java.io.*;
-import java.net.*;
-import java.util.*;
-
-public class JSRewriter extends XML.Node.Stream {
-
-    private XML.Node.Stream in;
-    private JSScope scope;
-
-    public JSRewriter(XML.Node.Stream in) { this.in = in; this.scope = new JSScope(null); }
-    public JSRewriter(XML.Node.Stream in, JSScope scope) { this.in = in; this.scope = scope; }
-
-    public boolean _next(int level, XML.Node n) {
-        if (!in.next(level, n)) return false;
-        if (n.cdata != null) n.cdata = eval(n.cdata);
-        else for(int i=1; i<n.numattrs; i+=2) n.attrs[i] = eval(n.attrs[i]);
-        return true;
-    }
-
-    private String eval(String s) {
-        if (s == null) return null;
-        StringBuffer ret = new StringBuffer();
-        while(s.indexOf("$[") != -1) {
-            ret.append(s.substring(0, s.indexOf("$[")));
-            s = s.substring(s.indexOf("$[")+2);
-            String s2 = "return (" + s.substring(0, s.indexOf(']')) + ");\n";
-            System.err.println("evaluating " + s2);
-            try {
-                JS js = JS.cloneWithNewParentScope(JS.fromReader("input", 0, new StringReader(s2)), scope);
-                Object r = js.call(null, null, null, null, 0);
-                System.err.println("output was " + r);
-                ret.append(r == null ? "null" : r.toString());
-            } catch (Exception e) {
-                e.printStackTrace();
-                ret.append(e.toString());
-            }
-            s = s.substring(s.indexOf(']') + 1);
-        }
-        ret.append(s);
-        return ret.toString();
-    }
-}
-
-
diff --git a/src/org/ibex/xt/Node.java b/src/org/ibex/xt/Node.java
new file mode 100644 (file)
index 0000000..f4fe0a4
--- /dev/null
@@ -0,0 +1,166 @@
+package org.ibex.xt;
+import org.ibex.js.*;
+import org.ibex.util.*;
+import org.ibex.io.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+public class Node {
+
+    public String   name = null;
+    public String   cdata = null;
+    public int      numattrs = 0;
+    public String[] attrs = null;
+    public String   uri = null;
+    private int     delta = 0;
+
+    public Node() { }
+    public Node(Node n) { copyFrom(n); }
+    public final void clear() { name = null; cdata = null; numattrs = 0; delta = 0; uri = null; }
+    public final void copyFrom(Node n) {
+        name=n.name; cdata=n.cdata; numattrs=n.numattrs; delta=n.delta; uri=n.uri;
+        if (n.attrs == null) { attrs = null; return; }
+        attrs = new String[n.attrs.length]; System.arraycopy(n.attrs, 0, attrs, 0, attrs.length); }
+    public final String attr(String key) {
+        for(int i=0; i<numattrs; i++) if (key.equals(attrs[i*2])) return attrs[i*2+1]; return null; }
+
+    public static abstract class Stream {
+        public static final Stream NULL = new Stream() { public boolean _read(Node n) { return false; } };
+        public static interface Functor { public Node.Stream wrap(Node.Stream in); }
+        public static class ConstantFunctor implements Functor {
+            private final Node.Stream stream;
+            public ConstantFunctor(Node.Stream stream) { this.stream = stream; }
+            public Node.Stream wrap(Node.Stream in) { return stream; }
+        }
+        public static abstract class Filter extends Stream {
+            Stream upstream;
+            Filter() { }
+            public Filter(Stream upstream) { this.upstream = upstream; }
+            public boolean upstreamRead(Node n) { upstream = upstream.simplify(); return upstream.read(n); }
+            public void wrapUpstream(Functor f) { upstream = f.wrap(upstream); }
+            public Filter graft(Functor f, Node n) {
+                upstream = new Graft((upstream instanceof Peekable) ? (Peekable)upstream : new Peekable(upstream), n, f);
+                return this;
+            }
+        }
+
+        public Stream simplify() { return this; }
+        protected abstract boolean _read(Node n);
+        public final boolean read(Node n) { n.clear(); if (!_read(n)) { n.clear(); return false; } return true; }
+
+        public static class Peekable extends Filter {
+            public Peekable(Stream s) { super(s); }
+            private Node pending = null;
+            public boolean peek(Node n) {
+                if (pending == null) {
+                    Node n2 = new Node();
+                    if (!upstreamRead(n2)) return false;
+                    pending = n2;
+                }
+                n.copyFrom(pending);
+                return true;
+            }
+            public boolean _read(Node n) {
+                if (pending != null) { n.copyFrom(pending); pending = null; return true; }
+                return upstreamRead(n);
+            }
+        }
+
+        private static class Graft extends Filter {
+            private Stream inner2;
+            private Peekable a;
+            int total = 0;
+            int net = 0;
+            boolean simple = false;
+            private Node pending = new Node();
+            public Graft(final Peekable a, final Node n, final Functor f) {
+                this.a = a;
+                upstream = inner2 = new Stream() {
+                        public boolean _read(Node n) {
+                            if (!Graft.this.a.peek(n)) return false;
+                            if (net + n.delta <= 0) return false;
+                            Graft.this.a.read(n);
+                            net += n.delta;
+                            return true;
+                        } };
+                wrapUpstream(f);
+                if (__read(pending)) pending.delta = n.delta;
+                total = n.delta;
+            }
+            public boolean _read(Node n) {
+                if (pending != null) { n.copyFrom(pending); pending = null; return true; }
+                boolean ret = __read(n);
+                if (ret) total += n.delta;
+                return ret;
+            }
+            public boolean __read(Node n) {
+                if (simple) return a.read(n);
+                if (upstreamRead(n)) return true;
+                while(inner2.read(n));
+                if (!a.read(n)) return false;
+                n.delta += net - total;
+                simple = true;
+                return true;
+            }
+        }
+
+        public static class FromXML extends Node.Stream {
+            private final XML.Pull xml;
+            private XML.Element parent = null;
+            private XML.Element e;
+            private int currentdelta = 0;
+            public FromXML(Reader r) { this.xml = new XML.Pull(r); }
+            protected boolean _read(Node n) { try {
+                Object ret = xml.read();
+                if (ret == null) return false;
+                if (ret instanceof String) {
+                    n.cdata = (String)ret;
+                    n.delta = xml.level - currentdelta;
+                    currentdelta = xml.level;
+                    return true;
+                }
+                XML.Element e = (XML.Element)ret;
+                n.name = e.getLocalName();
+                n.uri = e.getUri();
+                n.numattrs = e.getAttrLen();
+                n.delta = e.level - currentdelta;
+                currentdelta = e.level;
+                if (n.attrs == null || n.attrs.length < n.numattrs*2) n.attrs = new String[n.numattrs*4];
+                for(int i=0; i<n.numattrs; i++) { n.attrs[i*2]   = e.getAttrKey(i); n.attrs[i*2+1] = e.getAttrVal(i); }
+                return true;
+            } catch (Exception e) { throw new RuntimeException(e); } }
+        }
+
+        public void toXML(Writer writer) throws IOException { Node n = new Node(); if (read(n)) toXML(writer, n); }
+        private Node toXML(Writer w, Node n) throws IOException {
+            final String name = n.name;
+            if (n.cdata != null) {
+                w.write(n.cdata);
+                if (!read(n)) n = null;
+            } else {
+                w.write("<");
+                w.write(name);
+                for(int i=0; i < n.numattrs * 2; i+=2) {
+                    w.write(" ");
+                    w.write(n.attrs[i]);
+                    w.write("=\"");
+                    w.write(n.attrs[i+1]);
+                    w.write("\"");
+                }
+                if (!read(n)) n = null;
+                if (n == null || n.delta <= 0) {
+                    w.write("/>");
+                } else {
+                    w.write(">");
+                    while(n != null && n.delta > 0) n = toXML(w, n);
+                    w.write("</");
+                    w.write(name);
+                    w.write(">");
+                }
+            }
+            if (n != null) n.delta++;
+            return n;
+        }
+    }
+}
diff --git a/src/org/ibex/xt/Prevalence.java b/src/org/ibex/xt/Prevalence.java
new file mode 100644 (file)
index 0000000..ef87147
--- /dev/null
@@ -0,0 +1,89 @@
+package org.ibex.xt;
+import org.ibex.js.*;
+import org.ibex.util.*;
+import org.ibex.io.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.servlet.*;
+import javax.servlet.http.*;
+import com.thoughtworks.xstream.*;
+import org.prevayler.*;
+import org.prevayler.implementation.snapshot.*;
+
+public class Prevalence {
+
+    static final Hashtable prevaylers = new Hashtable();
+
+    public static void destroy(ServletContext cx, Prevayler prevayler) { try {
+        prevaylers.remove(cx);
+        prevayler.takeSnapshot();
+        prevayler.close();
+    } catch (Exception e) { e.printStackTrace(); } }
+
+    private static class SnapshotThread extends Thread {
+        ServletContext cx;
+        public SnapshotThread(ServletContext cx) { this.cx = cx; }
+        public void run() {
+            try {
+                Thread.sleep(10000);
+                Prevayler privatePrevayler = (Prevayler)prevaylers.get(cx);
+                if (privatePrevayler == null) return;
+                privatePrevayler.takeSnapshot();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public static Prevayler getPrevayler(ServletContext cx) {
+        try {
+            Prevayler prevayler;
+            synchronized(cx) {
+                prevayler = (Prevayler)prevaylers.get(cx);
+                if (prevayler == null) {
+                    PrevaylerFactory pf = new PrevaylerFactory();
+                    String base = cx.getRealPath("/") + "WEB-INF" + File.separatorChar + "prevalent";
+                    System.err.println("prevayling to " + base);
+                    pf.configurePrevalenceBase(base);
+                    XStreamSnapshotManager manager = new XStreamSnapshotManager(new JS(), base, null) {
+                            protected XStream createXStream() {
+                                XStream xstream = new XStream();
+                                xstream.alias("js", JS.class);
+                                xstream.alias("jsdate", JSDate.class);
+                                return xstream;
+                            }
+                        };
+                    System.err.println("configuring with " + manager);
+                    pf.configureSnapshotManager(manager);
+                    //pf.configureClassLoader(JSTransaction.class.getClassLoader());
+                    prevayler = pf.create();
+                    prevaylers.put(cx, prevayler);
+                    new SnapshotThread(cx).start();
+                }
+            }
+            return prevayler;
+        } catch (Exception e) { throw new RuntimeException(e); } }
+
+    public static class JSTransaction implements Transaction {
+        public static final long serialVersionUid = 0xfb2aa281;
+        private JS js;
+        public JSTransaction(JS js) { this.js = js; }
+        public void executeOn(Object o, Date now) {
+            try {
+                js.call(o, new JSDate(now.getTime()), null, null, 2);
+            } catch (Exception e) { throw new RuntimeException(e); }
+        }
+    }
+
+    public static class JSQuery implements Query {
+        public static final long serialVersionUid = 0xfb2aa282;
+        private JS js;
+        public JSQuery(JS js) { this.js = js; }
+        public Object query(Object o, Date now) {
+            try {
+                return js.call(o, new JSDate(now.getTime()), null, null, 2);
+            } catch (Exception e) { throw new RuntimeException(e); }
+        }
+    }
+}
similarity index 55%
rename from src/org/ibex/xml/Servlet.java
rename to src/org/ibex/xt/Servlet.java
index 833f8fc..be36ac8 100644 (file)
@@ -1,4 +1,4 @@
-package org.ibex.xml;
+package org.ibex.xt;
 import org.ibex.js.*;
 import org.ibex.util.*;
 import org.ibex.io.*;
@@ -11,42 +11,46 @@ import com.thoughtworks.xstream.*;
 import org.prevayler.*;
 import org.prevayler.implementation.snapshot.*;
 
-
 public class Servlet extends HttpServlet {
 
-    private ServletResolver resolver = new ServletResolver();
-    private class ServletResolver implements XML.Node.Stream.Resolver {
-        public XML.Node.Stream.Functor resolve(String uri) {
-            if (uri.indexOf(':') == -1) throw new RuntimeException("uri does not contain an method: " + uri);
-            String method = uri.substring(0, uri.indexOf(':'));
-            String rest = uri.substring(uri.indexOf(':'))+1;
-            //case "xtree":  return XTree.tag(rest);
-            //#switch(method)
-            case "webinf": return new Template(cx.getRealPath(rest));
-            case "java":   try { return (XML.Node.Stream.Functor)Class.forName(rest).newInstance(); }
-                           catch (Exception e) { throw new RuntimeException(e); }
-            //#end
-            throw new RuntimeException("unknown method " + method);    
+    private ServletScope servletscope = null;
+    private String path;
+    private Prevayler prevayler;
+    private JS prevalent;
+    private ServletContext cx = null;
+
+    public void destroy() { try {
+        synchronized(this.getClass()) {
+            Prevayler privatePrevayler = prevayler;
+            if (prevayler == null) return;
+            prevayler = null;
+            Prevalence.destroy(cx, prevayler);
         }
+    } catch (Exception e) { e.printStackTrace(); } }
+
+    public void init(ServletConfig sc) throws ServletException {
+        cx = sc.getServletContext();
+        prevayler = Prevalence.getPrevayler(cx);
     }
 
+    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { doGet(request, response); }
     public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
-        JSScope scope = new ServletScope(request, response);
-        String path = cx.getRealPath(((HttpServletRequest)request).getServletPath());
+        servletscope = new ServletScope(request, response, cx);
+        path = cx.getRealPath(((HttpServletRequest)request).getServletPath());
         Reader xmlreader = new InputStreamReader(new FileInputStream(path));
-        XML.Node.Stream s = new JSRewriter(XML.Node.Stream.in(xmlreader), scope);
-        s.out(response.getWriter());
+        new Template(servletscope, new JSScope(servletscope), xmlreader).wrap(null).toXML(response.getWriter());
     }
 
-    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { doGet(request, response); }
-
     public class ServletScope extends JSScope {
         HttpServletRequest request;
         HttpServletResponse response;
-        public ServletScope(ServletRequest request, ServletResponse response) {
+        ServletContext cx;
+        public String getRealPath(String s) { return cx.getRealPath(s); }
+        public ServletScope(ServletRequest request, ServletResponse response, ServletContext cx) {
             super(null);
             this.request = (HttpServletRequest)request;
             this.response = (HttpServletResponse)response;
+            this.cx = cx;
         }
         private JS params = new JS() {
                 public Object get(Object key) { return request.getParameter(JS.toString(key)); }
@@ -96,19 +100,21 @@ public class Servlet extends HttpServlet {
             //#switch(method)
             case "prevalent.query":
                 try {
-                    return prevayler.execute(new JSQuery(JS.cloneWithNewParentScope((JS)a, null)));
+                    return prevayler.execute(new Prevalence.JSQuery(JS.cloneWithNewParentScope((JS)a, null)));
                 } catch (Exception e) {
                     e.printStackTrace();
                     throw new RuntimeException(e); }
 
-            case "prevalent.execute":
-                try {
-                    prevayler.execute(new JSTransaction(JS.cloneWithNewParentScope((JS)a, null)));
-                } catch (Exception e) { 
-                    e.printStackTrace();
-                    throw new RuntimeException(e); }
-
             case "session.invalidate":    request.getSession(true).invalidate(); return null;
+            case "context.list":
+                String path = JS.toString(a);
+                if (path.indexOf("..") != -1) throw new JSExn("cannot use .. in paths");
+                File f = new File(cx.getRealPath("/") + File.separatorChar + path);
+                if (!f.isDirectory()) return null;
+                String[] contents = f.list();
+                JSArray ret = new JSArray(contents.length);
+                for(int i=0; i<contents.length; i++) ret.addElement(contents[i]);
+                return ret;
             //#end
             return null;
         }
@@ -116,9 +122,7 @@ public class Servlet extends HttpServlet {
             //#switch(key)
             case "body":
             case "arg":                   return null;
-            case "prevalent":             return getSub("prevalent");
-            case "prevalent.query":       return METHOD;
-            case "prevalent.execute":     return METHOD;
+            case "prevalent":             return prevalent;
             case "request":               return getSub("request");
             case "request.user":          return request.getRemoteUser();
             case "request.header":        return requestHeader;
@@ -135,6 +139,10 @@ public class Servlet extends HttpServlet {
             case "session.created":       return new JSDate(request.getSession(true).getCreationTime());
             case "session.accessed":      return new JSDate(request.getSession(true).getLastAccessedTime());
             case "session.invalidate":    return METHOD;
+            case "page":                  return getSub("page");
+            case "page.lastmodified":     return new JSDate(new File(path).lastModified());
+            case "context":               return getSub("context");
+            case "context.list":          return METHOD;
             case "params":                return params;
             case "cookie":                return cookies;
             //#end
@@ -146,6 +154,9 @@ public class Servlet extends HttpServlet {
             case "response.code":         response.setStatus(JS.toInt(val));
             case "response.redirect":     response.sendRedirect(JS.toString(val));
             case "response.contentType":  response.setContentType(JS.toString(val));
+            case "prevalent":             
+                try { prevayler.execute(new Prevalence.JSTransaction(JS.cloneWithNewParentScope((JS)val, null)));
+                } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); }
             //#end
             } catch (IOException e) {
                 throw new JSExn(e);
@@ -153,92 +164,4 @@ public class Servlet extends HttpServlet {
         }
     }
 
-    // Prevalence //////////////////////////////////////////////////////////////////////////////
-    
-    static final Hashtable prevaylers = new Hashtable();
-    private Prevayler prevayler;
-    private JS prevalent;
-    private ServletContext cx = null;
-    public void destroy() {
-        try {
-            synchronized(this.getClass()) {
-                Prevayler privatePrevayler = prevayler;
-                if (prevayler == null) return;
-                prevayler = null;
-                prevaylers.remove(cx);
-                prevayler.takeSnapshot();
-                prevayler.close();
-            }
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-    }
-
-    private static class SnapshotThread extends Thread {
-        ServletContext cx;
-        public SnapshotThread(ServletContext cx) { this.cx = cx; }
-        public void run() {
-            try {
-                Thread.sleep(10000);
-                Prevayler privatePrevayler = (Prevayler)prevaylers.get(cx);
-                if (privatePrevayler == null) return;
-                privatePrevayler.takeSnapshot();
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
-        }
-    }
-
-    public void init(ServletConfig sc) throws ServletException {
-        try {
-            cx = sc.getServletContext();
-            synchronized(cx) {
-                prevayler = (Prevayler)prevaylers.get(cx);
-                if (prevalent == null) {
-                    PrevaylerFactory pf = new PrevaylerFactory();
-                    String base = cx.getRealPath("/") + "WEB-INF" + File.separatorChar + "prevalent";
-                    System.err.println("prevayling to " + base);
-                    pf.configurePrevalenceBase(base);
-                    XStreamSnapshotManager manager = new XStreamSnapshotManager(new JS(), base, null) {
-                            protected XStream createXStream() {
-                                XStream xstream = new XStream();
-                                xstream.alias("js", JS.class);
-                                xstream.alias("jsdate", JSDate.class);
-                                return xstream;
-                            }
-                        };
-                    System.err.println("configuring with " + manager);
-                    pf.configureSnapshotManager(manager);
-                    //pf.configureClassLoader(JSTransaction.class.getClassLoader());
-                    prevayler = pf.create();
-                    prevaylers.put(cx, prevayler);
-                    new SnapshotThread(cx).start();
-                }
-            }
-            prevalent = (JS)prevayler.prevalentSystem();
-        } catch (Exception e) {
-            throw new ServletException(e);
-        }
-    }
-
-    public static class JSTransaction implements Transaction {
-        private JS js;
-        public JSTransaction(JS js) { this.js = js; }
-        public void executeOn(Object o, Date now) {
-            try {
-                js.call(o, new JSDate(now.getTime()), null, null, 2);
-            } catch (Exception e) { throw new RuntimeException(e); }
-        }
-    }
-
-    public static class JSQuery implements Query {
-        private JS js;
-        public JSQuery(JS js) { this.js = js; }
-        public Object query(Object o, Date now) {
-            try {
-                return js.call(o, new JSDate(now.getTime()), null, null, 2);
-            } catch (Exception e) { throw new RuntimeException(e); }
-        }
-    }
-
 }
diff --git a/src/org/ibex/xt/Template.java b/src/org/ibex/xt/Template.java
new file mode 100644 (file)
index 0000000..f3955f2
--- /dev/null
@@ -0,0 +1,174 @@
+package org.ibex.xt;
+import org.ibex.js.*;
+import org.ibex.util.*;
+import org.ibex.io.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.servlet.*;
+import javax.servlet.http.*;
+
+public class Template extends Node.Stream.Filter implements Node.Stream.Functor {
+
+    static Template newTemplate(Servlet.ServletScope servletscope, JSScope scope, String str) {
+        try {
+            File f = new File(str);
+            if (!f.exists()) f = new File(str + ".xt");
+            if (!f.exists()) f = new File(str + ".xml");
+            return new Template(servletscope, scope, new InputStreamReader(new FileInputStream(f)));
+        } catch (Exception e) { throw new RuntimeException(e); }
+    }
+
+    static JSScope copyNodeToScope(Node n, JSScope scope) {
+        try {
+            for(int i=0; i<n.numattrs; i++) {
+                scope.declare(n.attrs[i*2]);
+                scope.put(n.attrs[i*2], n.attrs[i*2+1]);
+            }
+            return scope;
+        } catch (Exception e) { throw new RuntimeException(e); }
+    }
+
+    private class JSRewriter extends Node.Stream {
+        private Node.Stream in;
+        private JSScope scope;
+        public JSRewriter(Node.Stream in, JSScope scope) { this.in = in; this.scope = scope; }
+        protected boolean _read(Node n) { if (!in.read(n)) return false; transform(n, scope); return true; }
+    }
+
+    public static Node transform(Node n, JSScope scope) {
+        if (n.cdata != null) n.cdata = eval(n.cdata, scope).toString();
+        else for(int i=1; i<n.numattrs*2; i+=2) n.attrs[i] = eval(n.attrs[i], scope).toString();
+        return n;
+    }
+
+    private static Object eval(String s, JSScope scope) {
+        if (s == null) return null;
+        StringBuffer ret = new StringBuffer();
+        for(boolean first = true; s.indexOf("${") != -1; first = false) {
+            ret.append(s.substring(0, s.indexOf("${")));
+            String s2 = s.substring(s.indexOf("${")+2);
+            Object app = exec("return (" + s2.substring(0, s2.indexOf('}')) + ");\n", scope);
+            s = s.substring(s.indexOf('}') + 1);
+            //if (first && s.trim().length() == 0) return app;
+            if (!(app == null || app instanceof String || app instanceof Number || app instanceof Boolean))
+                throw new RuntimeException("javascripts within ${...} can only return strings, numbers, and booleans; not a " +
+                                           app.getClass().getName());
+            ret.append(app == null ? "null" : app.toString());
+        }
+        ret.append(s);
+        return ret.toString();
+    }
+
+    public static Object exec(String s, JSScope scope) {
+        try {
+            return JS.eval(JS.cloneWithNewParentScope(JS.fromReader("input", 0, new StringReader(s)), scope));
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new RuntimeException(e);
+        }
+    }
+
+    private JSScope scope;
+    private Servlet.ServletScope servletscope;
+    private Node.Stream children;
+    public Node.Stream wrap(Node.Stream children) { this.children = children; return this; }
+    public Template(Servlet.ServletScope servletscope, JSScope scope, Reader template) {
+        super(new Node.Stream.FromXML(template));
+        this.scope = scope;
+        this.servletscope = servletscope;
+    }
+    public boolean _read(Node n) { boolean ret = __read(n); if (ret) transform(n, scope); return ret; }
+    public boolean __read(final Node n) {
+        if (!upstreamRead(n)) return false;
+        if (n.cdata != null) return true;
+        final String uri = n.uri;
+        final String name = n.name;
+        if (uri.indexOf(':') == -1)
+            throw new RuntimeException("uri does not contain a colon: " + uri + " (tag name " + name + ")");
+        final String method = uri.substring(0, uri.indexOf(':'));
+        final String rest = uri.substring(uri.indexOf(':')+1);
+        if (uri.equals("http://www.w3.org/1999/xhtml")) { return true;
+        } else if (method.equals("webinf")) {
+            return graft(newTemplate(servletscope, copyNodeToScope(transform(n, scope), new JSScope(servletscope)),
+                                     servletscope.getRealPath("/") + "/WEB-INF/" + rest + name), n).upstreamRead(n);
+        } else if (uri.equals("http://xt.ibex.org/")) {
+            //#switch(name)
+            case "if":       
+                transform(n, scope);
+                return graft("true".equals(n.attr("if")) ? new DropTag() : new DropAll(), n).upstreamRead(n);
+            case "js":       return graft(new JsTag(scope), n).upstreamRead(n);
+            case "foreach":  return graft(new ForEach(n, scope), n).upstreamRead(n);
+            case "children":
+                if (children == null) return true;
+                graft(new Node.Stream.ConstantFunctor(children), n);
+                children = null;
+                return upstreamRead(n);
+                //#end
+                return true;
+        } else if (method.equals("java")) {
+            try { return graft((Node.Stream.Functor)Class.forName(rest).newInstance(), n).upstreamRead(n); }
+            catch (Exception e) { throw new RuntimeException(e); }
+        }
+        throw new RuntimeException("Unknown namespace URI " + uri);    
+    }
+
+    private class ForEach extends Node.Stream.Filter implements Node.Stream.Functor {
+        private Node[] nodes = null;
+        private Vec array = new Vec();
+        private JSScope scope;
+        public ForEach(Node n, JSScope s) {
+            super(Node.Stream.NULL);
+            Vec v = ((JSArray)exec("return (" + n.attr("in").toString() + ");", this.scope = s)).toVec();
+            while(true) { Object o = v.pop(); if (o == null) break; array.push(o); }
+        }
+        public Node.Stream wrap(Node.Stream kids) {
+            Vec nodes = new Vec();
+            Node n2 = new Node();
+            while(kids.read(n2)) nodes.addElement(new Node(n2));
+            nodes.copyInto(this.nodes = new Node[nodes.size()]);
+            return this;
+        }
+        protected boolean _read(Node n) {
+            if (upstreamRead(n)) return true;
+            if (array.size() == 0) return false;
+            JSScope scope2 = new JSScope(scope);
+            try { scope2.declare("x"); scope2.put("x", array.pop()); } catch (JSExn e) { throw new RuntimeException(e); }
+            return graft(new ConstantFunctor(new JSRewriter(new Node.Stream() {
+                    private int i = 0;
+                    protected boolean _read(Node n) {
+                        if (i>=nodes.length) return false;
+                        n.copyFrom(nodes[i++]);
+                        return true;
+                    } }, scope2)), n).upstreamRead(n);
+        }
+    }
+
+    private class DropTag implements Node.Stream.Functor {
+        public Node.Stream wrap(Node.Stream kids) {
+            return kids;
+        } }
+
+    private class DropAll implements Node.Stream.Functor {
+        public Node.Stream wrap(Node.Stream kids) {
+            return new Node.Stream() { public boolean _read(Node n) { return false; } };
+        } }
+
+    private class JsTag implements Node.Stream.Functor {
+        JSScope scope;
+        public JsTag(JSScope scope) { this.scope = scope; }
+        public Node.Stream wrap(final Node.Stream s) {
+            return new Node.Stream() {
+                    protected boolean _read(Node n) {
+                        boolean ret = s.read(n);
+                        if (ret && n.cdata != null) {
+                            System.err.println("exec("+n.cdata+")");
+                            exec(n.cdata, scope);
+                            return _read(n);
+                        }
+                        return ret;
+                    }
+                };
+        }
+    }
+}