1 package org.ibex.xt.shell;
8 import org.ibex.util.*;
9 import org.ibex.util.Collections;
11 import org.ibex.xt.Prevalence;
12 import org.prevayler.*;
14 // FIXME: replace Prevayler references with careful Obj.call(Obj o)
15 // implementation for dynamically changing scopes in rework
17 public class JSRemote extends JS {
18 public static final int VERSION = 1;
20 private final URL server;
21 private final String path;
23 private transient String cookie = null;
24 private transient Keys keys = null;
26 public JSRemote(URL s) { server = s; path = null; }
27 public JSRemote(URL s, String p) { server = s; path = p; }
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)
33 out.writeInt(VERSION); if (in.readInt() != VERSION) return;
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());
42 } catch (ClassNotFoundException e) {
43 throw new IOException("unexpected class not found: " + e.getMessage());
46 r.execute(p, root, out);
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");
54 if (cookie != null) c.setRequestProperty("Cookie", cookie);
58 ObjectOutputStream out = new ObjectOutputStream(c.getOutputStream());
59 out.writeInt(VERSION);
60 out.writeObject(request);
63 String cook = c.getHeaderField("Set-Cookie");
64 if (cook != null && cook.length() > 0) cookie = cook.substring(0, cook.indexOf(';'));
66 ObjectInputStream in = new ObjectInputStream(c.getInputStream());
67 int ver = in.readInt();
68 if (ver != VERSION) throw new IOException("server version "+ver+", expected "+VERSION);
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) {
78 resp = send(new Request(null) {
79 protected void execute() throws JSExn, IOException {
81 prevayler.execute(new Prevalence.JSTransaction(t));
82 out.writeObject(null);
83 } catch (Exception e) { e.printStackTrace(); out.writeObject(e); }
86 } catch (Exception e) {
87 throw new RuntimeException("transaction failed", e);
90 if (resp != null && resp instanceof Exception)
91 throw new RuntimeException("transaction failed", (Exception)resp);
94 public Collection keys() { return keys == null ? keys = new Keys() : keys.update(); }
96 public Object get(Object k) throws JSExn {
98 ObjectInputStream in = send(new Request(path, k) {
99 protected void execute() throws JSExn, IOException {
100 Object o = scope.get(key);
102 out.writeObject(null);
103 else if (o instanceof Number || o instanceof Boolean || o instanceof String)
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()+"]");
110 Object o = in.readObject(); in.close();
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;
116 } catch (JSExn e) { throw e;
117 } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
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));
126 }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
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));
134 }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
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));
142 }).readBoolean(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
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));
151 }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
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));
160 }).readObject(); } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
163 // FIXME: map trap functions to server
165 public static abstract class Request implements Serializable {
166 protected Prevayler prevayler;
167 protected ObjectOutputStream out;
169 protected String path;
170 protected Object key;
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);
180 int pos = k.lastIndexOf('.');
182 if (path == null) path = "";
183 path += k.substring(0, pos);
184 k = k.substring(pos + 1);
187 } else this.key = key;
191 public void execute(Prevayler p, JS r, ObjectOutputStream o) throws IOException {
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 ");
209 } catch(JSExn e) { out.writeObject(e); }
214 protected abstract void execute() throws JSExn, IOException;
217 public static class JSImmutable extends JS {
218 private final JS wrapped;
219 public JSImmutable(JS toWrap) { wrapped = toWrap; }
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); }
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; }
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;
247 private int size = -1;
249 public int size() { if (size == -1) update(); return size; }
250 public Iterator iterator() { updateList(); return new KeyIt(); }
252 private synchronized Keys update() {
253 if (updating) return this;
254 modCount++; items.clear();
256 final ObjectInputStream in = send(new Request(path) {
257 protected void execute() throws JSExn, IOException {
258 Collection c = scope.keys(); out.writeInt(c.size());
262 } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
267 private synchronized Keys updateList() {
268 if (updating) return this;
270 modCount++; items.clear();
272 final ObjectInputStream in = send(new Request(path) {
273 protected void execute() throws JSExn, IOException {
274 Collection c = scope.keys(); out.writeInt(c.size());
276 if (c.size() > 1000) { // FIXME: better way to unload server?
279 List l = new ArrayList(c);
281 i = l.listIterator();
283 while (i.hasNext()) out.writeObject(i.next());
287 new Thread() { public void run() {
289 for (int i=0; i < size; i++) items.add(in.readObject());
291 } catch (Exception e) {
292 size = -1; items.clear();
293 throw new RuntimeException("JSRemote", e);
295 synchronized (Keys.this) { Keys.this.updating = false; }
298 } catch (Exception e) { throw new RuntimeException("JSRemote", e); }
303 private class KeyIt implements Iterator {
304 private final int mod;
307 KeyIt() { mod = modCount; }
308 public boolean hasNext() {
309 if (modCount != mod) throw new ConcurrentModificationException();
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) {}
318 return items.get(pos++);
320 public void remove() { throw new UnsupportedOperationException(); } // FIXME