KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > de > schlichtherle > io > ArchiveFileSystem


1 /*
2  * ArchiveFileSystem.java
3  *
4  * Created on 3. November 2004, 21:57
5  */

6 /*
7  * Copyright 2005-2006 Schlichtherle IT Services
8  *
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  * http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  */

21
22 package de.schlichtherle.io;
23
24 import de.schlichtherle.io.archive.spi.ArchiveEntry;
25 import de.schlichtherle.io.archive.spi.InputArchive;
26
27 import java.io.CharConversionException JavaDoc;
28 import java.io.FileFilter JavaDoc;
29 import java.io.FilenameFilter JavaDoc;
30 import java.io.IOException JavaDoc;
31 import java.util.Collections JavaDoc;
32 import java.util.Comparator JavaDoc;
33 import java.util.Enumeration JavaDoc;
34 import java.util.HashMap JavaDoc;
35 import java.util.LinkedList JavaDoc;
36 import java.util.Map JavaDoc;
37 import java.util.TreeMap JavaDoc;
38 import javax.swing.Icon JavaDoc;
39
40 /**
41  * This class implements a virtual file system of archive entries for use
42  * by the archive controller provided to the constructor.
43  * <p>
44  * <b>WARNING:</b>This class is <em>not</em> thread safe!
45  * All calls to non-static methods <em>must</em> be synchronized on the
46  * respective <tt>ArchiveController</tt> object!
47  *
48  * @author Christian Schlichtherle
49  * @version @version@
50  * @since TrueZIP 6.0 (refactored from the former <code>ZipFileSystem</code>)
51  */

