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