001: /*=============================================================================
002: * Copyright Texas Instruments 2000. All Rights Reserved.
003: *
004: * This program is free software; you can redistribute it and/or
005: * modify it under the terms of the GNU Lesser General Public
006: * License as published by the Free Software Foundation; either
007: * version 2 of the License, or (at your option) any later version.
008: *
009: * This program is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012: * Lesser General Public License for more details.
013: *
014: * You should have received a copy of the GNU Lesser General Public
015: * License along with this library; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: *
018: * $ProjectHeader: OSCRIPT 0.155 Fri, 20 Dec 2002 18:34:22 -0800 rclark $
019: */
020:
021: package oscript.fs;
022:
023: import java.io.*;
024: import java.util.*;
025: import java.util.zip.*;
026:
027: import oscript.exceptions.ProgrammingErrorException;
028:
029: /* XXX constructor should probably take an AbstractFile rather than File
030: */
031:
032: /**
033: * A <code>JarFileSystem</code> implements a filesystem on top of a <i>.jar</i>
034: * or <i>.zip</i> file.
035: *
036: * @author Rob Clark (rob@ti.com)
037: * <!--$Format: " * @version $Revision$"$-->
038: * @version 1.21
039: */
040: public class JarFileSystem extends AbstractFileSystem {
041: private File file;
042: private ZipFile zipFile;
043: private String fileDescriptor;
044: private final boolean autoflush;
045:
046: /**
047: * Maps path to JarFile.
048: */
049: private Hashtable jarFileTable = new Hashtable();
050:
051: /*=======================================================================*/
052: /**
053: * Class Constructor.
054: *
055: * @param root the root of the local filesystem
056: */
057: public JarFileSystem(File root) throws ZipException, IOException {
058: this (root, false);
059: }
060:
061: /*=======================================================================*/
062: /**
063: * Class Constructor.
064: *
065: * @param root the root of the local filesystem
066: * @param autoflush if <code>true</code> automatically flush the entire
067: * filesystem. Note that flushing entire filesystem to disk involves
068: * writing the entire jar file, and can be expensive
069: */
070: public JarFileSystem(File root, boolean autoflush)
071: throws ZipException, IOException {
072: file = root;
073: fileDescriptor = file.getAbsolutePath();
074:
075: this .autoflush = autoflush;
076:
077: if (file.exists())
078: zipFile = new ZipFile(fileDescriptor);
079: }
080:
081: /*=======================================================================*/
082: /**
083: * Class Constructor.
084: *
085: * @param root the root of the local filesystem
086: */
087: public JarFileSystem(String root) throws ZipException, IOException {
088: this (new File(root));
089: }
090:
091: /*=======================================================================*/
092:
093: protected synchronized void finalize() throws Throwable {
094: flush();
095: }
096:
097: private boolean flushing = false;
098: private boolean needsFlush = false;
099: private Runnable flusher = null;
100:
101: private synchronized void scheduleFlush() {
102: if (!flushing && (flusher == null)) {
103: flusher = new Runnable() {
104: public void run() {
105: try {
106: synchronized (JarFileSystem.this ) {
107: flush();
108: flusher = null;
109: }
110: } catch (Throwable t) {
111: t.printStackTrace();
112: }
113: }
114:
115: public String toString() {
116: return "[flusher, file=" + file + "]";
117: }
118: };
119:
120: needsFlush = true;
121: if (autoflush)
122: flusher.run();
123: else
124: oscript.OscriptBuiltins.atExit(flusher);
125: }
126: }
127:
128: /*=======================================================================*/
129: /**
130: * If any changes have been made, flush them out to disk.
131: */
132: public synchronized void flush() throws IOException {
133: flushing = true;
134:
135: try {
136: if (needsFlush) {
137: needsFlush = false;
138:
139: ByteArrayOutputStream bos = new ByteArrayOutputStream();
140: ZipOutputStream zos = new ZipOutputStream(bos);
141:
142: // first write out existing elements:
143: if (zipFile != null) {
144: for (Enumeration e = zipFile.entries(); e
145: .hasMoreElements();) {
146: ZipEntry zipEntry = (ZipEntry) (e.nextElement());
147: JarFile jarFile = (JarFile) (jarFileTable
148: .get(getZipEntryName(zipEntry)));
149:
150: // if already in table, postpone writing until later, otherwise write now:
151: if (jarFile == null)
152: writeZipEntry(zos, zipEntry, zipFile
153: .getInputStream(zipEntry));
154: }
155: }
156:
157: // then write out new elements:
158: for (Enumeration e = jarFileTable.keys(); e
159: .hasMoreElements();) {
160: String name = (String) (e.nextElement());
161: JarFile jarFile = (JarFile) (jarFileTable.get(name));
162:
163: if (jarFile.exists()) {
164: if (jarFile.isDirectory()) {
165: writeZipEntry(zos, jarFile.getZipEntry(),
166: null);
167: } else {
168: jarFile.flush(); // have to flush before getZipEntry... maybe
169: writeZipEntry(zos, jarFile.getZipEntry(),
170: jarFile.getInputStream());
171: }
172: }
173: }
174:
175: if (zipFile != null)
176: zipFile.close();
177:
178: zos.flush();
179: zos.close();
180:
181: file.delete();
182: file.createNewFile();
183:
184: FileOutputStream fos = new FileOutputStream(file
185: .getAbsolutePath());
186: fos.write(bos.toByteArray());
187: fos.flush();
188: fos.close();
189:
190: zipFile = new ZipFile(file, ZipFile.OPEN_READ);
191: }
192: } finally {
193: flushing = false;
194: }
195: }
196:
197: /**
198: * Utility to write the specified entry to a file...
199: */
200: private void writeZipEntry(ZipOutputStream zos, ZipEntry zipEntry,
201: InputStream is) throws IOException {
202: // open new entry:
203: zos.putNextEntry(zipEntry);
204:
205: // write data:
206: if (is != null) {
207: byte[] buf = new byte[1024];
208: int in;
209: while ((in = is.read(buf, 0, buf.length)) > 0)
210: zos.write(buf, 0, in);
211: }
212:
213: // close entry:
214: zos.closeEntry();
215: }
216:
217: /**
218: * Since the zip entry name may end with trailing '/', strip them off.
219: */
220: private String getZipEntryName(ZipEntry ze) {
221: String name = ze.getName();
222: while ((name.length() > 0) && name.endsWith("/"))
223: name = name.substring(0, name.length() - 1);
224: return name;
225: }
226:
227: /*=======================================================================*/
228: /**
229: * Try to resolve the specified path. If unresolved, return <code>null</code>.
230: *
231: * @param mountPath the path this fs is mounted at to resolve the requested file
232: * @param path path to file
233: * @return file or <code>null</code>
234: */
235: protected AbstractFile resolveInFileSystem(String mountPath,
236: String path) {
237: if (path.endsWith("/"))
238: throw new ProgrammingErrorException("this is bad, path="
239: + path);
240:
241: JarFile jarFile = (JarFile) (jarFileTable.get(path));
242:
243: if (jarFile == null) {
244: jarFile = new JarFile(mountPath, path);
245: jarFileTable.put(path, jarFile);
246: }
247:
248: return jarFile;
249: }
250:
251: /*=======================================================================*/
252: /**
253: * Return an iterator of children of the specified path.
254: *
255: * @param mountPath the path this fs is mounted at to resolve the requested file
256: * @param path path to file, relative to <code>mountPath</code>
257: * @return a collection of <code>AbstractFile</code>
258: */
259: protected synchronized Collection childrenInFileSystem(
260: String mountPath, String path) throws IOException {
261: Hashtable childTable = new Hashtable(); // so we don't add any child twice!
262:
263: if (zipFile != null)
264: for (Enumeration e = zipFile.entries(); e.hasMoreElements();)
265: childrenInFileSystemHelper(mountPath, path, childTable,
266: ((ZipEntry) (e.nextElement())).getName()); // getZipEntryName( (ZipEntry)(e.nextElement()) );
267:
268: for (Enumeration e = jarFileTable.keys(); e.hasMoreElements();)
269: childrenInFileSystemHelper(mountPath, path, childTable,
270: (String) (e.nextElement()));
271:
272: return childTable.values();
273: }
274:
275: private void childrenInFileSystemHelper(String mountPath,
276: String path, Hashtable childTable, String name) {
277: // ignore META-INF:
278: if (name.startsWith("META-INF"))
279: return;
280:
281: if (name.startsWith(path)) {
282: int plen = path.length();
283:
284: // strip of leading '/'s
285: if ((plen < name.length()) && (name.charAt(plen) == '/'))
286: plen++;
287:
288: int idx = name.indexOf('/', plen);
289: String childName = name.substring(plen, (idx != -1) ? idx
290: : name.length());
291:
292: if ((childName.length() > 0)
293: && (childTable.get(childName) == null)) {
294: AbstractFile file = resolveInFileSystem(mountPath,
295: ((plen == 0) ? "" : (path + SEPERATOR_CHAR))
296: + childName);
297: if (file.exists())
298: childTable.put(childName, file);
299: }
300: }
301: }
302:
303: private static final byte[] EMPTY_BUF = new byte[0];
304:
305: /*=======================================================================*/
306: /**
307: * An interface to be implemented by something that can implement file-like
308: * operations. Ie read as a stream, write as a stream. Note that a single
309: * <code>JarFile</code> instance represent any given path.
310: */
311: class JarFile implements AbstractFile {
312: private String mountPath;
313: private String path;
314: private String entryDescriptor;
315: private String ext;
316:
317: /**
318: * Since ZipEntry#getTime() seems to be very inefficient, in
319: * particular it seems to create a lot of garbage to be GC'd, we
320: * cache the value.
321: */
322: private long lastModified = -1;
323:
324: /**
325: * Buffer for write data to be flushed to disk. Contains entire file
326: * contents. If this is not <code>null</code> it supercedes data from
327: * ZipFile.
328: */
329: byte[] buf;
330:
331: private ZipEntry zipEntry;
332:
333: ZipEntry getZipEntry() {
334: synchronized (JarFileSystem.this ) {
335: if (zipFile != null) {
336: // NOTE: we have to try to resolve as a directory first, to work
337: // around a bug in ZipFile::getEntry()... otherwise the trailing
338: // "/" won't be part of the ZipEntry name, and isDirectory() will
339: // return incorrect value:
340: if (zipEntry == null)
341: zipEntry = zipFile.getEntry(path + "/");
342: if (zipEntry == null)
343: zipEntry = zipFile.getEntry(path);
344: }
345: return zipEntry;
346: }
347: }
348:
349: /**
350: * Class Constructor.
351: */
352: JarFile(String mountPath, String path) {
353: this .mountPath = mountPath;
354: this .path = path;
355:
356: // determine entry descriptor, used to uniquely identify a file:
357: entryDescriptor = fileDescriptor + "@@/" + path;
358:
359: // determine extension:
360: int idx = path.lastIndexOf('.');
361: if (idx != -1)
362: ext = path.substring(idx + 1);
363: else
364: ext = "";
365: }
366:
367: /**
368: * Get the extension, which indicates the type of file. Usually the extension
369: * is part of the filename, ie. if the extension was <i>os</i>, the filename
370: * would end with <i>.os</i>.
371: *
372: * @return a string indicating the type of file
373: */
374: public String getExtension() {
375: return ext;
376: }
377:
378: /**
379: * Is it possible to write to this file.
380: */
381: public boolean canWrite() {
382: return exists() && file.canWrite();
383: }
384:
385: /**
386: * Is it possible to read from this file.
387: */
388: public boolean canRead() {
389: return exists() && file.canRead();
390: }
391:
392: /**
393: * Tests whether the file denoted by this abstract pathname exists.
394: *
395: * @return <code>true</code> iff the file exists
396: */
397: public boolean exists() {
398: return getZipEntry() != null;
399: }
400:
401: /**
402: * Test whether this file is a directory.
403: *
404: * @return <code>true</code> iff this file is a directory
405: */
406: public boolean isDirectory() {
407: return exists() && getZipEntry().isDirectory();
408: }
409:
410: /**
411: * Test whether this file is a regular file. A file is a regular file if
412: * it is not a directory.
413: *
414: * @return <code>true</code> iff this file is a regular file.
415: */
416: public boolean isFile() {
417: return exists() && !getZipEntry().isDirectory();
418: }
419:
420: /**
421: * Return the time of last modification. The meaning of these value is not
422: * so important, but the implementation must ensure that a higher value is
423: * returned for the more recent version of a given element. Ie. if at some
424: * point this returns X, an <code>AbstractFile</code> representing the same
425: * "file", but created at a later time, should return X if the file has not
426: * been modified, or >X if the file has been modified.
427: *
428: * @return larger value indicates more recent modification
429: */
430: public long lastModified() {
431: if (lastModified != -1)
432: return lastModified;
433:
434: synchronized (JarFileSystem.this ) {
435: if (exists())
436: return lastModified = getZipEntry().getTime();
437: else
438: return -1; // perhaps should throw some exception?
439: }
440: }
441:
442: /**
443: * Return the length of the file in bytes.
444: */
445: public long length() {
446: if (!exists())
447: return -1;
448: else if (buf != null)
449: return buf.length;
450: else
451: return getZipEntry().getSize();
452: }
453:
454: /**
455: * Create a new empty file, if it does not yet exist.
456: *
457: * @return <code>true</code> iff the file does not exist and was
458: * successfully created.
459: * @throws IOException if error
460: */
461: public boolean createNewFile() throws IOException {
462: return createImpl(false);
463: }
464:
465: /**
466: * If this file does not exist, create it as a directory.
467: *
468: * @return <code>true</code> iff directory successfully created
469: */
470: public boolean mkdir() throws IOException {
471: String parentPath = dirname(path);
472: if (parentPath != null)
473: if (!resolveInFileSystem(mountPath, parentPath)
474: .exists())
475: throw new IOException(
476: "parent directory does not exist");
477: return createImpl(true);
478: }
479:
480: /**
481: * If this file does not exist, create it as a directory. All necessary
482: * parent directories are also created. If this operation fails, it may
483: * have successfully created some or all of the parent directories.
484: *
485: * @return <code>true</code> iff directory successfully created
486: */
487: public boolean mkdirs() throws IOException {
488: return createImpl(true);
489: }
490:
491: private boolean createImpl(boolean isDir) throws IOException {
492: synchronized (JarFileSystem.this ) {
493: boolean rc = exists();
494:
495: if (!rc) {
496: // recursively create parent directories, if needed...
497: // we stop travelling up the tree once we reach the
498: // mount point (ie. our root) since by definition our
499: // mount point must exist and be a directory.
500: String parentPath = dirname(path);
501: if (parentPath != null) {
502: AbstractFile parent = resolveInFileSystem(
503: mountPath, parentPath);
504: parent.mkdirs();
505: parent.touch();
506: } else {
507: touchMountPoint(JarFileSystem.this );
508: }
509:
510: String path = this .path;
511:
512: if (isDir)
513: path += "/";
514:
515: zipEntry = new ZipEntry(path);
516: buf = EMPTY_BUF;
517: touch();
518: }
519:
520: return rc;
521: }
522: }
523:
524: /**
525: * Update the timestamp on this file to the current time.
526: *
527: * @throws IOException if error
528: */
529: public void touch() throws IOException {
530: if (!exists())
531: throw new IOException("does not exist");
532: getZipEntry().setTime(
533: lastModified = System.currentTimeMillis());
534: scheduleFlush();
535: }
536:
537: /**
538: * Delete this file. If this file is a directory, then the directory must
539: * be empty.
540: *
541: * @return <code>true<code> iff the directory is successfully deleted.
542: * @throws IOException if error
543: */
544: public boolean delete() throws IOException {
545: throw new ProgrammingErrorException("unimplemented");
546: // note: remember to touch() parent directory
547: }
548:
549: /**
550: * Get an input stream to read from this file.
551: *
552: * @return input stream
553: * @throws IOException if <code>canRead</code> returns <code>true</code>
554: * @see #canRead
555: */
556: public InputStream getInputStream() throws IOException {
557: if (isDirectory())
558: throw new IOException("cannot read directory: " + this );
559:
560: synchronized (JarFileSystem.this ) {
561: if (!exists())
562: throw new IOException("does not exist");
563:
564: flush(); // XXX
565:
566: if (buf == null)
567: return zipFile.getInputStream(zipEntry);
568: else
569: return new ByteArrayInputStream(buf);
570: }
571: }
572:
573: /**
574: * Get an output stream to write to this file.
575: *
576: * @return output stream
577: * @throws IOException if <code>canWrite</code> returns <code>false</code>
578: * @see #canWrite
579: */
580: public OutputStream getOutputStream(boolean append)
581:
582: throws IOException {
583: if (isDirectory())
584: throw new IOException("cannot write directory: " + this );
585:
586: synchronized (JarFileSystem.this ) {
587: if (!exists())
588: throw new IOException("does not exist");
589: return new BufferOutputStream(append);
590: }
591: }
592:
593: /**
594: * Be the same as the entryDescriptor, to make JarFiles useful as a
595: * key into a hash table.
596: */
597: public int hashCode() {
598: return entryDescriptor.hashCode();
599: }
600:
601: /**
602: */
603: public void flush() throws IOException {
604: for (Iterator itr = bosMap.keySet().iterator(); itr
605: .hasNext();)
606: ((BufferOutputStream) (itr.next())).flush();
607: }
608:
609: /**
610: * Keep track of outstanding output streams, so we can force them
611: * to flush iff needed.
612: */
613: private WeakHashMap bosMap = new WeakHashMap();
614:
615: /**
616: */
617: public boolean equals(Object obj) {
618: return (obj instanceof JarFile)
619: && entryDescriptor
620: .equals(((JarFile) obj).entryDescriptor);
621: }
622:
623: /**
624: * Rather than use ByteArrayOutputStream directly, we need to overload so
625: * we have control over flushing and finalizing. Note that zipEntry must
626: * be created (ie. the file entry must exist) in order to create the out-
627: * put stream.
628: */
629: private class BufferOutputStream extends ByteArrayOutputStream {
630: private boolean append;
631:
632: BufferOutputStream(boolean append) {
633: this .append = append;
634: bosMap.put(this , null);
635: }
636:
637: public void flush() throws IOException {
638: if (append)
639: throw new ProgrammingErrorException("unimplemented");
640: else
641: setBuf(toByteArray());
642: }
643:
644: public void close() throws IOException {
645: flush();
646: }
647: }
648:
649: /**
650: * Accessor used by a BufferOutputStream to update the buf... does
651: * extra house keeping like setting last modified time, etc.
652: */
653: private void setBuf(byte[] newBuf) {
654: synchronized (JarFileSystem.this ) {
655: ZipEntry zipEntry = getZipEntry();
656:
657: buf = newBuf;
658: zipEntry.setSize(buf.length);
659: zipEntry.setCompressedSize(-1); // invalidate currently stored value
660: zipEntry.setTime(lastModified = System
661: .currentTimeMillis());
662:
663: scheduleFlush();
664: }
665: }
666:
667: /**
668: * Get the file path, which globally identifies the file.
669: *
670: * @return a unique string
671: * @see #getName
672: */
673: public String getPath() {
674: return mountPath + "/" + path;
675: }
676:
677: /**
678: * Get the name of this file, which is the last component of the complete path.
679: *
680: * @return the file name
681: * @see #getPath
682: */
683: public String getName() {
684: return basename(getPath());
685: }
686:
687: public String toString() {
688: return getPath();
689: }
690: }
691: }
692:
693: /*
694: * Local Variables:
695: * tab-width: 2
696: * indent-tabs-mode: nil
697: * mode: java
698: * c-indentation-style: java
699: * c-basic-offset: 2
700: * eval: (c-set-offset 'substatement-open '0)
701: * eval: (c-set-offset 'case-label '+)
702: * eval: (c-set-offset 'inclass '+)
703: * eval: (c-set-offset 'inline-open '0)
704: * End:
705: */
|