001 /*
002 * This file is part of muCommander, http://www.mucommander.com
003 * Copyright (C) 2002-2008 Maxence Bernard
004 *
005 * muCommander is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * muCommander is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
017 */
018
019 package com.mucommander.file;
020
021 import com.mucommander.file.filter.FileFilter;
022 import com.mucommander.file.filter.FilenameFilter;
023 import com.mucommander.file.impl.ProxyFile;
024 import com.mucommander.util.StringUtils;
025
026 import javax.swing.tree.DefaultMutableTreeNode;
027 import java.io.IOException;
028 import java.io.InputStream;
029 import java.util.Vector;
030
031 /**
032 * <code>AbstractArchiveFile</code> is the superclass of all archive files. It allows archive file to be browsed as if
033 * they were regular directories, independently of the underlying protocol used to access the actual file.
034 *
035 * <p><code>AbstractArchiveFile</code> extends {@link ProxyFile} to delegate the <code>AbstractFile</code>
036 * implementation to the actual archive file and overrides some methods to provide the added functionality.<br>
037 * There are two kinds of <code>AbstractArchiveFile</code>, both of which extend this class:
038 * <ul>
039 * <li>{@link AbstractROArchiveFile}: read-only archives, these are only able to perform read operations such as
040 * listing the archive's contents or retrieving a particular entry's contents.
041 * <li>{@link AbstractRWArchiveFile}: read-write archives, these are also able to modify the archive by adding or
042 * deleting an entry from the archive. These operations usually require random access to the underlying file,
043 * so write operations may not be available on all underlying file types. The {@link #isWritableArchive()} method allows
044 * to determine whether the archive file is able to carry out write operations or not.
045 * </ul>
046 * When implementing a new archive file/format, either <code>AbstractROArchiveFile</code> or <code>AbstractRWArchiveFile</code>
047 * should be subclassed, but not this class.
048 * </p>
049 *
050 * <p>The first time one of the <code>ls()</code> methods is called to list the archive's contents, the
051 * {@link #getEntries()} method is called to retrieve a list of *all* the entries contained by the archive, not only the
052 * ones at the top level but also the ones nested one of several levels below. Using this list of entries, it creates
053 * a tree to map the structure of the archive and list the content of any particular directory within the archive.
054 * This tree is recreated (<code>getEntries()</code> is called again) only if the archive file has changed, i.e. its
055 * date has changed since the tree was created.</p>
056 *
057 * <p>Files returned by the <code>ls()</code> are {@link ArchiveEntryFile} instances which use an {@link ArchiveEntry}
058 * object to retrieve the entry's attributes. In turn, these <code>ArchiveEntryFile</code> instances query the
059 * associated <code>AbstractArchiveFile</code> to list their content.
060 * <br>From an implementation perspective, one only needs to deal with {@link ArchiveEntry} instances, all the nuts
061 * and bolts are taken care of by this class.</p>
062
063 * @see com.mucommander.file.FileFactory
064 * @see com.mucommander.file.ArchiveFormatProvider
065 * @see com.mucommander.file.ArchiveEntry
066 * @see com.mucommander.file.ArchiveEntryFile
067 * @see com.mucommander.file.archiver.Archiver
068 * @author Maxence Bernard
069 */
070 public abstract class AbstractArchiveFile extends ProxyFile {
071
072 /** Archive entries tree */
073 protected ArchiveEntryTree entryTreeRoot;
074
075 /** Date this file had when the entries tree was created. Used to detect if the archive file has changed and entries
076 * need to be reloaded */
077 protected long entryTreeDate;
078
079
080 /**
081 * Creates an AbstractArchiveFile on top of the given file.
082 *
083 * @param file the file on top of which to create the archive
084 */
085 protected AbstractArchiveFile(AbstractFile file) {
086 super(file);
087 }
088
089 /**
090 * Creates the entries tree, used by {@link #ls(ArchiveEntryFile, com.mucommander.file.filter.FilenameFilter, com.mucommander.file.filter.FileFilter)}
091 * to quickly list the contents of an archive's subfolder.
092 *
093 * @throws IOException if an error occured while retrieving this archive's entries
094 */
095 protected void createEntriesTree() throws IOException {
096 ArchiveEntryTree treeRoot = new ArchiveEntryTree();
097
098 long start = System.currentTimeMillis();
099
100 Vector entries = getEntries();
101
102 if(com.mucommander.Debug.ON) com.mucommander.Debug.trace("entries loaded in "+(System.currentTimeMillis()-start)+" ms, nbEntries="+entries.size());
103 start = System.currentTimeMillis();
104
105 int nbEntries = entries.size();
106 for(int i=0; i<nbEntries; i++) {
107 treeRoot.addArchiveEntry((ArchiveEntry)entries.elementAt(i));
108 }
109
110 if(com.mucommander.Debug.ON) com.mucommander.Debug.trace("entries tree created in "+(System.currentTimeMillis()-start)+" ms");
111
112 this.entryTreeRoot = treeRoot;
113 declareEntriesTreeUpToDate();
114 }
115
116 /**
117 * Checks if the entries tree exists and if this file hasn't been modified since the tree was last created.
118 * If any of those 2 conditions isn't met, the entries tree is (re)created.
119 *
120 * @throws IOException if an error occurred while creating the tree
121 */
122 protected void checkEntriesTree() throws IOException {
123 if(this.entryTreeRoot ==null || getDate()!=this.entryTreeDate)
124 createEntriesTree();
125 }
126
127 /**
128 * Declares the entries tree up-to-date by setting the current tree date to the archive file's.
129 * This method should be called by {@link AbstractRWArchiveFile} implementations when the archive file has been
130 * modified and the entries propagated in the tree, to avoid the tree from being automatically re-created when
131 * {@link #checkEntriesTree()} is called.
132 */
133 protected void declareEntriesTreeUpToDate() {
134 this.entryTreeDate = getDate();
135 }
136
137 /**
138 * Adds the given {@link ArchiveEntry} to the entries tree. This method will create the tree if it doesn't already
139 * exist, or re-create it if the archive file has changed since it was last created.
140 *
141 * @param entry the ArchiveEntry to add to the tree
142 * @throws IOException if an error occurred while creating the entries tree
143 */
144 protected void addToEntriesTree(ArchiveEntry entry) throws IOException {
145 checkEntriesTree();
146 entryTreeRoot.addArchiveEntry(entry);
147 }
148
149 /**
150 * Removes the given {@link ArchiveEntry} from the entries tree. This method will create the tree if it doesn't
151 * already exist, or re-create it if the archive file has changed since it was last created.
152 *
153 * @param entry the ArchiveEntry to remove from the tree
154 * @throws IOException if an error occurred while creating the entries tree
155 */
156 protected void removeFromEntriesTree(ArchiveEntry entry) throws IOException {
157 checkEntriesTree();
158 DefaultMutableTreeNode entryNode = entryTreeRoot.findEntryNode(entry.getPath());
159
160 if(entryNode!=null) {
161 DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode)entryNode.getParent();
162 parentNode.remove(entryNode);
163 }
164 }
165
166 /**
167 * Returns the {@link ArchiveEntryTree} instance corresponding to the root of the archive entry tree.
168 * The returned value can be <code>null</code> if the tree hasn't been intialized yet.
169 *
170 * @return the ArchiveEntryTree instance corresponding to the root of the archive entry tree
171 */
172 ArchiveEntryTree getArchiveEntryTree() {
173 return entryTreeRoot;
174 }
175
176 /**
177 * Returns the contents of the specified folder entry.
178 */
179 protected AbstractFile[] ls(ArchiveEntryFile entryFile, FilenameFilter filenameFilter, FileFilter fileFilter) throws IOException {
180 // Make sure the entries tree is created and up-to-date
181 checkEntriesTree();
182
183 if(!entryFile.isBrowsable())
184 throw new IOException();
185
186 DefaultMutableTreeNode matchNode = entryTreeRoot.findEntryNode(entryFile.getEntry().getPath());
187 if(matchNode==null)
188 throw new IOException();
189
190 return ls(matchNode, entryFile, filenameFilter, fileFilter);
191 }
192
193 /**
194 * Returns the contents (direct children) of the specified tree node.
195 */
196 protected AbstractFile[] ls(DefaultMutableTreeNode treeNode, AbstractFile parentFile, FilenameFilter filenameFilter, FileFilter fileFilter) throws IOException {
197 AbstractFile files[];
198 int nbChildren = treeNode.getChildCount();
199
200 // No FilenameFilter, create entry files and store them directly into an array
201 if(filenameFilter==null) {
202 files = new AbstractFile[nbChildren];
203
204 for(int c=0; c<nbChildren; c++) {
205 files[c] = getArchiveEntryFile((ArchiveEntry)(((DefaultMutableTreeNode)treeNode.getChildAt(c)).getUserObject()), parentFile, true);
206 }
207 }
208 // Use provided FilenameFilter and temporarily store created entry files that match the filter in a Vector
209 else {
210 Vector filesV = new Vector();
211 for(int c=0; c<nbChildren; c++) {
212 ArchiveEntry entry = (ArchiveEntry)(((DefaultMutableTreeNode)treeNode.getChildAt(c)).getUserObject());
213 if(!filenameFilter.accept(entry.getName()))
214 continue;
215
216 filesV.add(getArchiveEntryFile(entry, parentFile, true));
217 }
218
219 files = new AbstractFile[filesV.size()];
220 filesV.toArray(files);
221 }
222
223 return fileFilter==null?files:fileFilter.filter(files);
224 }
225
226 /**
227 * Creates and returns an AbstractFile using the provided entry and parent file. This method takes care of
228 * creating the proper AbstractArchiveFile instance if the entry is itself an archive.
229 * The entry file's path will use the separator of the underlying file, as returned by {@link #getSeparator()}.
230 * That means entries paths of archives located on Windows local filesystems will use '\' as a separator, and
231 * '/' for Unix local archives.
232 */
233 protected AbstractFile getArchiveEntryFile(ArchiveEntry entry, AbstractFile parentFile, boolean exists) throws IOException {
234
235 String entryPath = entry.getPath();
236
237 // If the parent file's separator is not '/' (the default entry separator), replace '/' occurrences by
238 // the parent file's separator. For local files Under Windows, this allows entries' path to have '\' separators.
239 String fileSeparator = getSeparator();
240 if(!fileSeparator.equals("/"))
241 entryPath = StringUtils.replaceCompat(entryPath, "/", fileSeparator);
242
243 FileURL archiveURL = getURL();
244 FileURL entryURL = (FileURL)archiveURL.clone();
245 entryURL.setPath(addTrailingSeparator(archiveURL.getPath()) + entryPath);
246
247 AbstractFile entryFile = FileFactory.wrapArchive(
248 new ArchiveEntryFile(
249 entryURL,
250 this,
251 entry,
252 exists
253 )
254 );
255 entryFile.setParent(parentFile);
256
257 return entryFile;
258 }
259
260 /**
261 * Creates and returns an AbstractFile that corresponds to the given entry path within the archive.
262 * The requested entry may or may not exist in the archive, the {@link #exists()} method of the returned entry file
263 * can be used used to get this information. However, if the requested entry does not exist in the archive and is
264 * not located at the top level (i.e. is located in a subfolder), its parent folder must exist in the archive or
265 * else an <code>IOException</code> will be thrown.
266 *
267 * <p>Important note: the given path's separator character must be '/' and the path must be relative to the
268 * archive's root, i.e. not start with a leading '/', otherwise the entry will not be found.</p>
269 *
270 * @param entryPath path to an entry within this archive
271 * @return an AbstractFile that corresponds to the given entry path
272 * @throws IOException if neither the entry nor its parent exist within the archive
273 */
274 public AbstractFile getArchiveEntryFile(String entryPath) throws IOException {
275 // Make sure the entries tree is created and up-to-date
276 checkEntriesTree();
277
278 entryPath = entryPath.replace('\\', '/');
279
280 // Find the entry node corresponding to the given path
281 DefaultMutableTreeNode entryNode = entryTreeRoot.findEntryNode(entryPath);
282
283 if(entryNode==null) {
284 int depth = ArchiveEntry.getDepth(entryPath);
285
286 AbstractFile parentFile;
287 if(depth==0)
288 parentFile = this;
289 else {
290 String parentPath = entryPath;
291 if(parentPath.endsWith("/"))
292 parentPath = parentPath.substring(0, parentPath.length()-1);
293
294 parentPath = parentPath.substring(0, parentPath.lastIndexOf('/'));
295
296 parentFile = getArchiveEntryFile(parentPath);
297 if(parentFile==null) // neither the entry nor the parent exist
298 throw new IOException();
299 }
300
301 return getArchiveEntryFile(new ArchiveEntry(entryPath, false), parentFile, false);
302 }
303
304 DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode)entryNode.getParent();
305 // Todo: suboptimal recursion, findEntryNode() is called each time
306 return getArchiveEntryFile((ArchiveEntry)entryNode.getUserObject(), parentNode== entryTreeRoot?this: getArchiveEntryFile(((ArchiveEntry)parentNode.getUserObject()).getPath()), true);
307 }
308
309
310 //////////////////////
311 // Abstract methods //
312 //////////////////////
313
314 /**
315 * Returns a Vector of {@link ArchiveEntry}, representing all the entries this archive file contains.
316 * This method will be called the first time one of the <code>ls()</code> is called. If will not be further called,
317 * unless the file's date has changed since the last time one of the <code>ls()</code> methods was called.
318 */
319 public abstract Vector getEntries() throws IOException;
320
321 /**
322 * Returns an InputStream to read from the given archive entry. The specified {@link ArchiveEntry} instance is
323 * necessarily one of the entries that were returned by {@link #getEntries()}.
324 */
325 public abstract InputStream getEntryInputStream(ArchiveEntry entry) throws IOException;
326
327 /**
328 * Returns <code>true</code> if this archive file is writable, i.e. is capable of adding and deleting entries to
329 * the underlying archive file.
330 *
331 * <p>
332 * This method is implemented by {@link com.mucommander.file.AbstractROArchiveFile} and
333 * {@link com.mucommander.file.AbstractRWArchiveFile} to respectively return <code>false</code> and
334 * <code>true</code>. This method may be overridden by <code>AbstractRWArchiveFile</code> implementations if write
335 * access is only available under certain conditions, for example if it requires random write access to the
336 * proxied archive file (which may not always be available).
337 * Therefore, this method should be used to test if an <code>AbstractArchiveFile</code> is writable, rather than
338 * testing if it is an instance of <code>AbstractRWArchiveFile</code>.
339 * </p>
340 *
341 * @return true if this archive is writable, i.e. is capable of adding and deleting entries to
342 * the underlying archive file.
343 */
344 public abstract boolean isWritableArchive();
345
346
347 ////////////////////////
348 // Overridden methods //
349 ////////////////////////
350
351 /**
352 * This method is overridden to list and return the topmost entries contained by this archive.
353 * The returned files are {@link ArchiveEntryFile} instances.
354 *
355 * @return the topmost entries contained by this archive
356 * @throws IOException if the archive entries could not be listed
357 */
358 public AbstractFile[] ls() throws IOException {
359 // Make sure the entries tree is created and up-to-date
360 checkEntriesTree();
361
362 return ls(entryTreeRoot, this, null, null);
363 }
364
365 /**
366 * This method is overridden to list and return the topmost entries contained by this archive, filtering out
367 * the ones that do not match the specified {@link FilenameFilter}. The returned files are {@link ArchiveEntryFile}
368 * instances.
369 *
370 * @param filter the FilenameFilter to be used to filter files out from the list, may be <code>null</code>
371 * @return the topmost entries contained by this archive
372 * @throws IOException if the archive entries could not be listed
373 */
374 public AbstractFile[] ls(FilenameFilter filter) throws IOException {
375 // Make sure the entries tree is created and up-to-date
376 checkEntriesTree();
377
378 return ls(entryTreeRoot, this, filter, null);
379 }
380
381 /**
382 * This method is overridden to list and return the topmost entries contained by this archive, filtering out
383 * the ones that do not match the specified {@link FileFilter}. The returned files are {@link ArchiveEntryFile} instances.
384 *
385 * @param filter the FilenameFilter to be used to filter files out from the list, may be <code>null</code>
386 * @return the topmost entries contained by this archive
387 * @throws IOException if the archive entries could not be listed
388 */
389 public AbstractFile[] ls(FileFilter filter) throws IOException {
390 // Make sure the entries tree is created and up-to-date
391 checkEntriesTree();
392
393 return ls(entryTreeRoot, this, null, filter);
394 }
395
396 /**
397 * Always returns <code>true</code>, archive files can be browsed even though they are not directories.
398 */
399 public boolean isBrowsable() {
400 return true;
401 }
402
403 /**
404 * Always returns <code>false</code>, archive files can be browsed but they are not directiories.
405 */
406 public boolean isDirectory() {
407 return false;
408 }
409
410 /**
411 * Returns the proxied file's free space if this archive is writable (as reported by {@link #isWritableArchive()},
412 * else returns <code>0</code>.
413 *
414 * @return the proxied file's free space is this archive is writable, 0 otherwise.
415 */
416 public long getFreeSpace() {
417 if(isWritableArchive())
418 return file.getFreeSpace();
419 else
420 return 0;
421 }
422
423 /**
424 * Always returns <code>false</code>.
425 */
426 public boolean canRunProcess() {
427 return false;
428 }
429
430 /**
431 * Always throws an <code>IOException</code>.
432 */
433 public com.mucommander.process.AbstractProcess runProcess(String[] tokens) throws IOException {
434 throw new IOException();
435 }
436 }