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    }