2003/10/31 09:50:08
[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 String getDescriptiveName() { return ""; }
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 void 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 abstract class CachedRes extends Res {
91         private Hash cachedInputStreams = new Hash();
92         abstract InputStream _getInputStream(String path) throws IOException;
93         public final InputStream getInputStream(String path) throws IOException {
94             CachedInputStream cis = (CachedInputStream)cachedInputStreams.get(path);
95             if (cis == null) {
96                 cis = new CachedInputStream(_getInputStream(path));
97                 cachedInputStreams.put(path, cis);
98             }
99             return cis.getInputStream();
100         }
101     }
102
103     /** HTTP or HTTPS resource */
104     public static class HTTP extends CachedRes {
105         private String url;
106         HTTP(String url) { this.url = url; }
107         public String getDescriptiveName() { return url; }
108         public InputStream _getInputStream(String path) throws IOException {
109             return new org.xwt.HTTP(url + path).GET(); }
110     }
111
112     /** byte arrays */
113     public static class ByteArray extends Res {
114         private byte[] bytes;
115         ByteArray(byte[] bytes) { this.bytes = bytes; }
116         public String getDescriptiveName() { return "byte[]"; }
117         public InputStream getInputStream(String path) throws IOException {
118             if (!"".equals(path)) throw new JS.Exn("can't get subresources of a byte[] resource");
119             return new ByteArrayInputStream(bytes);
120         }
121     }
122
123     /** a file */
124     public static class File extends Res {
125         private String path;
126         File(String path) { this.path = path; }
127         public String getDescriptiveName() { return "file://" + path; }
128         public InputStream getInputStream(String rest) throws IOException {
129             return new FileInputStream((path + rest).replace('/', java.io.File.separatorChar)); }
130     }
131
132     /** "unwrap" a Zip archive */
133     public static class Zip extends Res {
134         private Res parent;
135         Zip(Res parent) { this.parent = parent; }
136         public String getDescriptiveName() { return parent.getDescriptiveName() + "!"; }
137         public InputStream getInputStream(String path) throws IOException {
138             if (path.startsWith("/")) path = path.substring(1);
139             InputStream pis = parent.getInputStream();
140             ZipInputStream zis = new ZipInputStream(pis);
141             ZipEntry ze = zis.getNextEntry();
142             while(ze != null && !ze.getName().equals(path)) ze = zis.getNextEntry();
143             if (ze == null) throw new JS.Exn("requested file (" + path + ") not found in archive");
144             return new KnownLength.KnownLengthInputStream(zis, (int)ze.getSize());
145         }
146     }
147
148     /** the Builtin resource */
149     public static class Builtin extends Res {
150         public Builtin() { };
151         public String getDescriptiveName() { return "[builtin]"; }
152         public InputStream getInputStream(String path) throws IOException {
153             if (!path.equals("")) throw new IOException("the builtin resource has no subresources");
154             return Platform.getBuiltinInputStream();
155         }
156     }
157
158     /** what you get when you reference a subresource */
159     public static class Ref extends Res {
160         Res parent;
161         Object key;
162         Ref(Res parent, Object key) { this.parent = parent; this.key = key; }
163         public String getDescriptiveName() {
164             String pdn = parent.getDescriptiveName();
165             if (pdn.equals("")) return key.toString();
166             if (!pdn.endsWith("!")) pdn += ".";
167             return pdn + key.toString();
168         }
169         public Res addExtension(String extension) {
170             return (key instanceof String && ((String)key).endsWith(extension)) ? this : new Ref(parent, key + extension);
171         }
172         public InputStream getInputStream(String path) throws IOException {
173             return parent.getInputStream("/" + key + path);
174         }
175         public Res getParent() { return parent; }
176         public Res graft(Object newResource) { return new Graft(parent, key, newResource); }
177     }
178
179     // FEATURE: eliminate code duplication with JS.Graft
180     /** shadow resource which replaces the graft */
181     public static class Graft extends Res {
182         Res graftee;
183         Object replaced_key;
184         Object replaced_val;
185         Graft(Res graftee, Object key, Object val) { this.graftee = graftee; replaced_key = key; replaced_val = val; }
186         public boolean equals(Object o) { return (this == o || graftee.equals(o)); }
187         public int hashCode() { return graftee.hashCode(); }
188         public InputStream getInputStream(String s) throws IOException { return graftee.getInputStream(s); }
189         public Object get(Object key) { return replaced_key.equals(key) ? replaced_val : graftee.get(key); }
190         public String getDescriptiveName() { return graftee.getDescriptiveName(); }
191         public Res getParent() { return graftee.getParent(); }
192         public Object callMethod(Object method, Array args, boolean checkOnly) throws JS.Exn {
193             if (!replaced_key.equals(method)) return graftee.callMethod(method, args, checkOnly);
194             if (replaced_val instanceof Callable) return checkOnly ? Boolean.TRUE : ((Callable)replaced_val).call(args);
195             if (checkOnly) return Boolean.FALSE;
196             throw new JS.Exn("attempt to call non-function");
197         }
198         public Number coerceToNumber() { return graftee.coerceToNumber(); }
199         public String coerceToString() { return graftee.coerceToString(); }
200         public boolean coerceToBoolean() { return graftee.coerceToBoolean(); }
201         public String typeName() { return graftee.typeName(); }
202     }
203
204     /** shadow resource which replaces the graft */
205     public static class ProgressWatcher extends Res {
206         final Res watchee;
207         JS.CompiledFunction callback;
208         ProgressWatcher(Res watchee, JS.CompiledFunction callback) { this.watchee = watchee; this.callback = callback; }
209         public String getDescriptiveName() { return watchee.getDescriptiveName(); }
210         public InputStream getInputStream(String s) throws IOException {
211             final InputStream is = watchee.getInputStream(s);
212             return new FilterInputStream(is) {
213                     int bytesDownloaded = 0;
214                     public int read() throws IOException {
215                         int ret = super.read();
216                         if (ret != -1) bytesDownloaded++;
217                         return ret;
218                     }
219                     public int read(byte[] b, int off, int len) throws IOException {
220                         int ret = super.read(b, off, len);
221                         if (ret != 1) bytesDownloaded += ret;
222                         Scheduler.add(new Scheduler.Task() { public Object call(Object arg) {
223                             JS.Array args = new JS.Array();
224                             args.addElement(new Integer(bytesDownloaded));
225                             args.addElement(new Integer(is instanceof KnownLength ? ((KnownLength)is).getLength() : 0));
226                             // FIXME
227                             // new JS.Thread(callback, callbackScope).resume();
228                             return null;
229                         } });
230                         return ret;
231                     }
232                 };
233         }
234     }
235
236     /** unpacks a Microsoft CAB file (possibly embedded in another file; we scan for 'MSCF' */
237     public static class CAB extends Res {
238         private Res parent;
239         CAB(Res parent) { this.parent = parent; }
240         private int swap_endian(int i) {
241             return ((i & 0xff) << 24) | ((i & 0xff00) << 8) | ((i & 0xff0000) >>> 8) | (i >>> 24);
242         }
243         public InputStream getInputStream(String path) throws IOException {
244             try {
245                return org.xwt.util.CAB.getFileInputStream(parent.getInputStream(), 2, path);
246             } catch (EOFException eof) {
247                throw new JS.Exn("MSCF header tag not found in file");
248             } catch (IOException ioe) {
249                throw new JS.Exn("IOException while reading file");
250             }
251         }
252     }
253
254     public Object callMethod(Object method, Array args, boolean checkOnly) throws JS.Exn {
255         if (method.equals("getUTF")) {
256             if (checkOnly) return Boolean.TRUE;
257             if (args.length() != 0) return null;
258             try {
259                 CharArrayWriter caw = new CharArrayWriter();
260                 InputStream is = getInputStream();
261                 BufferedReader r = new BufferedReader(new InputStreamReader(is));
262                 char[] buf = new char[1024];
263                 while(true) {
264                     int numread = r.read(buf, 0, 1024);
265                     if (numread == -1) break;
266                     caw.write(buf, 0, numread);
267                 }
268                 return caw.toString();
269             } catch (IOException e) {
270                 if (Log.on) Log.log(Res.class, "IO Exception while reading from file");
271                 if (Log.on) Log.log(Res.class, e);
272                 throw new JS.Exn("error while reading from Resource");
273             }
274         } else if (method.equals("getDOM")) {
275             if (checkOnly) return Boolean.TRUE;
276             if (args.length() != 0) return null;
277             return new XMLHelper().doParse();
278         }
279         if (checkOnly) return Boolean.FALSE;
280         return null;
281     }
282
283     private class XMLHelper extends XML {
284         Vector obStack = new Vector();
285         public XMLHelper() { super(BUFFER_SIZE); }
286         public void startElement(XML.Element c) throws XML.SchemaException {
287             JS o = new JS.Obj();
288             o.put("$name", c.localName);
289             for(int i=0; i<c.len; i++) o.put(c.keys[i], c.vals[i]);
290             o.put("$numchildren", new Integer(0));
291             obStack.addElement(o);
292         }
293         public void endElement(XML.Element c) throws XML.SchemaException {
294             if (obStack.size() == 1) return;
295             JS me = (JS)obStack.lastElement();
296             obStack.setSize(obStack.size() - 1);
297             JS parent = (JS)obStack.lastElement();
298             int numchildren = ((Integer)parent.get("$numchildren")).intValue();
299             parent.put("$numchildren", new Integer(numchildren + 1));
300             parent.put(new Integer(numchildren), me);
301         }
302         public void characters(char[] ch, int start, int length) throws XML.SchemaException {
303             String s = new String(ch, start, length);
304             JS parent = (JS)obStack.lastElement();
305             int numchildren = ((Integer)parent.get("$numchildren")).intValue();
306             Object lastChild = parent.get(new Integer(numchildren - 1));
307             if (lastChild instanceof String) {
308                 parent.put(new Integer(numchildren - 1), lastChild + s);
309             } else {
310                 parent.put("$numchildren", new Integer(numchildren + 1));
311                 parent.put(new Integer(numchildren), s);
312             }
313         }
314         public void whitespace(char[] ch, int start, int length) {}
315         public JS doParse() throws JS.Exn {
316             try { 
317                 InputStream is = getInputStream();
318                 BufferedReader r = new BufferedReader(new InputStreamReader(is));
319                 parse(r);
320             } catch (XML.XMLException e) {
321                 throw new JS.Exn("error parsing XML: " + e.toString());
322             } catch (IOException e) {
323                 if (Log.on) Log.log(this, "IO Exception while reading from file");
324                 if (Log.on) Log.log(this, e);
325                 throw new JS.Exn("error reading from Resource");
326             }
327             return obStack.size() >= 1 ? (JS)obStack.elementAt(0) : null;
328         }
329     }
330
331     public void writeTo(OutputStream os) throws IOException {
332         InputStream is = getInputStream();
333         byte[] buf = new byte[1024];
334         while(true) {
335             int numread = is.read(buf, 0, 1024);
336             if (numread == -1) break;
337             if (Log.on) Log.log(this, "wrote " + numread + " bytes");
338             os.write(buf, 0, numread);
339         }
340         os.flush();
341
342         // we have to close this because flush() doesn't work on Win32-GCJ
343         os.close();
344     }
345 }