extra comments
[org.ibex.xt-crawshaw.git] / src / java / org / ibex / xt / shell / JSRemote.java
1 package org.ibex.xt.shell;
2
3 import java.io.*;
4 import java.net.*;
5 import java.util.*;
6
7 import org.ibex.js.*;
8 import org.ibex.util.*;
9 import org.ibex.util.Collections;
10
11 import org.ibex.xt.Prevalence;
12 import org.prevayler.*;
13
14 // FIXME: replace Prevayler references with careful Obj.call(Obj o)
15 //        implementation for dynamically changing scopes in rework
16 //        of org.ibex.js.
17 public class JSRemote extends JS {
18     public static final int VERSION = 1;
19
20     private final URL server;
21     private final String path;
22
23     private transient String cookie = null;
24     private transient Keys keys = null;
25
26     public JSRemote(URL s) { server = s; path = null; }
27     public JSRemote(URL s, String p) { server = s; path = p; }
28
29     /** Receives a Request object from a client on <tt>in</tt>
30      *  and writes a response on <tt>out</tt> */
31     public static void receive(Prevayler p, JS root, ObjectInputStream in, ObjectOutputStream out)
32                                throws IOException {
33         out.writeInt(VERSION); if (in.readInt() != VERSION) return;
34
35         Request r;
36         try {
37             Object o = in.readObject();
38             if (o == null) throw new IOException("unexpected null request");
39             if (!(o instanceof Request)) throw new IOException(
40                 "unexpected request object: "+o.getClass().getName());
41             r = (Request)o;
42         } catch (ClassNotFoundException e) {
43             throw new IOException("unexpected class not found: " + e.getMessage());
44         }
45
46         r.execute(p, root, out);
47     }
48
49     /** Sends a request to the server. */
50     protected ObjectInputStream send(Request request) throws IOException {
51         URLConnection c = server.openConnection();
52         ((HttpURLConnection)c).setRequestMethod("POST");
53         c.setDoOutput(true);
54         if (cookie != null) c.setRequestProperty("Cookie", cookie);
55
56         c.connect();
57
58         ObjectOutputStream out = new ObjectOutputStream(c.getOutputStream());
59         out.writeInt(VERSION);
60         out.writeObject(request);
61         out.close();
62
63         String cook = c.getHeaderField("Set-Cookie");
64         if (cook != null && cook.length() > 0) cookie = cook.substring(0, cook.indexOf(';'));
65
66         ObjectInputStream in = new ObjectInputStream(c.getInputStream());
67         int ver = in.readInt();
68         if (ver != VERSION) throw new IOException("server version "+ver+", expected "+VERSION);
69         return in;
70     }
71
72     /** FEATURE: It's questionable as to whether this belongs here. JSRemote shouldn't
73      *  really know anything about prevayler, but that rquires another layer between
74      *  this class and the http layer. Maybe not a bad idea. */
75     public void transaction(final JS t) {
76         Object resp;
77         try {
78             resp = send(new Request(null) {
79                 protected void execute() throws JSExn, IOException {
80                     try {
81                         prevayler.execute(new Prevalence.JSTransaction(t));
82                         out.writeObject(null);
83                     } catch (Exception e) { e.printStackTrace(); out.writeObject(e); }
84                 }
85             }).readObject();
86         } catch (Exception e) {
87             throw new RuntimeException("transaction failed", e);
88         }
89
90         if (resp != null && resp instanceof Exception)
91             throw new RuntimeException("transaction failed", (Exception)resp);
92     }
93
94     public Collection keys() { return keys == null ? keys = new Keys() : keys.update(); }
95
96     public Object get(Object k) throws JSExn {
97         try {
98             ObjectInputStream in = send(new Request(path, k) {
99                 protected void execute() throws JSExn, IOException {
100                     Object o = scope.get(key);
101                     if (o == null)
102                         out.writeObject(null);
103                     else if (o instanceof Number || o instanceof Boolean || o instanceof String)
104                         out.writeObject(o);
105                     else if (o instanceof JS)
106                         out.writeObject(new JSRemote(server, (path == null ? "" : path + '.') + key));
107                     else throw new JSExn("unexpected class type " + o + " ["+o.getClass()+"]");
108                 }
109             });
110             Object o = in.readObject(); in.close();
111
112             if (o == null) return null;
113             else if (o instanceof JS) return new JSImmutable((JS)o);
114             else if (o instanceof Exception) throw (Exception)o;
115             else return o;
116         } catch (JSExn e) { throw e;
117         } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
118     }
119
120     // FIXME: unroll JSRemote if it is a value
121     public Object put(Object k, final Object value) throws JSExn {
122         try { return send(new Request(path, k) {
123             protected void execute() throws JSExn, IOException {
124                 out.writeObject(scope.put(key, value));
125             }
126         }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
127     }
128
129     public Object remove(Object k) throws JSExn {
130         try { return send(new Request(path, k) {
131             protected void execute() throws JSExn, IOException {
132                 out.writeObject(scope.remove(key));
133             }
134         }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
135     }
136
137     public boolean containsKey(Object k) {
138         try { return send(new Request(path, k) {
139             protected void execute() throws JSExn, IOException {
140                 out.writeBoolean(scope.containsKey(key));
141             }
142         }).readBoolean(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
143     }
144
145     public Object call(final Object a0, final Object a1, final Object a2,
146                        final Object[] rest, final int nargs) throws JSExn {
147         try { return send(new Request(path) {
148             protected void execute() throws JSExn, IOException {
149                 out.writeObject(scope.call(a0, a1, a2, rest, nargs));
150             }
151         }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
152     }
153
154     public Object callMethod(final Object m, final Object a0, final Object a1, final Object a2,
155                              final Object[] rest, final int nargs) throws JSExn {
156         try { return send(new Request(path) {
157             protected void execute() throws JSExn, IOException {
158                 out.writeObject(scope.callMethod(m, a0, a1, a2, rest, nargs));
159             }
160         }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
161     }
162
163     // FIXME: map trap functions to server
164
165     public static abstract class Request implements Serializable {
166         protected Prevayler prevayler;
167         protected ObjectOutputStream out;
168         protected JS scope;
169         protected String path;
170         protected Object key;
171
172         public Request(String path) { this.path = path; key = null; }
173         public Request(String path, Object key) {
174             if (key != null && key instanceof String) {
175                 String k = (String)key;
176                 if (k.length() > 0) {
177                     if (k.charAt(0) == '.') k = k.substring(1);
178                     if (k.charAt(k.length() - 1) == '.') k = k.substring(0, k.length() - 1);
179                 }
180                 int pos = k.lastIndexOf('.');
181                 if (pos >= 0) {
182                     if (path == null) path = "";
183                     path += k.substring(0, pos);
184                     k = k.substring(pos + 1);
185                 }
186                 this.key = k;
187             } else this.key = key;
188
189             this.path = path;
190         }
191         public void execute(Prevayler p, JS r, ObjectOutputStream o) throws IOException {
192             prevayler = p;
193             out = o;
194             scope = r;
195             try { 
196                 if (path != null) {
197                     StringTokenizer st = new StringTokenizer(path, ".");
198                     while (st.hasMoreTokens()) {
199                         String s = st.nextToken().trim();
200                         if (s.length() > 0 && !s.equals(".")) {
201                             Object ob = scope.get(s);
202                             if (ob == null || !(ob instanceof JS))
203                                 throw new JSExn("path not found ");
204                             scope = (JS)ob;
205                         }
206                     }
207                 }
208                 execute();
209             } catch(JSExn e) { out.writeObject(e); }
210             prevayler = null;
211             out = null;
212             scope = null;
213         }
214         protected abstract void execute() throws JSExn, IOException;
215     }
216
217     public static class JSImmutable extends JS {
218         private final JS wrapped;
219         public JSImmutable(JS toWrap) { wrapped = toWrap; }
220
221         public Collection keys() throws JSExn {
222             return Collections.unmodifiableCollection(wrapped.keys()); }
223         public Object get(Object key) throws JSExn { return wrapped.get(key); }
224         public boolean containsKey(Object key) { return wrapped.containsKey(key); }
225         public Object call(Object a0, Object a1, Object a2, Object[] r, int n)
226             throws JSExn { return wrapped.call(a0, a1, a2, r, n); }
227
228         public Object callMethod(Object m, Object a0, Object a1, Object a2, Object[] r, int n)
229             throws JSExn { throw new JSExn("immutable JS"); }
230         public Object put(Object k, Object v) throws JSExn {
231             throw new JSExn("immutable JS"); }
232         public Object remove(Object k) throws JSExn {
233             throw new JSExn("immutable JS"); }
234         public void putAndTriggerTraps(Object k, Object v) throws JSExn {
235             throw new JSExn("immutable JS"); }
236         public Object getAndTriggerTraps(Object k) throws JSExn {
237             throw new JSExn("immutable JS"); }
238         protected boolean isTrappable(Object n, boolean i) { return false; }
239     }
240
241     private class Keys extends AbstractCollection implements Serializable {
242         private transient boolean updating = false;
243         private transient final List items =
244             Collections.synchronizedList(new ArrayList());
245         private transient int modCount = 0;
246
247         private int size = -1;
248
249         public int size() { if (size == -1) update(); return size; }
250         public Iterator iterator() { updateList(); return new KeyIt(); }
251
252         private synchronized Keys update() {
253             if (updating) return this;
254             modCount++; items.clear();
255             try {
256                 final ObjectInputStream in = send(new Request(path) {
257                     protected void execute() throws JSExn, IOException {
258                         Collection c = scope.keys(); out.writeInt(c.size());
259                     }
260                 });
261                 size = in.readInt();
262             } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
263  
264             return this;
265         }
266
267         private synchronized Keys updateList() {
268             if (updating) return this;
269             updating = true;
270             modCount++; items.clear();
271             try {
272                 final ObjectInputStream in = send(new Request(path) {
273                     protected void execute() throws JSExn, IOException {
274                         Collection c = scope.keys(); out.writeInt(c.size());
275                         Iterator i;
276                         if (c.size() > 1000) { // FIXME: better way to unload server?
277                             i = c.iterator();
278                         }  else {
279                             List l = new ArrayList(c);
280                             Collections.sort(l);
281                             i = l.listIterator();
282                         }
283                         while (i.hasNext()) out.writeObject(i.next());
284                     }
285                 });
286                 size = in.readInt();
287                 new Thread() { public void run() {
288                     try {
289                         for (int i=0; i < size; i++) items.add(in.readObject());
290                         in.close();
291                     } catch (Exception e) {
292                         size = -1; items.clear();
293                         throw new RuntimeException("JSRemote", e);
294                     } finally {
295                         synchronized (Keys.this) { Keys.this.updating = false; }
296                     }
297                 }}.start();
298             } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
299
300             return this;
301         }
302
303         private class KeyIt implements Iterator {
304             private final int mod;
305             private int pos = 0;
306
307             KeyIt() { mod = modCount; }
308             public boolean hasNext() {
309                 if (modCount != mod) throw new ConcurrentModificationException();
310                 return pos < size;
311             }
312             public Object next() {
313                 if (modCount != mod) throw new ConcurrentModificationException();
314                 while (pos >= items.size()) {
315                     if (pos >= size) throw new NoSuchElementException();
316                     try { Thread.sleep(50); } catch (InterruptedException e) {}
317                 }
318                 return items.get(pos++);
319             }
320             public void remove() { throw new UnsupportedOperationException(); } // FIXME
321         }
322     }
323 }