2003/11/03 07:36:40
[org.ibex.core.git] / src / org / xwt / Res.java
1 // Copyright 2003 Adam Megacz, see the COPYING file for licensing [GPL]
2 package org.xwt;
3
4 import java.io.*;
5 import java.util.*;
6 import java.util.zip.*;
7 import org.xwt.js.*;
8 import org.xwt.util.*;
9 import org.bouncycastle.util.encoders.Base64;
10
11 /** base class for XWT resources */
12 public abstract class Res extends JS {
13
14     public abstract String getDescriptiveName();
15     public String typeName() { return "resource"; }
16
17     /** cache of subresources so that the equality operator works on them */
18     private Hash refCache = null;
19
20     public Template t = null;
21
22     public Res getParent() { return null; }
23
24     /** an InputStream that makes sure it is not in the MessageQueue when blocked on a read */
25     // FIXME
26     private static class BackgroundInputStream extends FilterInputStream {
27         BackgroundInputStream(InputStream i) { super(i); }
28     /*
29         private void suspend() throws IOException {
30             if (!ThreadMessage.suspendThread())
31                 throw new IOException("attempt to perform background-only operation in a foreground thread");
32         }
33         private void resume() {
34             ThreadMessage.resumeThread();
35         }
36         public int read() throws IOException {
37             suspend();
38             try { return super.read(); }
39             finally { resume(); }
40         }
41         public int read(byte[] b, int off, int len) throws IOException {
42             suspend();
43             try { return super.read(b, off, len); }
44             finally { resume(); }
45         }
46     */
47     }
48
49     /** returns an InputStream containing the Resource's contents */
50     public final InputStream getInputStream() throws IOException { return new BackgroundInputStream(getInputStream("")); }
51     public abstract InputStream getInputStream(String path) throws IOException;
52
53     /** graft newResource in place of this resource on its parent */
54     public Res graft(Object newResource) { throw new JS.Exn("cannot graft onto this resource"); }
55
56     /** if the path of this resource does not end with extension, return a new one wit it appended */
57     public Res addExtension(String extension) { return new Ref(this, extension); }
58
59     public Object[] keys() { throw new JS.Exn("cannot enumerate a resource"); } 
60     public Object put(Object key, Object val) { throw new JS.Exn("cannot put to a resource"); } 
61     public Object get(Object key) {
62         if ("".equals(key)) {
63             Template t = Template.getTemplate(addExtension(".xwt"));
64             return t == null ? null : t.getStatic();
65         }
66         Object ret = refCache == null ? null : refCache.get(key);
67         if (ret != null) return ret;
68         ret = new Ref(this, key);
69         if (refCache == null) refCache = new Hash();
70         refCache.put(key, ret);
71         return ret;
72     }
73
74     public static Res stringToRes(String url) {
75         if (url.indexOf('!') != -1) {
76             Res ret = new Zip(stringToRes(url.substring(0, url.lastIndexOf('!'))));
77             String subpath = url.substring(url.lastIndexOf('!') + 1);
78             if (subpath.length() > 0) ret = (Res)ret.get(subpath);
79             return ret;
80         }
81         if (url.startsWith("http://")) return new HTTP(url);
82         if (url.startsWith("https://")) return new HTTP(url);
83         if (url.startsWith("cab:")) return new Cab(stringToRes(url.substring(4)));
84         if (url.startsWith("data:")) return new ByteArray(Base64.decode(url.substring(5)));
85         if (url.startsWith("utf8:")) return new ByteArray(url.substring(5).getBytes());
86         throw new JS.Exn("invalid resource specifier " + url);
87     }
88
89     /** subclass from this if you want a CachedInputStream for each path */
90     public static class CachedRes extends Res {
91         private Res parent;
92         private boolean disk = false;
93
94         // FIXME: security concern here
95         private String subdir = null;
96
97         public String getDescriptiveName() { return parent.getDescriptiveName(); }
98         private Hash cachedInputStreams = new Hash();
99         public CachedRes(Res parent, String subdir, boolean disk) {
100             this.parent = parent; this.disk = disk; this.subdir = subdir;
101         }
102         public InputStream getInputStream(String path) throws IOException {
103             CachedInputStream cis = (CachedInputStream)cachedInputStreams.get(path);
104             if (cis == null) {
105                 java.io.File f = null;
106                 if (disk) {
107                     // FIXME ugly
108                     // FIXME need separate hash for disk/nondisk
109                     f = new java.io.File(System.getProperty("user.home") +
110                                          java.io.File.separatorChar + ".xwt" +
111                                          java.io.File.separatorChar + "caches" +
112                                          java.io.File.separatorChar + subdir +
113                                          java.io.File.separatorChar +
114                                          new String(Base64.encode(parent.getDescriptiveName().getBytes())));
115                     Log.log(this, "caching resource in " + f);
116                     new java.io.File(f.getParent()).mkdirs();
117                     if (f.exists()) return new FileInputStream(f);
118                 }
119                 cis = new CachedInputStream(parent.getInputStream(path), f);
120                 cachedInputStreams.put(path, cis);
121             }
122             return cis.getInputStream();
123         }
124     }
125
126     /** HTTP or HTTPS resource */
127     public static class HTTP extends Res {
128         private String url;
129         HTTP(String url) { this.url = url; }
130         public String getDescriptiveName() { return url; }
131         public InputStream getInputStream(String path) throws IOException {
132             return new org.xwt.HTTP(url + path).GET(); }
133     }
134
135     /** byte arrays */
136     public static class ByteArray extends Res {
137         private byte[] bytes;
138         ByteArray(byte[] bytes) { this.bytes = bytes; }
139         public String getDescriptiveName() { return "byte[]"; }
140         public InputStream getInputStream(String path) throws IOException {
141             if (!"".equals(path)) throw new JS.Exn("can't get subresources of a byte[] resource");
142             return new ByteArrayInputStream(bytes);
143         }
144     }
145
146     /** a file */
147     public static class File extends Res {
148         private String path;
149         File(String path) { this.path = path; }
150         public String getDescriptiveName() { return "file://" + path; }
151         public InputStream getInputStream(String rest) throws IOException {
152             return new FileInputStream((path + rest).replace('/', java.io.File.separatorChar)); }
153     }
154
155     /** "unwrap" a Zip archive */
156     public static class Zip extends Res {
157         private Res parent;
158         Zip(Res parent) { this.parent = parent; }
159         public String getDescriptiveName() { return parent.getDescriptiveName() + "!"; }
160         public InputStream getInputStream(String path) throws IOException {
161             if (path.startsWith("/")) path = path.substring(1);
162             InputStream pis = parent.getInputStream();
163             ZipInputStream zis = new ZipInputStream(pis);
164             ZipEntry ze = zis.getNextEntry();
165             while(ze != null && !ze.getName().equals(path)) ze = zis.getNextEntry();
166             if (ze == null) throw new JS.Exn("requested file (" + path + ") not found in archive");
167             return new KnownLength.KnownLengthInputStream(zis, (int)ze.getSize());
168         }
169     }
170
171     /** "unwrap" a Cab archive */
172     public static class Cab extends Res {
173         private Res parent;
174         Cab(Res parent) { this.parent = parent; }
175         public String getDescriptiveName() { return "cab[" + parent.getDescriptiveName() + "]"; }
176         public InputStream getInputStream(String path) throws IOException {
177             // FIXME: knownlength
178             if (path.startsWith("/")) path = path.substring(1);
179             return new org.xwt.translators.MSPack(parent.getInputStream()).getInputStream(path);
180         }
181     }
182
183     /** the Builtin resource */
184     public static class Builtin extends Res {
185         public Builtin() { };
186         public String getDescriptiveName() { return "[builtin]"; }
187         public InputStream getInputStream(String path) throws IOException {
188             if (!path.equals("")) throw new IOException("the builtin resource has no subresources");
189             return Platform.getBuiltinInputStream();
190         }
191     }
192
193     /** what you get when you reference a subresource */
194     public static class Ref extends Res {
195         Res parent;
196         Object key;
197         Ref(Res parent, Object key) { this.parent = parent; this.key = key; }
198         public String getDescriptiveName() {
199             String pdn = parent.getDescriptiveName();
200             if (pdn.equals("")) return key.toString();
201             if (!pdn.endsWith("!")) pdn += ".";
202             return pdn + key.toString();
203         }
204         public Res addExtension(String extension) {
205             return (key instanceof String && ((String)key).endsWith(extension)) ? this : new Ref(parent, key + extension);
206         }
207         public InputStream getInputStream(String path) throws IOException {
208             return parent.getInputStream("/" + key + path);
209         }
210         public Res getParent() { return parent; }
211         public Res graft(Object newResource) { return new Graft(parent, key, newResource); }
212     }
213
214     // FEATURE: eliminate code duplication with JS.Graft
215     /** shadow resource which replaces the graft */
216     public static class Graft extends Res {
217         Res graftee;
218         Object replaced_key;
219         Object replaced_val;
220         Graft(Res graftee, Object key, Object val) { this.graftee = graftee; replaced_key = key; replaced_val = val; }
221         public boolean equals(Object o) { return (this == o || graftee.equals(o)); }
222         public int hashCode() { return graftee.hashCode(); }
223         public InputStream getInputStream(String s) throws IOException { return graftee.getInputStream(s); }
224         public Object get(Object key) { return replaced_key.equals(key) ? replaced_val : graftee.get(key); }
225         public String getDescriptiveName() { return graftee.getDescriptiveName(); }
226         public Res getParent() { return graftee.getParent(); }
227         public Object callMethod(Object method, Array args, boolean checkOnly) throws JS.Exn {
228             if (!replaced_key.equals(method)) return graftee.callMethod(method, args, checkOnly);
229             if (replaced_val instanceof Callable) return checkOnly ? Boolean.TRUE : ((Callable)replaced_val).call(args);
230             if (checkOnly) return Boolean.FALSE;
231             throw new JS.Exn("attempt to call non-function");
232         }
233         public Number coerceToNumber() { return graftee.coerceToNumber(); }
234         public String coerceToString() { return graftee.coerceToString(); }
235         public boolean coerceToBoolean() { return graftee.coerceToBoolean(); }
236         public String typeName() { return graftee.typeName(); }
237     }
238
239     /** shadow resource which replaces the graft */
240     public static class ProgressWatcher extends Res {
241         final Res watchee;
242         Function callback;
243         ProgressWatcher(Res watchee, Function callback) { this.watchee = watchee; this.callback = callback; }
244         public String getDescriptiveName() { return watchee.getDescriptiveName(); }
245         public InputStream getInputStream(String s) throws IOException {
246             final InputStream is = watchee.getInputStream(s);
247             return new FilterInputStream(is) {
248                     int bytesDownloaded = 0;
249                     public int read() throws IOException {
250                         int ret = super.read();
251                         if (ret != -1) bytesDownloaded++;
252                         return ret;
253                     }
254                     public int read(byte[] b, int off, int len) throws IOException {
255                         int ret = super.read(b, off, len);
256                         if (ret != 1) bytesDownloaded += ret;
257                         Scheduler.add(new Scheduler.Task() { public void perform() {
258                             JS.Array args = new JS.Array();
259                             args.addElement(new Integer(bytesDownloaded));
260                             args.addElement(new Integer(is instanceof KnownLength ? ((KnownLength)is).getLength() : 0));
261                             // FIXME
262                             //new JS.Context(callback, null, args).resume();
263                         } });
264                         return ret;
265                     }
266                 };
267         }
268     }
269
270     public Object callMethod(Object method, Array args, boolean checkOnly) throws JS.Exn {
271         if (method.equals("getUTF")) {
272             if (checkOnly) return Boolean.TRUE;
273             if (args.length() != 0) return null;
274             try {
275                 CharArrayWriter caw = new CharArrayWriter();
276                 InputStream is = getInputStream();
277                 BufferedReader r = new BufferedReader(new InputStreamReader(is));
278                 char[] buf = new char[1024];
279                 while(true) {
280                     int numread = r.read(buf, 0, 1024);
281                     if (numread == -1) break;
282                     caw.write(buf, 0, numread);
283                 }
284                 return caw.toString();
285             } catch (IOException e) {
286                 if (Log.on) Log.log(Res.class, "IO Exception while reading from file");
287                 if (Log.on) Log.log(Res.class, e);
288                 throw new JS.Exn("error while reading from Resource");
289             }
290         } else if (method.equals("getDOM")) {
291             if (checkOnly) return Boolean.TRUE;
292             if (args.length() != 0) return null;
293             return new XMLHelper().doParse();
294         }
295         if (checkOnly) return Boolean.FALSE;
296         return null;
297     }
298
299     private class XMLHelper extends XML {
300         Vector obStack = new Vector();
301         public XMLHelper() { super(BUFFER_SIZE); }
302         public void startElement(XML.Element c) throws XML.SchemaException {
303             JS o = new JS.Obj();
304             o.put("$name", c.localName);
305             for(int i=0; i<c.len; i++) o.put(c.keys[i], c.vals[i]);
306             o.put("$numchildren", new Integer(0));
307             obStack.addElement(o);
308         }
309         public void endElement(XML.Element c) throws XML.SchemaException {
310             if (obStack.size() == 1) return;
311             JS me = (JS)obStack.lastElement();
312             obStack.setSize(obStack.size() - 1);
313             JS parent = (JS)obStack.lastElement();
314             int numchildren = ((Integer)parent.get("$numchildren")).intValue();
315             parent.put("$numchildren", new Integer(numchildren + 1));
316             parent.put(new Integer(numchildren), me);
317         }
318         public void characters(char[] ch, int start, int length) throws XML.SchemaException {
319             String s = new String(ch, start, length);
320             JS parent = (JS)obStack.lastElement();
321             int numchildren = ((Integer)parent.get("$numchildren")).intValue();
322             Object lastChild = parent.get(new Integer(numchildren - 1));
323             if (lastChild instanceof String) {
324                 parent.put(new Integer(numchildren - 1), lastChild + s);
325             } else {
326                 parent.put("$numchildren", new Integer(numchildren + 1));
327                 parent.put(new Integer(numchildren), s);
328             }
329         }
330         public void whitespace(char[] ch, int start, int length) {}
331         public JS doParse() throws JS.Exn {
332             try { 
333                 InputStream is = getInputStream();
334                 BufferedReader r = new BufferedReader(new InputStreamReader(is));
335                 parse(r);
336             } catch (XML.XMLException e) {
337                 throw new JS.Exn("error parsing XML: " + e.toString());
338             } catch (IOException e) {
339                 if (Log.on) Log.log(this, "IO Exception while reading from file");
340                 if (Log.on) Log.log(this, e);
341                 throw new JS.Exn("error reading from Resource");
342             }
343             return obStack.size() >= 1 ? (JS)obStack.elementAt(0) : null;
344         }
345     }
346
347     public void writeTo(OutputStream os) throws IOException {
348         InputStream is = getInputStream();
349         byte[] buf = new byte[1024];
350         while(true) {
351             int numread = is.read(buf, 0, 1024);
352             if (numread == -1) break;
353             if (Log.on) Log.log(this, "wrote " + numread + " bytes");
354             os.write(buf, 0, numread);
355         }
356         os.flush();
357
358         // we have to close this because flush() doesn't work on Win32-GCJ
359         os.close();
360     }
361 }