new xt shell based on JS transparency layer
[org.ibex.xt-crawshaw.git] / src / java / org / ibex / xt / shell / JSRemote.java
diff --git a/src/java/org/ibex/xt/shell/JSRemote.java b/src/java/org/ibex/xt/shell/JSRemote.java
new file mode 100644 (file)
index 0000000..f7886d8
--- /dev/null
@@ -0,0 +1,244 @@
+package org.ibex.xt.shell;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+import org.ibex.js.*;
+import org.ibex.util.*;
+import org.ibex.util.Collections;
+
+public class JSRemote extends JS {
+    public static final int VERSION = 1;
+
+    private final URL server;
+    private final String path;
+
+    private transient String cookie = null;
+    private transient Keys keys = null;
+
+    public JSRemote(URL s) { server = s; path = null; }
+    public JSRemote(URL s, String p) { server = s; path = p; }
+
+    /** Receives a Request object from a client on <tt>in</tt>
+     *  and writes a response on <tt>out</tt> */
+    public static void receive(JS root, ObjectInputStream in, ObjectOutputStream out)
+                               throws IOException {
+        out.writeInt(VERSION); if (in.readInt() != VERSION) return;
+
+        Request r;
+        try {
+            Object o = in.readObject();
+            if (o == null) throw new IOException("unexpected null request");
+            if (!(o instanceof Request)) throw new IOException(
+                "unexpected request object: "+o.getClass().getName());
+            r = (Request)o;
+        } catch (ClassNotFoundException e) {
+            throw new IOException("unexpected class not found: " + e.getMessage());
+        }
+
+        r.execute(root, out);
+    }
+
+    /** Sends a request to the server. */
+    protected ObjectInputStream send(Request request) throws IOException {
+        URLConnection c = server.openConnection();
+        ((HttpURLConnection)c).setRequestMethod("POST");
+        c.setDoOutput(true);
+        if (cookie != null) c.setRequestProperty("Cookie", cookie);
+
+        c.connect();
+
+        ObjectOutputStream out = new ObjectOutputStream(c.getOutputStream());
+        out.writeInt(VERSION);
+        out.writeObject(request);
+        out.close();
+
+        String cook = c.getHeaderField("Set-Cookie");
+        if (cook != null && cook.length() > 0) cookie = cook.substring(0, cook.indexOf(';'));
+
+        ObjectInputStream in = new ObjectInputStream(c.getInputStream());
+        int ver = in.readInt();
+        if (ver != VERSION) throw new IOException("server version "+ver+", expected "+VERSION);
+        return in;
+    }
+
+    public Collection keys() { return keys == null ? keys = new Keys() : keys.update(); }
+
+    public Object get(Object k) throws JSExn {
+        try {
+            ObjectInputStream in = send(new Request(path, k) {
+                protected void execute() throws JSExn, IOException {
+                    Object o = scope.get(key);
+                    if (o == null)
+                        out.writeObject(null);
+                    else if (o instanceof Number || o instanceof Boolean || o instanceof String)
+                        out.writeObject(o);
+                    else if (o instanceof JS)
+                        out.writeObject(new JSRemote(server, (path == null ? "" : path + '.') + key));
+                    else throw new JSExn("unexpected class type " + o + " ["+o.getClass()+"]");
+                }
+            });
+            Object o = in.readObject(); in.close();
+
+            if (o == null) return null;
+            else if (o instanceof Exception) throw (Exception)o;
+            else return o;
+        } catch (JSExn e) { throw e;
+        } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
+    }
+
+    // FIXME: unroll JSRemote if it is a value
+    public void put(Object k, final Object value) throws JSExn {
+        try { send(new Request(path, k) {
+            protected void execute() throws JSExn, IOException {
+                scope.put(key, value);
+            }
+        }); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
+    }
+
+    public Object remove(Object k) throws JSExn {
+        try { return send(new Request(path, k) {
+            protected void execute() throws JSExn, IOException {
+                out.writeObject(scope.remove(key));
+            }
+        }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
+    }
+
+    public boolean containsKey(Object k) throws JSExn {
+        try { return send(new Request(path, k) {
+            protected void execute() throws JSExn, IOException {
+                out.writeBoolean(scope.containsKey(key));
+            }
+        }).readBoolean(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
+    }
+
+    public Object call(final Object a0, final Object a1, final Object a2,
+                       final Object[] rest, final int nargs) throws JSExn {
+        try { return send(new Request(path) {
+            protected void execute() throws JSExn, IOException {
+                out.writeObject(scope.call(a0, a1, a2, rest, nargs));
+            }
+        }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
+    }
+
+    public Object callMethod(final Object m, final Object a0, final Object a1, final Object a2,
+                             final Object[] rest, final int nargs) throws JSExn {
+        try { return send(new Request(path) {
+            protected void execute() throws JSExn, IOException {
+                out.writeObject(scope.callMethod(m, a0, a1, a2, rest, nargs));
+            }
+        }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
+    }
+
+    // FIXME: map trap functions to server
+
+    public static abstract class Request implements Serializable {
+        protected ObjectOutputStream out;
+        protected JS scope;
+        protected String path;
+        protected Object key;
+
+        public Request(String path) { this.path = path; key = null; }
+        public Request(String path, Object key) {
+            if (key != null && key instanceof String) {
+                String k = (String)key;
+                if (k.length() > 0) {
+                    if (k.charAt(0) == '.') k = k.substring(1);
+                    if (k.charAt(k.length() - 1) == '.') k = k.substring(0, k.length() - 1);
+                }
+                int pos = k.lastIndexOf('.');
+                if (pos >= 0) {
+                    path += k.substring(0, pos);
+                    k = k.substring(pos + 1);
+                }
+                this.key = k;
+            } else this.key = key;
+
+            this.path = path;
+        }
+        public void execute(JS r, ObjectOutputStream o) throws IOException {
+            out = o;
+            scope = r;
+            try { 
+                if (path != null) {
+                    StringTokenizer st = new StringTokenizer(path, ".");
+                    while (st.hasMoreTokens()) {
+                        String s = st.nextToken().trim();
+                        if (s.length() > 0 && !s.equals(".")) {
+                            Object ob = scope.get(s);
+                            if (ob == null || !(ob instanceof JS))
+                                throw new JSExn("path not found");
+                            scope = (JS)ob;
+                        }
+                    }
+                }
+                execute();
+            } catch(JSExn e) { out.writeObject(e); }
+            out = null;
+            scope = null;
+        }
+        protected abstract void execute() throws JSExn, IOException;
+    }
+
+    private class Keys extends AbstractCollection implements Serializable {
+        private transient final List items =
+            Collections.synchronizedList(new ArrayList());
+        private int modCount = 0, size = -1;
+
+        public Iterator iterator() { update(); return new KeyIt(); }
+        public int size() { if (size == -1) update(); return size; }
+
+        private synchronized Keys update() {
+            modCount++; items.clear();
+            try {
+                final ObjectInputStream in = send(new Request(path) {
+                    protected void execute() throws JSExn, IOException {
+                        Collection c = scope.keys(); out.writeInt(c.size());
+                        Iterator i;
+                        if (c.size() > 1000) { // FIXME: better way to unload server?
+                            i = c.iterator();
+                        }  else {
+                            List l = new ArrayList(c);
+                            Collections.sort(l);
+                            i = l.listIterator();
+                        }
+                        while (i.hasNext()) out.writeObject(i.next());
+                    }
+                });
+                size = in.readInt();
+                new Thread() { public void run() {
+                    try {
+                        for (int i=0; i < size; i++) items.add(in.readObject());
+                        in.close();
+                    } catch (Exception e) {
+                        size = -1; items.clear();
+                        throw new RuntimeException("JSRemote", e);
+                    }
+                }}.start();
+            } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
+
+            return this;
+        }
+
+        private class KeyIt implements Iterator {
+            private final int mod;
+            private int pos = 0;
+
+            KeyIt() { mod = modCount; }
+            public boolean hasNext() {
+                if (modCount != mod) throw new ConcurrentModificationException();
+                return pos < size;
+            }
+            public Object next() {
+                if (modCount != mod) throw new ConcurrentModificationException();
+                while (pos >= items.size()) {
+                    if (pos >= size) throw new NoSuchElementException();
+                    try { Thread.sleep(50); } catch (InterruptedException e) {}
+                }
+                return items.get(pos++);
+            }
+            public void remove() { throw new UnsupportedOperationException(); } // FIXME
+        }
+    }
+}