52 final class ArchiveFileSystem implements FileConstants {
53
54     /**
55      * The entry name of the root directory.
56      * This is for use by the {@link ArchiveController} class only!
57      */

58     static final String JavaDoc ROOT = ENTRY_SEPARATOR;
59
60     /** A comparator for entry names in reverse order. */
61     private static final Comparator JavaDoc REVERSE_ENTRIES_COMPARATOR
62             = new Comparator JavaDoc() {
63         public int compare(Object JavaDoc s1, Object JavaDoc s2) {
64             // Thanks to Robert Courchaine for the following patch, which
65
// makes it work for non-Sun J2SE APIs.
66
return ((String JavaDoc) s2).compareTo((String JavaDoc) s1); // reverse order
67
//return ((String) s2).compareTo(s1); // reverse order
68
}
69     };
70
71     /**
72      * A path split utility which knows the format of entry names in archive
73      * files.
74      *
75      * @param entryName The name of the entry which's parent and base name
76      * are to be returned in <tt>result</tt>.
77      * @param result An array of at least two strings:
78      * <ul>
79      * <li>Index 0 holds the parent name or <tt>null</tt> if the
80      * entry does not name a parent. If a parent exists, its name
81      * will always end with a slash.</li>
82      * <li>Index 1 holds the base name.</li>
83      * </ul>
84      *
85      * @return <tt>result</tt>.
86      */

87     // Note: The only reason why this is not private is to enable unit tests.
88
static String JavaDoc[] split(final String JavaDoc entryName, final String JavaDoc[] result) {
89         assert entryName != null;
90         assert result != null;
91         assert result.length >= 2;
92
93         // Calculate index of last character, ignoring trailing slash.
94
int end = entryName.length();
95         if (--end >= 0)
96             if (entryName.charAt(end) == ENTRY_SEPARATOR_CHAR)
97                 end--;
98
99         // Now look for the slash.
100
int i = entryName.lastIndexOf(ENTRY_SEPARATOR_CHAR, end);
101         end++;
102
103         // Finally split according to our findings.
104
if (i != -1) { // found slash?
105
i++;
106             result[0] = entryName.substring(0, i); // include separator, may produce only separator!
107
result[1] = entryName.substring(i, end); // between separator and trailing separator
108
} else { // no slash
109
if (end > 0) { // At least one character exists, excluding a trailing separator?
110
result[0] = ROOT;
111             } else {
112                 result[0] = null; // no parent
113
}
114             result[1] = entryName.substring(0, end); // between prefix and trailing separator
115
}
116
117         return result;
118     }
119     
120     /**
121      * The controller that this filesystem belongs to.
122      */

123     private final GeneralArchiveController controller;
124
125     /**
126      * The read only status of this file system.
127      */

128     private final boolean readOnly; // Defaults to false!
129

130     /**
131      * The map of ArchiveEntries in this file system.
132      * If this is a read-only file system, this is actually an unmodifiable
133      * map. This field should be considered final!
134      * <p>
135      * Note that the ArchiveEntries in this map are shared with the
136      * {@link InputArchive} object provided to this class' constructor.
137      */

138     private Map JavaDoc master;
139     
140     private final ArchiveEntry root;
141
142     private long modCount;
143     
144     /**
145      * For use by {@link #fixParents(ArchiveEntry)} and
146      * {@link #unlink(String)} only!
147      */

148     private final String JavaDoc[] split = new String JavaDoc[2];
149     
150     /**
151      * Creates a new archive file system and ensures its integrity.
152      * The root directory is created with its last modification time set to
153      * the system's current time.
154      * The file system is modifiable and marked as touched!
155      *
156      * @param controller The controller which will use this file system.
157      * This constructor will finally call
158      * {@link ArchiveController#fileSystemTouched} once it has fully
159      * initialized this instance.
160      */

161     ArchiveFileSystem(final GeneralArchiveController controller)
162     throws IOException JavaDoc {
163         this.controller = controller;
164         modCount = 1;
165         master = new CompoundMap(REVERSE_ENTRIES_COMPARATOR, 64);
166         root = createArchiveEntry(ROOT, null);
167         root.setTime(System.currentTimeMillis());
168         master.put(ROOT, root);
169         readOnly = false;
170         controller.touch();
171     }
172     
173     /**
174      * Mounts the archive file system from <tt>archive</tt> and ensures its
175      * integrity.
176      * First, a root directory with the given last modification time is
177      * created - it's never loaded from the archive!
178      * Then the entries from the archive are loaded into the file system and
179      * its integrity is checked:
180      * Any missing parent directories are created using the system's current
181      * time as their last modification time - existing directories will never
182      * be replaced.
183      * <p>
184      * Note that the entries in this file system are shared with
185      * <code>archive</code>.
186      *
187      * @param controller The controller which will use this file system.
188      * This constructor will solely use the controller as a factory
189      * to create missing archive entries using
190      * {@link GeneralArchiveController#createArchiveEntry}.
191      * @param archive The archive to mount the file system from.
192      * @param rootTime The last modification time of the root of the mounted
193      * file system in milliseconds since the epoch.
194      * @param readOnly If and only if <code>true</code>, any subsequent
195      * modifying operation will result in a
196      * {@link ArchiveReadOnlyException}.
197      */

198     ArchiveFileSystem(
199             final GeneralArchiveController controller,
200             final InputArchive archive,
201             final long rootTime,
202             final boolean readOnly) {
203         this.controller = controller;
204
205         final int iniCap = (int) (archive.getNumArchiveEntries() / 0.75f);
206         master = new CompoundMap(REVERSE_ENTRIES_COMPARATOR, iniCap);
207
208         try {
209             // Setup root.
210
root = createArchiveEntry(ROOT, null);
211             // Do NOT yet touch the file system!
212
root.setTime(rootTime);
213             master.put(ROOT, root);
214
215             Enumeration JavaDoc entries = archive.getArchiveEntries();
216             while (entries.hasMoreElements()) {
217                 final ArchiveEntry entry = (ArchiveEntry) entries.nextElement();
218                 final String JavaDoc name = entry.getName();
219                 // Map entry if it doesn't address the implicit root directory.
220
if (!ROOT.equals(name) && !("." + ENTRY_SEPARATOR).equals(name)) {
221                     entry.setMetaData(new ArchiveEntryMetaData(entry));
222                     master.put(name, entry);
223                 }
224             }
225
226             // Now perform a file system check to fix missing parent directories.
227
// This needs to be done separately!
228
entries = archive.getArchiveEntries();
229             while (entries.hasMoreElements()) {
230                 final ArchiveEntry entry = (ArchiveEntry) entries.nextElement();
231                 if (isLegalEntryName(entry.getName()))
232                     fixParents(entry);
233             }
234         } catch (CharConversionException JavaDoc cannotHappen) {
235             // The slash character is part of all character sets!
236
throw new AssertionError JavaDoc(cannotHappen);
237         }
238
239         // Reset master map to be unmodifiable if this is a readonly file system
240
this.readOnly = readOnly;
241         if (readOnly)
242             master = Collections.unmodifiableMap(master);
243
244         assert modCount == 0; // don't call !isTouched() - preconditions not yet met!
245
}
246
247     /**
248      * Checks whether the given entry entryName is a legal entry entryName.
249      * A legal entry name does not name the root directory (<code>"/"</code>)
250      * or the dot directory (<code>"."</code>) or the dot-dot directory
251      * (<code>".."</code>) or any of their descendants.
252      */

253     private static boolean isLegalEntryName(final String JavaDoc entryName) {
254         final int l = entryName.length();
255
256         if (l <= 0)
257             return false; // never fix empty pathnames
258

259         switch (entryName.charAt(0)) {
260         case ENTRY_SEPARATOR_CHAR:
261             return false; // never fix root or absolute pathnames
262

263         case '.':
264             if (l >= 2) {
265                 switch (entryName.charAt(1)) {
266                 case '.':
267                     if (l >= 3) {
268                         if (entryName.charAt(2) == ENTRY_SEPARATOR_CHAR) {
269                             assert entryName.startsWith(".." + ENTRY_SEPARATOR);
270                             return false;
271                         }
272                         // Fall through.
273
} else {
274                         assert "..".equals(entryName);
275                         return false;
276                     }
277                     break;
278
279                 case ENTRY_SEPARATOR_CHAR:
280                     assert entryName.startsWith("." + ENTRY_SEPARATOR);
281                     return false;
282
283                 default:
284                     // Fall through.
285
}
286             } else {
287                 assert ".".equals(entryName);
288                 return false;
289             }
290             break;
291
292         default:
293             // Fall through.
294
}
295         
296         return true;
297     }
298
299     /**
300      * Called from a constructor to fix the parent directories of
301      * <tt>entry</tt>, ensuring that all parent directories of the entry
302      * exist and that they contain the respective child.
303      * If a parent directory does not exist, it is created using an
304      * unkown time as the last modification time - this is defined to be a
305      * <i>ghost<i> directory.
306      * If a parent directory does exist, the respective child is added
307      * (possibly yet again) and the process is continued.
308      */

309     private void fixParents(final ArchiveEntry entry)
310     throws CharConversionException JavaDoc {
311         final String JavaDoc name = entry.getName();
312         // When recursing into this method, it may be called with the root
313
// directory as its parameter, so we may NOT skip the following test.
314
if (name.length() <= 0 || name.charAt(0) == ENTRY_SEPARATOR_CHAR)
315             return; // never fix root or empty or absolute pathnames
316
assert isLegalEntryName(name);
317
318         final String JavaDoc split[] = split(name, this.split);
319         final String JavaDoc parentName = split[0];
320         final String JavaDoc baseName = split[1];
321
322         ArchiveEntry parent = (ArchiveEntry) master.get(parentName);
323         if (parent == null) {
324             parent = createArchiveEntry(parentName, null);
325             //parent.setTime(0); // mark as ghost directory!
326
master.put(parentName, parent);
327         }
328
329         fixParents(parent);
330         parent.getMetaData().children.add(baseName);
331     }
332
333     /**
334      * Indicates whether this file system is read only or not.
335      * The default is <tt>false</tt>.
336      */

337     boolean isReadOnly() {
338         return readOnly;
339     }
340
341     /**
342      * Ensures that the controller's data structures required to output
343      * entries are properly initialized and marks this virtual archive
344      * file system as touched.
345      *
346      * @throws ArchiveReadOnlyExceptionn If this virtual archive file system
347      * is read only.
348      * @throws IOException If setting up the required data structures in the
349      * controller fails for some reason.
350      */

351     private void touch() throws IOException JavaDoc {
352         if (isReadOnly())
353             throw new ArchiveReadOnlyException();
354
355         // Order is important here because of exceptions!
356
if (modCount == 0)
357             controller.touch();
358         modCount++;
359     }
360     
361     /**
362      * Indicates whether this file system has been modified since
363      * its time of creation or the last call to <tt>resetTouched()</tt>.
364      */

365     boolean isTouched() {
366         assert controller.getFileSystem() == this;
367         return modCount != 0;
368     }
369
370     /**
371      * Resets this file system's touch status so that an instant call to
372      * <tt>isTouched()</tt> will return false.
373      */

374     private void resetTouched() {
375         modCount = 0;
376     }
377
378     /**
379      * Returns an enumeration of all ArchiveEntry objects in this file system
380      * in reversed pathname order, i.e. all getArchiveEntries in a directory are
381      * enumerated before their containing directory.
382      * <p>
383      * Example:<pre>
384      * a/b/c
385      * a/b/
386      * a/
387      * /
388      * </pre>
389      */

390     Enumeration JavaDoc getReversedEntries() {
391         assert controller.getFileSystem() == this;
392         // The comparator already reverses the archive entries!
393
return Collections.enumeration(master.values());
394     }
395     
396     /**
397      * Returns the root directory of this file system. This will always
398      * exist.
399      */

400     private ArchiveEntry getRoot() {
401         return root;
402     }
403
404     /**
405      * Looks up the specified entry in the file system and returns it or
406      * <tt>null</tt> if not existent.
407      */

408     ArchiveEntry get(String JavaDoc entryName) {
409         assert controller.getFileSystem() == this;
410         return (ArchiveEntry) master.get(entryName);
411     }
412
413     /**
414      * Equivalent to {@link #beginCreateAndLink(String, boolean, ArchiveEntry)
415      * beginCreateAndLink(entryName, createParents, null)}.
416      */

417     Delta beginCreateAndLink(
418             final String JavaDoc entryName,
419             final boolean createParents)
420     throws CharConversionException JavaDoc, ArchiveIllegalOperationException {
421         assert controller.getFileSystem() == this;
422         return new CreateAndLinkDelta(entryName, createParents, null);
423     }
424
425     /**
426      * Begins a "create and link entry" transaction to ensure that either a
427      * new entry for the given <tt>entryName</tt> will be created or an
428      * existing entry is replaced within this virtual archive file system.
429      * <p>
430      * This is the first step of a two-step process to create an archive entry
431      * and link it into this virtual archive file system.
432      * To commit the transaction, call {@link Delta#commit} on the returned object
433      * after you have successfully conducted the operations which compose the
434      * transaction.
435      * <p>
436      * Upon a <code>commit</code> operation, the last modification time of
437      * the newly created and linked entries will be set to the system's
438      * current time at the moment the transaction has begun and the file
439      * system will be marked as touched at the moment the transaction has
440      * been committed.
441      * <p>
442      * Note that there is no rollback operation: After this method returns,
443      * nothing in the virtual file system has changed yet and all information
444      * required to commit the transaction is contained in the returned object.
445      * Hence, if the operations which compose the transaction fails, the
446      * returned object may be safely collected by the garbage collector,
447      *
448      * @param entryName The full path name of the entry to create or replace.
449      * This must be a relative path name.
450      * @param createParents If <tt>true</tt>, any non-existing parent
451      * directory will be created in this file system with its last
452      * modification time set to the system's current time.
453      * @param blueprint If not <code>null</code>, then the newly created or
454      * replaced entry shall inherit as much attributes from this
455      * object as possible (with the exception of the name).
456      * This is typically used for archive copy operations and requires
457      * some support by the archive driver.
458      * However, the last modification time is always retained.
459      * @throws CharConversionException If <code>entryName</code> contains
460      * characters which cannot be represented by the underlying
461      * archive driver.
462      * @throws ArchiveIllegalOperationException If one of the following is true:
463      * <ul>
464      * <li>The entry name indicates a directory (trailing <tt>/</tt>)
465      * and its entry does already exist within this file system.
466      * <li>The entry is a file or directory and does already exist as
467      * the respective other type within this file system.
468      * <li>The parent directory does not exist and
469      * <tt>createParents</tt> is <tt>false</tt>.
470      * <li>One of the entry's parents denotes a file.
471      * </ul>
472      * @throws ArchiveReadOnlyExceptionn If this virtual archive file system
473      * is read only.
474      * @return A transaction object. You must call its
475      * {@link Delta#commit} method in order to commit
476      * link the newly created entry into this virtual archive file
477      * system.
478      */

479     Delta beginCreateAndLink(
480             final String JavaDoc entryName,
481             final boolean createParents,
482             final ArchiveEntry blueprint)
483     throws CharConversionException JavaDoc, ArchiveIllegalOperationException {
484         assert controller.getFileSystem() == this;
485         return new CreateAndLinkDelta(entryName, createParents, blueprint);
486     }
487
488     /**
489      * A simple transaction for creating (and hence probably replacing) and
490      * linking an entry in this virtual archive file system.
491      *
492      * @see #beginCreateAndLink
493      */

494     private class CreateAndLinkDelta extends AbstractDelta {
495         private final long time = System.currentTimeMillis();
496         private final Element[] elements;
497
498         private CreateAndLinkDelta(
499                 final String JavaDoc entryName,
500                 final boolean createParents,
501                 final ArchiveEntry blueprint)
502         throws ArchiveIllegalOperationException, CharConversionException JavaDoc {
503             assert entryName.length() > 0;
504             assert entryName.charAt(0) != ENTRY_SEPARATOR_CHAR;
505
506             if (isReadOnly())
507                 throw new ArchiveReadOnlyException();
508             elements = createElements(entryName, createParents, blueprint, 1);
509         }
510
511         private Element[] createElements(
512                 final String JavaDoc entryName,
513                 final boolean createParents,
514                 final ArchiveEntry blueprint,
515                 final int level)
516         throws ArchiveIllegalOperationException, CharConversionException JavaDoc {
517             // First, retrieve the parent's entryName and base name.
518
final String JavaDoc split[]
519                     = split(entryName, ArchiveFileSystem.this.split);
520             final String JavaDoc parentName = split[0]; // could be separator only to indicate root
521
final String JavaDoc baseName = split[1];
522
523             final Element[] elements;
524
525             // Lookup parent entry, creating it where necessary and allowed.
526
final ArchiveEntry parent = (ArchiveEntry) master.get(parentName);
527             final ArchiveEntry entry;
528             if (parent != null) {
529                 final ArchiveEntry oldEntry
530                         = (ArchiveEntry) master.get(entryName);
531                 ensureMayBeReplaced(entryName, oldEntry);
532                 elements = new Element[level + 1];
533                 elements[0] = new Element(File.EMPTY, parent);
534                 entry = createArchiveEntry(entryName, level == 1 ? blueprint : null);
535                 elements[1] = new Element(baseName, entry);
536             } else if (createParents) {
537                 elements = createElements(
538                         parentName, createParents, blueprint, level + 1);
539                 entry = createArchiveEntry(entryName, level == 1 ? blueprint : null);
540                 elements[elements.length - level]
541                         = new Element(baseName, entry);
542             } else {
543                 throw new ArchiveIllegalOperationException(
544                         entryName, "Missing parent directory!");
545             }
546             if (blueprint != null && level == 1) {
547                 // According to the contract of ArchiveDriver, this should
548
// have been done by ArchiveDriver.createArchiveEntry(),
549
// but we want to play it safe.
550
entry.setTime(blueprint.getTime());
551             } else {
552                 entry.setTime(time);
553             }
554
555             return elements;
556         }
557
558         private void ensureMayBeReplaced(
559                 final String JavaDoc entryName,
560                 final ArchiveEntry oldEntry)
561         throws ArchiveIllegalOperationException {
562             final int end = entryName.length() - 1;
563             if (entryName.charAt(end) == ENTRY_SEPARATOR_CHAR) { // entryName indicates directory
564
if (oldEntry != null)
565                     throw new ArchiveIllegalOperationException(entryName,
566                             "Directories cannot be replaced!");
567                 if (master.get(entryName.substring(0, end)) != null)
568                     throw new ArchiveIllegalOperationException(entryName,
569                             "Directories cannot replace files!");
570             } else { // entryName indicates file
571
if (master.get(entryName + ENTRY_SEPARATOR) != null)
572                     throw new ArchiveIllegalOperationException(entryName,
573                             "Files cannot replace directories!");
574             }
575         }
576
577         /** Links the entries into this virtual archive file system. */
578         public void commit() throws IOException JavaDoc {
579             assert controller.getFileSystem() == ArchiveFileSystem.this;
580             touch();
581             ArchiveEntry parent = elements[0].entry;
582             for (int i = 1, l = elements.length; i < l ; i++) {
583                 final Element element = elements[i];
584                 final String JavaDoc baseName = element.baseName;
585                 final ArchiveEntry entry = element.entry;
586                 if (parent.getMetaData().children.add(baseName)
587                         && parent.getTime() >= 0) {
588                     parent.setTime(System.currentTimeMillis()); // NOT time!
589
}
590                 master.put(entry.getName(), entry);
591                 parent = entry;
592             }
593         }
594
595         public ArchiveEntry getEntry() {
596             assert controller.getFileSystem() == ArchiveFileSystem.this;
597             return elements[elements.length - 1].entry;
598         }
599     } // class CreateAndLinkDelta
600

601     private static abstract class AbstractDelta implements Delta {
602         /** A data class for use by subclasses. */
603         protected static class Element {
604             protected final String JavaDoc baseName;
605             protected final ArchiveEntry entry;
606
607             // This constructor is provided for convenience only.
608
protected Element(String JavaDoc baseName, ArchiveEntry entry) {
609                 this.baseName = baseName; // may be null!
610
assert entry != null;
611                 this.entry = entry;
612             }
613         }
614     } // class AbstractDelta
615

616     /**
617      * This interface encapsulates the methods required to begin and commit
618      * a simplified transaction (a delta) on this virtual archive file system.
619      * <p>
620      * Note that there is no <code>begin</code> or <code>rollback</code>
621      * method in this class.
622      * Instead, <code>begin</code> is expected to be implemented by the
623      * constructor of the implementation and must not modify the file system,
624      * so that an explicit <code>rollback</code> is not required.
625      */

626     interface Delta {
627         /**
628          * Returns the entry operated by this delta.
629          */

630         ArchiveEntry getEntry();
631
632         /**
633          * Commits the simplified transaction, possibly modifying this
634          * virtual archive file system.
635          *
636          * @throws IOException If the commit operation fails for any I/O
637          * related reason.
638          */

639         void commit() throws IOException JavaDoc;
640     }
641
642     /**
643      * Creates an archive entry which is going to be linked into this virtual
644      * archive file system in the near future.
645      * The returned entry has properly initialized meta data, but is
646      * otherwise left as created by the archive driver.
647      *
648      * @param name The path name of the entry to create or replace.
649      * This must be a relative path name.
650      * @param blueprint If not <code>null</code>, then the newly created entry
651      * shall inherit as much attributes from this object as possible
652      * (with the exception of the name).
653      * This is typically used for archive copy operations and requires
654      * some support by the archive driver.
655      *
656      * @return An {@link ArchiveEntry} created by the archive driver and
657      * properly initialized with meta data.
658      *
659      * @throws CharConversionException If <code>entryName</code> contains
660      * characters which cannot be represented by the underlying
661      * archive driver.
662      */

663     private ArchiveEntry createArchiveEntry(String JavaDoc name, ArchiveEntry blueprint)
664     throws CharConversionException JavaDoc {
665         ArchiveEntry entry = controller.createArchiveEntry(name, blueprint);
666         entry.setMetaData(new ArchiveEntryMetaData(entry));
667         return entry;
668     }
669
670     /**
671      * If this method returns, the entry identified by the given
672      * <tt>entryName</tt> has been successfully deleted from the virtual
673      * archive file system.
674      * If the entry is a directory, it must be empty for successful deletion.
675      *
676      * @return Only if the entry has been successfully deleted from the
677      * virtual file system.
678      *
679      * @throws ArchiveReadOnlyExceptionn If the virtual archive file system is
680      * read only.
681      * @throws ArchiveIllegalOperationException If the operation failed for
682      * any other reason.
683      */

684     private void unlink(final String JavaDoc entryName)
685     throws IOException JavaDoc {
686         assert entryName.length() > 0;
687         assert entryName.charAt(0) != ENTRY_SEPARATOR_CHAR;
688
689         try {
690             final ArchiveEntry entry = (ArchiveEntry) master.remove(entryName);
691             if (entry == null)
692                 throw new ArchiveIllegalOperationException(entryName,
693                         "Entry does not exist!");
694             if (entry == root
695                     || entry.isDirectory() && entry.getMetaData().children.size() != 0) {
696                 master.put(entryName, entry); // Restore file system
697
throw new ArchiveIllegalOperationException(entryName,
698                         "Directory is not empty!");
699             }
700             final String JavaDoc split[] = split(entryName, this.split);
701             final String JavaDoc parentName = split[0];
702             final ArchiveEntry parent = (ArchiveEntry) master.get(parentName);
703             assert parent != null : "The parent directory of \"" + entryName
704                         + "\" is missing - archive file system is corrupted!";
705             final boolean ok = parent.getMetaData().children.remove(split[1]);
706             assert ok : "The parent directory of \"" + entryName
707                         + "\" does not contain this entry - archive file system is corrupted!";
708             touch();
709             parent.setTime(System.currentTimeMillis());
710         }
711         catch (UnsupportedOperationException JavaDoc unmodifiableMap) {
712             throw new ArchiveReadOnlyException();
713         }
714     }
715
716     //
717
// Exceptions:
718
//
719

720     /**
721      * This exception is thrown when a client tries to perform an illegal
722      * operation on an archive file system.
723      * <p>
724      * This exception is private by intention: Clients should not even
725      * know about the existence of virtual archive file systems.
726      * Most methods in the {@link File} class will catch this exception and
727      * return the boolean value <code>false</code> instead in order to
728      * overwrite a super class method.
729      * Even if not (e.g. with {@link File#createNewFile()}, a client
730      * will just see "some subclass of an {@link IOException}".
731      */

732     private static class ArchiveIllegalOperationException extends IOException JavaDoc {
733         /** The entry's path name. */
734         private final String JavaDoc entryName;
735
736         private ArchiveIllegalOperationException(String JavaDoc message) {
737             super(message);
738             this.entryName = null;
739         }
740
741         private ArchiveIllegalOperationException(String JavaDoc entryName, String JavaDoc message) {
742             super(message);
743             this.entryName = entryName;
744         }
745
746         public String JavaDoc getMessage() {
747             // For performance reasons, this string is constructed on demand
748
// only!
749
if (entryName != null)
750                 return entryName + " (" + super.getMessage() + ")";
751             else
752                 return super.getMessage();
753         }
754     }
755
756     /**
757      * This exception is thrown when a client tries to modify a read only
758      * virtual archive file system.
759      */

760     private static class ArchiveReadOnlyException extends ArchiveIllegalOperationException {
761         private ArchiveReadOnlyException() {
762             super("This archive is read-only!");
763         }
764     }
765
766     //
767
// File system operations used by the ArchiveController class:
768
//
769

770     boolean exists(final String JavaDoc entryName) {
771         return get(entryName) != null || get(entryName + ENTRY_SEPARATOR) != null;
772     }
773     
774     boolean isFile(final String JavaDoc entryName) {
775         return get(entryName) != null;
776     }
777     
778     boolean isDirectory(final String JavaDoc entryName) {
779         return get(entryName + ENTRY_SEPARATOR) != null;
780     }
781
782     Icon JavaDoc getOpenIcon(final String JavaDoc entryName) {
783         assert !EMPTY.equals(entryName);
784
785         ArchiveEntry entry = get(entryName);
786         if (entry == null)
787             entry = get(entryName + ENTRY_SEPARATOR_CHAR);
788         return entry != null ? entry.getOpenIcon() : null;
789     }
790
791     Icon JavaDoc getClosedIcon(final String JavaDoc entryName) {
792         assert !EMPTY.equals(entryName);
793
794         ArchiveEntry entry = get(entryName);
795         if (entry == null)
796             entry = get(entryName + ENTRY_SEPARATOR_CHAR);
797         return entry != null ? entry.getClosedIcon() : null;
798     }
799     
800     boolean canWrite(final String JavaDoc entryName) {
801         return !isReadOnly() && exists(entryName);
802     }
803
804     boolean setReadOnly(final String JavaDoc entryName) {
805         return isReadOnly() && exists(entryName);
806     }
807     
808     long length(final String JavaDoc entryName) {
809         final ArchiveEntry entry = get(entryName);
810         if (entry != null) {
811             // TODO: Review: Can we avoid this special case? It's probably Zip32Driver specific!
812
// This entry is a plain file in the file system.
813
// If entry.getSize() returns -1, the length is yet unknown.
814
// This may happen if e.g. a ZIP entry has only been partially
815
// written, i.e. not yet closed by another thread, or if this is
816
// a ghost directory.
817
// As this is not specified in the contract of the File class,
818
// return 0 in this case instead.
819
final long length = entry.getSize();
820             return length != -1 ? length : 0;
821         }
822         // This entry is a directory in the file system or does not exist.
823
return 0;
824     }
825
826     long lastModified(final String JavaDoc entryName) {
827         ArchiveEntry entry = get(entryName);
828         if (entry == null)
829             entry = get(entryName + ENTRY_SEPARATOR);
830         if (entry != null) {
831             // Depending on the driver type, entry.getTime() could return
832
// a negative value. E.g. this is the default value that the
833
// ArchiveDriver uses for newly created entries in order to indicate
834
// an unknown time.
835
// As this is not specified in the contract of the File class,
836
// return 0 in this case instead.
837
final long time = entry.getTime();
838             return time >= 0 ? time : 0;
839         }
840         // This entry does not exist.
841
return 0;
842     }
843
844     boolean setLastModified(final String JavaDoc entryName, final long time)
845     throws IOException JavaDoc {
846         if (time < 0)
847             throw new IllegalArgumentException JavaDoc(entryName +
848                     ": Negative entry modification time!");
849
850         if (isReadOnly())
851             return false;
852
853         ArchiveEntry entry = get(entryName);
854         if (entry == null) {
855             entry = get(entryName + ENTRY_SEPARATOR);
856             if (entry == null) {
857                 // This entry does not exist.
858
return false;
859             }
860         }
861
862         // Order is important here!
863
touch();
864         entry.setTime(time);
865
866         return true;
867     }
868     
869     String JavaDoc[] list(final String JavaDoc entryName) {
870         // Lookup the entry as a directory.
871
final ArchiveEntry entry = get(entryName + ENTRY_SEPARATOR);
872         if (entry != null)
873             return entry.getMetaData().list();
874         else
875             return null; // does not exist as a directory
876
}
877     
878     String JavaDoc[] list(
879             final String JavaDoc entryName,
880             final FilenameFilter JavaDoc filenameFilter,
881             final File dir) {
882         // Lookup the entry as a directory.
883
final ArchiveEntry entry = get(entryName + ENTRY_SEPARATOR);
884         if (entry != null)
885             if (filenameFilter != null)
886                 return entry.getMetaData().list(filenameFilter, dir);
887             else
888                 return entry.getMetaData().list(); // most efficient
889
else
890             return null; // does not exist as directory
891
}
892
893     File[] listFiles(
894             final String JavaDoc entryName,
895             final FilenameFilter JavaDoc filenameFilter,
896             final File dir,
897             final FileFactory factory) { // deprecated warning is OK!
898
// Lookup the entry as a directory.
899
final ArchiveEntry entry = get(entryName + ENTRY_SEPARATOR);
900         if (entry != null)
901             return entry.getMetaData().listFiles(filenameFilter, dir, factory);
902         else
903             return null; // does not exist as a directory
904
}
905     
906     File[] listFiles(
907             final String JavaDoc entryName,
908             final FileFilter JavaDoc fileFilter,
909             final File dir,
910             final FileFactory factory) { // deprecated warning is OK!
911
// Lookup the entry as a directory.
912
final ArchiveEntry entry = get(entryName + ENTRY_SEPARATOR);
913         if (entry != null)
914             return entry.getMetaData().listFiles(fileFilter, dir, factory);
915         else
916             return null; // does not exist as a directory
917
}
918
919     void mkdir(final String JavaDoc entryName, final boolean createParents)
920     throws IOException JavaDoc {
921         beginCreateAndLink(entryName + ENTRY_SEPARATOR, createParents).commit();
922     }
923     
924     void delete(final String JavaDoc entryName)
925     throws IOException JavaDoc {
926         if (get(entryName) != null) {
927             unlink(entryName);
928             return;
929         }
930         final String JavaDoc dirEntryName = entryName + ENTRY_SEPARATOR;
931         if (get(dirEntryName) != null) {
932             unlink(dirEntryName);
933             return;
934         }
935         throw new IOException JavaDoc(entryName + " (archive entry does not exist)");
936     }
937     
938     //
939
// Miscellaneous stuff:
940
//
941

942     /**
943      * A map which combines the fast sorted enumerations of the values in a
944      * sorted map with the fast key/value adding/removal/lookup in a hash map
945      * at the cost of memory and a slight overhead for a daemon thread.
946      * Beware! This is a hack: Only the map operations which are actually
947      * used by this class are implemented.
948      */

949     private static class CompoundMap extends HashMap JavaDoc {
950         private static final LinkedList JavaDoc actions = new LinkedList JavaDoc();
951         private static final Updater mapper = new Updater();
952         static {
953             mapper.start();
954         }
955         private static boolean done;
956
957         private final TreeMap JavaDoc tree;
958         
959         private CompoundMap(Comparator JavaDoc comparator, int initialCapacity) {
960             super(initialCapacity);
961             tree = new TreeMap JavaDoc(comparator);
962             // Prevent starving of mapper thread.
963
int priority = Thread.currentThread().getPriority();
964             if (mapper.getPriority() < priority)
965                 mapper.setPriority(priority);
966         }
967
968         public Object JavaDoc remove(Object JavaDoc key) {
969             synchronized (actions) {
970                 actions.addLast(new Action(key));
971                 done = false;
972                 actions.notifyAll();
973             }
974             return super.remove(key);
975         }
976
977         public Object JavaDoc put(Object JavaDoc key, Object JavaDoc value) {
978             synchronized (actions) {
979                 actions.addLast(new Action(key, value));
980                 done = false;
981                 actions.notifyAll();
982             }
983             return super.put(key, value);
984         }
985
986         public java.util.Collection JavaDoc values() {
987             synchronized (actions) {
988                 while (!done) {
989                     try {
990                         actions.wait();
991                     } catch (InterruptedException JavaDoc ignored) {
992                     }
993                 }
994                 return tree.values();
995             }
996         }
997
998         private class Action implements Runnable JavaDoc {
999             private final boolean put;
1000            private final Object JavaDoc key;
1001            private Object JavaDoc value;
1002
1003            private Action(Object JavaDoc key, Object JavaDoc value) {
1004                put = true;
1005                this.key = key;
1006                this.value = value;
1007            }
1008
1009            private Action(Object JavaDoc key) {
1010                put = false;
1011                this.key = key;
1012            }
1013
1014            public void run() {
1015                if (put)
1016                    tree.put(key, value);
1017                else
1018                    tree.remove(key);
1019            }
1020        }
1021
1022        private static class Updater extends Thread JavaDoc {
1023            private Updater() {
1024                super("TrueZIP CompoundMap Updater");
1025                setDaemon(true);
1026            }
1027            
1028            public void run() {
1029                while (true) {
1030                    final Action action;
1031                    synchronized (actions) {
1032                        done = actions.isEmpty();
1033                        if (done) {
1034                            // I'm done and going to wait now, so don't wait
1035
// for me meanwhile!
1036
actions.notifyAll();
1037                            try {
1038                                actions.wait();
1039                            } catch (InterruptedException JavaDoc ignored) {
1040                            }
1041                            continue; // play it again, Sam...
1042
}
1043                        action = (Action) actions.removeFirst();
1044                    }
1045                    action.run();
1046                }
1047            }
1048        }
1049    } // class CompoundMap
1050
}
1051
Popular Tags