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:
026: import oscript.exceptions.ProgrammingErrorException;
027:
028: /**
029: * An interface implemented by something that can resolve <code>AbstractFile</code>s.
030: *
031: * @author Rob Clark (rob@ti.com)
032: * <!--$Format: " * @version $Revision$"$-->
033: * @version 1.17
034: */
035: public abstract class AbstractFileSystem {
036: /**
037: * The current working directory, which is prepended to any paths
038: * that don't begin with a leading "/".
039: */
040: private static String cwd = "/";
041:
042: /**
043: * The seperator character used for paths. The implementation of
044: * the filesystem may translate this character into whatever is
045: * required for the native implementation of the filesystem.
046: */
047: public static final char SEPERATOR_CHAR = '/';
048:
049: /**
050: * Table of mount points, indexed by the path to the mount point.
051: */
052: private static final Hashtable mpTable = new Hashtable();
053:
054: private static long lastMountTime = 0;
055:
056: protected static final boolean enableWindozeHacks = System
057: .getProperty("os.name").toLowerCase().indexOf("windows") != -1;
058:
059: /**
060: * Get the current working directory.
061: *
062: * @return string
063: * @see #setCwd
064: */
065: public static String getCwd() {
066: return cwd;
067: }
068:
069: /**
070: * Set the current working directory. If the path is not an absolute path, it
071: * is postpended to the current cwd. If the path does not exist, or is not a
072: * directory, this will throw {@link java.lang.IllegalArgumentException}.
073: *
074: * @param cwd the new current working directory
075: * @throws IOException if the specified path is invalid.
076: * @see #getCwd
077: */
078: public static void setCwd(String cwd) throws IOException {
079: cwd = normalize(cwd);
080:
081: AbstractFile file = resolve(cwd);
082:
083: if (!file.exists()) {
084: System.err.println("bad path for: " + file);
085: System.err.println("canonical path: "
086: + ((File) file).getCanonicalPath());
087: }
088:
089: if ((file == null) || !file.exists())
090: throw new IOException("bad path: " + cwd);
091: else if (!file.isDirectory())
092: throw new IOException("not a directory: " + cwd);
093:
094: AbstractFileSystem.cwd = cwd;
095: }
096:
097: /**
098: * Normalize the path. This involves ensuring the path is
099: * fully qualified (begins with "/"), and gets rid of any
100: * extra "." and "/".
101: */
102: public static String normalize(String path) {
103: return normalize(cwd, path);
104: }
105:
106: public static String normalize(String cwd, String path) {
107: int idx;
108:
109: // replace '\' with '/':
110: path = path.replace('\\', '/');
111:
112: // deal with paths like "c:/...."
113: if (enableWindozeHacks) {
114: if ((path.length() >= 2)
115: && Character.isLetter(path.charAt(0))
116: && (path.charAt(1) == ':'))
117: path = "/" + Character.toUpperCase(path.charAt(0))
118: + path.substring(1);
119: }
120:
121: // add leading '/':
122: if (path.length() == 0)
123: path = "/";
124: else if (path.charAt(0) != '/')
125: path = cwd + '/' + path;
126:
127: // makes the next few steps simpler:
128: path += '/';
129:
130: // handle '/.':
131: while ((idx = path.indexOf("/./")) != -1)
132: path = path.substring(0, idx) + path.substring(idx + 2);
133:
134: // handle '/..':
135: while ((idx = path.indexOf("/../")) != -1)
136: path = ((idx == 0) ? "" : path.substring(0, path
137: .lastIndexOf('/', idx - 1)))
138: + path.substring(idx + 3);
139:
140: // remove trailing '/':
141: while ((path.length() > 0)
142: && (path.charAt(path.length() - 1) == '/'))
143: path = path.substring(0, path.length() - 1);
144:
145: // remove double '//':
146: while ((idx = path.indexOf("//")) != -1)
147: path = path.substring(0, idx) + path.substring(idx + 1);
148:
149: return path;
150: }
151:
152: /**
153: * Given a normalized path, return dirname. Ie. everything but the last
154: * component in the path.
155: */
156: public static String dirname(String path) {
157: int idx = path.lastIndexOf(SEPERATOR_CHAR);
158: if (idx == -1)
159: return null;
160: else
161: return path.substring(0, idx);
162: }
163:
164: /**
165: * Given a normalized path, return basename. Ie. the last component of the
166: * path.
167: */
168: public static String basename(String path) {
169: int idx = path.lastIndexOf(SEPERATOR_CHAR);
170: if (idx == -1)
171: return path;
172: else
173: return path.substring(idx + 1);
174: }
175:
176: /**
177: * Given a pathname, determine the mount point that should be used to
178: * resolve path.
179: */
180: private static MountPoint getMountPoint(String path) {
181: String origPath = path;
182: MountPoint mp = null;
183:
184: while (path != null) {
185: mp = (MountPoint) (mpTable.get(path));
186:
187: if (mp != null)
188: return mp;
189:
190: path = dirname(path);
191: }
192:
193: throw new ProgrammingErrorException("no mount point for: \""
194: + origPath + "\"");
195: }
196:
197: /**
198: * When a file is created/deleted in the topmost directory in a
199: * mounted file system, it needs some way to cause the timestamp
200: * on the directory(s) it is mounted into to change.
201: *
202: * @param fs the filesystem that was modified
203: */
204: protected synchronized static void touchMountPoint(
205: AbstractFileSystem fs) throws IOException {
206: for (Iterator itr = mpTable.keySet().iterator(); itr.hasNext();) {
207: String path = (String) (itr.next());
208: if (((MountPoint) (mpTable.get(path))).getFs() == fs)
209: resolve(path).touch();
210: }
211: }
212:
213: /**
214: * Mount a new filesystem. A filesystem can be mounted at any
215: * position within the virtual filesystem. A mounted filesystem
216: * could potentially eclipse a file. If multiple filesystems
217: * are mounted at the same point, the result is the union of all
218: * those filesystems.
219: *
220: * @param fs the filesystem to mount
221: * @param path the path to the location to mount (ex: "/")
222: */
223: public synchronized static void mount(AbstractFileSystem fs,
224: String path) {
225: if (oscript.data.Value.DEBUG)
226: System.err.println("mount: fs=" + fs + ", path=" + path);
227:
228: lastMountTime = System.currentTimeMillis();
229:
230: path = normalize(path);
231:
232: MountPoint mp = (MountPoint) (mpTable.get(path));
233: AbstractFileSystem target = (mp != null) ? mp.getFs() : null;
234:
235: if (target == null) {
236: mpTable.put(path, new MountPoint(path, fs));
237: } else if (target instanceof UnionFileSystem) {
238: ((UnionFileSystem) target).addFileSystem(fs);
239: } else {
240: UnionFileSystem ufs = new UnionFileSystem();
241: ufs.addFileSystem(target);
242: ufs.addFileSystem(fs);
243: mpTable.put(path, new MountPoint(path, ufs));
244: }
245:
246: // update lastModified time on parent directory:
247: try {
248: // there is no parent of "/" (which normalizes to "")
249: if (path.length() > 0)
250: resolve(dirname(path)).touch();
251: } catch (IOException e) {
252: throw new ProgrammingErrorException(e); // shouldn't happen
253: }
254: }
255:
256: static {
257: // windoze doesn't have a *real* root directory:
258: mount(new AbstractFileSystem() {
259:
260: protected AbstractFile resolveInFileSystem(
261: String mountPath, String path) {
262: return null;
263: }
264:
265: protected Collection childrenInFileSystem(String mountPath,
266: String path) {
267: return new LinkedList();
268: }
269:
270: protected void flush() throws IOException {
271: }
272:
273: }, "/");
274: }
275:
276: /**
277: * Try to load the specified file from one of the mounted filesystems.
278: *
279: * @param path the file to resolve
280: * @throws IOException if something goes wrong when reading file
281: * @see #addScriptPath(AbstractFileSystem)
282: */
283: public synchronized static AbstractFile resolve(String path)
284: throws IOException {
285: path = normalize(path);
286:
287: AbstractFile file = null;
288:
289: // search for filesystem:
290: MountPoint mp = getMountPoint(path);
291:
292: // resolve file:
293: file = mp.getFs().resolveInFileSystem(mp.getMountPath(),
294: mp.getRelativePath(path));
295:
296: // emulate non-existant files that are part of the path of a
297: // mount point that is further down the directory tree:
298: if ((file == null) || !file.exists()) {
299: for (Enumeration e = mpTable.keys(); e.hasMoreElements();) {
300: String mountPoint = (String) (e.nextElement());
301:
302: if (mountPoint.startsWith(path)) {
303: file = new MountPointFile(path);
304: break;
305: }
306: }
307: }
308:
309: // if still can't resolve file, emulate it:
310: if (file == null)
311: file = new BogusFile(path, false, false, false);
312:
313: return file;
314: }
315:
316: /**
317: * Get an iterator of the children of the specified file. If the specified
318: * file is not a directory, this will return an empty iterator.
319: *
320: * @param path the path to the file to get children of
321: * @return Collection of AbstractFile
322: * @see #children(AbstractFile)
323: */
324: public static synchronized Collection children(String path)
325: throws IOException {
326: path = (path == null) ? "/" : normalize(path);
327:
328: /* NOTE: if one of the children is itself a mount point, we need to filter
329: * out children with same name from the iteration returned by the fs
330: * implementation, because they will be eclipsed by the mount point.
331: * Also, we need to emulate directory elements that don't exist for
332: * mount points below the requested path.
333: */
334:
335: // find filesystem containing requested path:
336: MountPoint mp = getMountPoint(path);
337:
338: // list of mount points, which may eclipse other children, under this path:
339: Hashtable eclipsingChildTable = new Hashtable();
340:
341: for (Enumeration e = mpTable.keys(); e.hasMoreElements();) {
342: String mountPoint = (String) (e.nextElement());
343:
344: if (mountPoint.startsWith(path)
345: && !mp.equals(mpTable.get(mountPoint))) {
346: int idx = mountPoint.indexOf(SEPERATOR_CHAR, path
347: .length() + 1);
348: if (idx == -1)
349: idx = mountPoint.length();
350: String childName = mountPoint.substring(
351: path.length() + 1, idx);
352: eclipsingChildTable.put(childName, childName);
353: }
354: }
355:
356: Collection children = mp.getFs().childrenInFileSystem(
357: mp.getMountPath(), mp.getRelativePath(path));
358:
359: // if needed, touch up iterator by adding/replacing children...
360: if (eclipsingChildTable.size() > 0) {
361: LinkedList modifiedList = new LinkedList();
362: Iterator childIterator = children.iterator();
363:
364: // remove eclipsed children:
365: while (childIterator.hasNext()) {
366: AbstractFile file = (AbstractFile) (childIterator
367: .next());
368: boolean eclipsed = false;
369:
370: for (Enumeration e = eclipsingChildTable.keys(); e
371: .hasMoreElements();) {
372: if (file.getName().equals(e.nextElement())) {
373: eclipsed = true;
374: break;
375: }
376: }
377:
378: if (!eclipsed)
379: modifiedList.add(file);
380: }
381:
382: // now add the eclipsing files:
383: for (Enumeration e = eclipsingChildTable.keys(); e
384: .hasMoreElements();) {
385: String fullPath = path + SEPERATOR_CHAR
386: + e.nextElement();
387: AbstractFile file = resolve(fullPath);
388: modifiedList.add(file);
389: }
390:
391: children = modifiedList;
392: }
393:
394: return children;
395: }
396:
397: /**
398: * Get an iterator of the children of the specified file. If the specified
399: * file is not a directory, this will return an empty iterator.
400: *
401: * @param file the file to get children of
402: * @return Collection of AbstractFile
403: * @see #children(String)
404: */
405: public static Collection children(AbstractFile file)
406: throws IOException {
407: return children((file == null) ? "/" : file.getPath());
408: }
409:
410: /**
411: * Try to resolve the specified path. If unresolved, return <code>null</code>.
412: * Note that this gets called under the synchronization of the abstract file
413: * system, so it does not need to be re-entrant.
414: *
415: * @param mountPath the path this fs is mounted at to resolve the requested file
416: * @param path path to file, relative to <code>mountPath</code>
417: * @return file or <code>null</code>
418: */
419: protected abstract AbstractFile resolveInFileSystem(
420: String mountPath, String path) throws IOException;
421:
422: /**
423: * Return an iterator of children of the specified path.
424: *
425: * @param mountPath the path this fs is mounted at to resolve the requested file
426: * @param path path to file, relative to <code>mountPath</code>
427: * @return a collection of <code>AbstractFile</code>
428: */
429: protected abstract Collection childrenInFileSystem(
430: String mountPath, String path) throws IOException;
431:
432: /**
433: * Flush any pending changes within this filesystem.
434: */
435: protected abstract void flush() throws IOException;
436:
437: /**
438: * Return the last modification time of the root of the mount point itself. This
439: * is the same as the last modified time for the root of the filesystem.
440: */
441: protected long lastModified() throws IOException {
442: return resolveInFileSystem("", "").lastModified();
443: }
444:
445: /**
446: * Container for mount point information.
447: */
448: private static class MountPoint {
449: private String mountPath;
450: private AbstractFileSystem fs;
451:
452: MountPoint(String mountPath, AbstractFileSystem fs) {
453: this .mountPath = mountPath;
454: this .fs = fs;
455: }
456:
457: String getMountPath() {
458: return mountPath;
459: }
460:
461: AbstractFileSystem getFs() {
462: return fs;
463: }
464:
465: String getRelativePath(String path) {
466: // strip the mount path off path:
467: if (path.equals(mountPath))
468: return "";
469: else
470: return path.substring(mountPath.length() + 1);
471: }
472: }
473:
474: /**
475: * The UnionFileSystem is used to combine multiple mount points.
476: */
477: private static class UnionFileSystem extends AbstractFileSystem {
478: private LinkedList fsList = new LinkedList();
479:
480: void addFileSystem(AbstractFileSystem fs) {
481: fsList.add(fs);
482: }
483:
484: protected long lastModified() throws IOException {
485: return lastMountTime;
486: }
487:
488: protected AbstractFile resolveInFileSystem(String mountPath,
489: String path) throws IOException {
490: AbstractFile file = null;
491:
492: for (Iterator itr = fsList.iterator(); itr.hasNext();) {
493: AbstractFileSystem fs = (AbstractFileSystem) (itr
494: .next());
495:
496: file = fs.resolveInFileSystem(mountPath, path);
497:
498: if ((file != null) && file.exists())
499: return file;
500: }
501:
502: return file;
503: }
504:
505: protected Collection childrenInFileSystem(String mountPath,
506: String path) throws IOException {
507: LinkedList children = new LinkedList();
508:
509: for (Iterator itr = fsList.iterator(); itr.hasNext();) {
510: AbstractFileSystem fs = (AbstractFileSystem) (itr
511: .next());
512: children.addAll(fs
513: .childrenInFileSystem(mountPath, path));
514: }
515:
516: return children;
517: }
518:
519: protected void flush() throws IOException {
520: for (Iterator itr = fsList.iterator(); itr.hasNext();) {
521: AbstractFileSystem fs = (AbstractFileSystem) (itr
522: .next());
523: fs.flush();
524: }
525: }
526: }
527:
528: /**
529: * Since instances of BogusFile/MountpointFile are created as needed,
530: * and not tracked or cached, we need to track the timestamps in such
531: * a way that there is a 1:1 correspondence between normalized path
532: * and timestamp.
533: * <p>
534: * XXX perhaps we should serialize this table, to preserve timestamps
535: * across VM restarts?
536: */
537: private static Hashtable bogusFileTimestampTable = new Hashtable();
538:
539: /**
540: * A non-existant file which is created to simulate non-existant
541: * directories that are in a path to a mount point, or files which
542: * simply don't exist.
543: */
544: protected static class BogusFile implements AbstractFile {
545: private String path;
546: private boolean exists;
547: private boolean isDirectory;
548: private boolean isFile;
549:
550: BogusFile(String path, boolean exists, boolean isDirectory,
551: boolean isFile) {
552: if ("".equals(path))
553: path = "/";
554: this .path = path;
555: this .exists = exists;
556: this .isDirectory = isDirectory;
557: this .isFile = isFile;
558: ;
559:
560: if (bogusFileTimestampTable.get(path) == null)
561: touch();
562: }
563:
564: public boolean equals(Object obj) {
565: return (obj instanceof BogusFile)
566: && ((BogusFile) obj).getPath().equals(getPath());
567: }
568:
569: public int hashCode() {
570: return getPath().hashCode();
571: }
572:
573: public InputStream getInputStream() throws IOException {
574: throw new IOException("not supported");
575: }
576:
577: public OutputStream getOutputStream(boolean append)
578: throws IOException {
579: throw new IOException("not supported");
580: }
581:
582: public String getPath() {
583: return path;
584: }
585:
586: public String getName() {
587: return basename(path);
588: }
589:
590: public long lastModified() {
591: return ((Long) (bogusFileTimestampTable.get(path)))
592: .longValue();
593: }
594:
595: public long length() {
596: return 0;
597: }
598:
599: public boolean canRead() {
600: return true;
601: }
602:
603: public boolean canWrite() {
604: return false;
605: }
606:
607: public boolean exists() {
608: return exists;
609: }
610:
611: public boolean isDirectory() {
612: return isDirectory;
613: }
614:
615: public boolean isFile() {
616: return isFile;
617: }
618:
619: public boolean createNewFile() throws IOException {
620: throw new IOException("not supported");
621: }
622:
623: public void touch() {
624: bogusFileTimestampTable.put(path, new Long(System
625: .currentTimeMillis()));
626: }
627:
628: public boolean delete() throws IOException {
629: throw new IOException("cannot delete");
630: }
631:
632: public boolean mkdir() throws IOException {
633: throw new IOException("cannot mkdir");
634: }
635:
636: public boolean mkdirs() throws IOException {
637: throw new IOException("cannot mkdirs");
638: }
639:
640: public String getExtension() {
641: return "";
642: }
643:
644: public String toString() {
645: return getPath();
646: }
647: }
648:
649: /**
650: * A mount point file doesn't actually exist, but is used to simulate
651: * non existant components of the mount path of some file system. It
652: * is a directory and is not modifiable.
653: */
654: public static class MountPointFile extends BogusFile {
655: private AbstractFileSystem fs = null;
656:
657: MountPointFile(String path) {
658: super (path, true, true, false);
659: }
660:
661: // hmm, kinda hacky, but I want the FileSystemTreeModel to know the
662: // type so it can display an appropriate icon/info
663: public AbstractFileSystem getFs() {
664: if (fs == null)
665: fs = getMountPoint(getPath()).getFs();
666: return fs;
667: }
668:
669: public String getMountType() {
670: AbstractFileSystem fs = getFs();
671: if (fs instanceof LocalFileSystem)
672: return "/lfs/";
673: else if (fs instanceof JarFileSystem)
674: return "/jfs/";
675: else
676: return "/vfs/";
677: }
678:
679: public long lastModified() {
680: long t = -1;
681: try {
682: t = getFs().lastModified();
683: } catch (Throwable e) {
684: }
685: if (t == -1)
686: t = super .lastModified();
687: return t;
688: }
689: }
690: }
691:
692: /*
693: * Local Variables:
694: * tab-width: 2
695: * indent-tabs-mode: nil
696: * mode: java
697: * c-indentation-style: java
698: * c-basic-offset: 2
699: * eval: (c-set-offset 'substatement-open '0)
700: * eval: (c-set-offset 'case-label '+)
701: * eval: (c-set-offset 'inclass '+)
702: * eval: (c-set-offset 'inline-open '0)
703: * End:
704: */
|