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.io.*;
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.io.OutputStream;
030    
031    
032    /**
033     * <code>ArchiveEntryFile</code> represents a file entry inside an archive. An ArchiveEntryFile is always associated with an
034     * {@link ArchiveEntry} object which contains information about the entry (name, size, date, ...) and with an
035     * {@link AbstractArchiveFile} which acts as an entry repository and provides operations such as listing a directory
036     * entry's files, adding or removing entries (if the archive is writable), etc...
037     *
038     * <p>
039     * <code>ArchiveEntryFile</code> implements {@link com.mucommander.file.AbstractFile} by delegating methods to the
040     * <code>ArchiveEntry</code> and <code>AbstractArchiveFile</code> instances.
041     * <code>ArchiveEntryFile</code> is agnostic to the actual archive format. In other words, there is no need to extend
042     * this class for a particular archive format, <code>ArchiveEntry</code> and <code>AbstractArchiveFile</code> provide a
043     * general framework that isolates from the archive format's specifics.
044     * </p>
045     *
046     * @author Maxence Bernard
047     */
048    public class ArchiveEntryFile extends AbstractFile {
049    
050        /** The archive file that contains this entry */
051        protected AbstractArchiveFile archiveFile;
052    
053        /** This entry file's parent, can be the archive file itself if this entry is located at the top level */
054        protected AbstractFile parent;
055    
056        /** The ArchiveEntry object that contains information about this entry */
057        protected ArchiveEntry entry;
058    
059        /** True if this entry exists in the archive */
060        protected boolean exists;
061    
062    
063        /**
064         * Creates a new ArchiveEntryFile.
065         *
066         * @param url the FileURL instance that represents this file's location
067         * @param archiveFile the AbstractArchiveFile instance that contains this entry
068         * @param entry the ArchiveEntry object that contains information about this entry
069         * @param exists true if this entry exists in the archive
070         */
071        protected ArchiveEntryFile(FileURL url, AbstractArchiveFile archiveFile, ArchiveEntry entry, boolean exists) {
072            super(url);
073            this.archiveFile = archiveFile;
074            this.entry = entry;
075            this.exists = exists;
076        }
077            
078            
079        /**
080         * Returns the ArchiveEntry instance that contains information about the archive entry (path, size, date, ...).
081         *
082         * @return the ArchiveEntry instance that contains information about the archive entry (path, size, date, ...)
083         */
084        public ArchiveEntry getEntry() {
085            return entry;
086        }
087    
088        /**
089         * Returns the {@link AbstractArchiveFile} that contains the entry represented by this file.
090         *
091         * @return the AbstractArchiveFile that contains the entry represented by this file
092         */
093        public AbstractArchiveFile getArchiveFile() {
094            return archiveFile;
095        }
096    
097    
098        /**
099         * Returns the relative path of this entry, with respect to the archive file. The path separator of the returned
100         * path is the one returned by {@link #getSeparator()}. As a relative path, the returned path does not start
101         * with a separator character.
102         *
103         * @return the relative path of this entry, with respect to the archive file.
104         */
105        public String getRelativeEntryPath() {
106            String path = entry.getPath();
107    
108            // Replace all occurrences of the entry's separator by the archive file's separator, only if the separator is
109            // not "/" (i.e. the entry path separator).
110            String separator = getSeparator();
111            if(!separator.equals("/"))
112                path = StringUtils.replaceCompat(path, "/", separator);
113    
114            return path;
115        }
116    
117        /**
118         * Updates this entry's attributes in the archive and returns <code>true</code> if the update went OK.
119         *
120         * @return <code>true</code> if the attributes were successfully updated in the archive.  
121         */
122        private boolean updateEntryAttributes() {
123            try {
124                ((AbstractRWArchiveFile)archiveFile).updateEntry(entry);
125                return true;
126            }
127            catch(IOException e) {
128                return false;
129            }
130        }
131    
132    
133        /////////////////////////////////
134        // AbstractFile implementation //
135        /////////////////////////////////
136    
137        public long getDate() {
138            return entry.getDate();
139        }
140    
141        /**
142         * Returns <code>true</code> only if the archive file that contains this entry is writable.
143         */
144        public boolean canChangeDate() {
145            return archiveFile.isWritableArchive();
146        }
147    
148        /**
149         * Always returns <code>false</code> only if the archive file that contains this entry is not writable.
150         */
151        public boolean changeDate(long lastModified) {
152            if(!(exists && archiveFile.isWritableArchive()))
153                return false;
154    
155            long oldDate = entry.getDate();
156            entry.setDate(lastModified);
157    
158            boolean success = updateEntryAttributes();
159            if(!success)        // restore old date if attributes could not be updated
160                entry.setDate(oldDate);
161    
162            return success;
163        }
164    
165        public long getSize() {
166            return entry.getSize();
167        }
168            
169        public boolean isDirectory() {
170            return entry.isDirectory();
171        }
172    
173        public AbstractFile[] ls() throws IOException {
174            return archiveFile.ls(this, null, null);
175        }
176    
177        public AbstractFile[] ls(FilenameFilter filter) throws IOException {
178            return archiveFile.ls(this, filter, null);
179        }
180            
181        public AbstractFile[] ls(FileFilter filter) throws IOException {
182            return archiveFile.ls(this, null, filter);
183        }
184    
185        public AbstractFile getParent() {
186            return parent;
187        }
188            
189        public void setParent(AbstractFile parent) {
190            this.parent = parent;   
191        }
192    
193        /**
194         * Returns <code>true</code> if this entry exists within the archive file.
195         *
196         * @return true if this entry exists within the archive file
197         */
198        public boolean exists() {
199            return exists;
200        }
201            
202        public FilePermissions getPermissions() {
203            // Return the entry's permissions
204            return entry.getPermissions();
205        }
206    
207        /**
208         * Returns {@link PermissionBits#FULL_PERMISSION_BITS} or {@link PermissionBits#EMPTY_PERMISSION_BITS}, depending
209         * on whether the archive that contains this entry is writable or not.
210         */
211        public PermissionBits getChangeablePermissions() {
212            // Todo: some writable archive implementations may not have full 'set' permissions support, or even no notion of permissions
213            return archiveFile.isWritableArchive()?PermissionBits.FULL_PERMISSION_BITS:PermissionBits.EMPTY_PERMISSION_BITS;
214        }
215    
216        /**
217         * Always returns <code>false</code> only if the archive file that contains this entry is not writable.
218         */
219        public boolean changePermission(int access, int permission, boolean enabled) {
220            return changePermissions(ByteUtils.setBit(getPermissions().getIntValue(), (permission << (access*3)), enabled));
221        }
222    
223        public String getOwner() {
224            return entry.getOwner();
225        }
226    
227        public boolean canGetOwner() {
228            return entry.getOwner()!=null;
229        }
230    
231        public String getGroup() {
232            return entry.getGroup();
233        }
234    
235        public boolean canGetGroup() {
236            return entry.getGroup()!=null;
237        }
238    
239        /**
240         * Always returns <code>false</code>.
241         */
242        public boolean isSymlink() {
243            return false;
244        }
245    
246        /**
247         * Deletes this entry from the associated <code>AbstractArchiveFile</code> if it is writable (as reported by
248         * {@link com.mucommander.file.AbstractArchiveFile#isWritableArchive()}).
249         * Throws an <code>IOException</code> in any of the following cases:
250         * <ul>
251         *  <li>if the associated <code>AbstractArchiveFile</code> is not writable</li>
252         *  <li>if this entry does not exist in the archive</li>
253         *  <li>if this entry is a non-empty directory</li>
254         *  <li>if an I/O error occurred</li>
255         * </ul>
256         *
257         * @throws IOException in any of the cases listed above.
258         */
259        public void delete() throws IOException {
260            if(exists && archiveFile.isWritableArchive()) {
261                AbstractRWArchiveFile rwArchiveFile = (AbstractRWArchiveFile)archiveFile;
262    
263                // Throw an IOException if this entry is a non-empty directory
264                if(isDirectory()) {
265                    ArchiveEntryTree tree = rwArchiveFile.getArchiveEntryTree();
266                    if(tree!=null) {
267                        DefaultMutableTreeNode node = tree.findEntryNode(entry.getPath());
268                        if(node!=null && node.getChildCount()>0)
269                            throw new IOException();
270                    }
271                }
272    
273                // Delete the entry in the archive file
274                rwArchiveFile.deleteEntry(entry);
275    
276                // Non-existing entries are considered as zero-length regular files
277                entry.setDirectory(false);
278                entry.setSize(0);
279                exists = false;
280            }
281            else
282                throw new IOException();
283        }
284    
285        /**
286         * Creates this entry as a directory in the associated <code>AbstractArchiveFile</code> if the archive is
287         * writable (as reported by {@link com.mucommander.file.AbstractArchiveFile#isWritableArchive()}).
288         * Throws an <code>IOException</code> if it isn't, if this entry already exists in the archive or if an I/O error
289         * occurred.
290         *
291         * @throws IOException if the associated archive file is not writable, if this entry already exists in the archive,
292         * or if an I/O error occurred
293         */
294        public void mkdir() throws IOException {
295            if(!exists && archiveFile.isWritableArchive()) {
296                AbstractRWArchiveFile rwArchivefile = (AbstractRWArchiveFile)archiveFile;
297                // Update the ArchiveEntry
298                entry.setDirectory(true);
299                entry.setDate(System.currentTimeMillis());
300                entry.setSize(0);
301    
302                // Add the entry to the archive file
303                rwArchivefile.addEntry(entry);
304    
305                // The entry now exists
306                exists = true;
307            }
308            else
309                throw new IOException();
310        }
311    
312        /**
313         * Delegates to the archive file's {@link AbstractArchiveFile#getFreeSpace()} method.
314         */
315        public long getFreeSpace() {
316            return archiveFile.getFreeSpace();
317        }
318    
319        /**
320         * Delegates to the archive file's {@link AbstractArchiveFile#getTotalSpace()} method.
321         */
322        public long getTotalSpace() {
323            return archiveFile.getTotalSpace();
324        }
325    
326        /**
327         * Delegates to the archive file's {@link AbstractArchiveFile#getEntryInputStream(ArchiveEntry)}} method.
328         */
329        public InputStream getInputStream() throws IOException {
330            return archiveFile.getEntryInputStream(entry);
331        }
332    
333        /**
334         * Returns an <code>OutputStream</code> that allows to write this entry's contents if the archive is
335         * writable (as reported by {@link com.mucommander.file.AbstractArchiveFile#isWritableArchive()}).
336         * Throws an <code>IOException</code> if it isn't or if an I/O error occurred.
337         *
338         * <p>
339         * This method will create this entry as a regular file in the archive if it doesn't already exist, or replace
340         * it if it already does.
341         * </p>
342         *
343         * @throws IOException if the associated archive file is not writable, if this entry already exists in the archive,
344         * or if an I/O error occurred
345         */
346        public OutputStream getOutputStream(boolean append) throws IOException {
347            if(archiveFile.isWritableArchive()) {
348                if(append)
349                    throw new IOException("Can't append to an existing archive entry");
350    
351                if(exists) {
352                    try {
353                        delete();
354                    }
355                    catch(IOException e) {
356                        // Go ahead and try to add the file anyway 
357                    }
358                }
359    
360                // Update the ArchiveEntry's size as data gets written to the OutputStream
361                OutputStream out = new CounterOutputStream(((AbstractRWArchiveFile)archiveFile).addEntry(entry),
362                        new ByteCounter() {
363                            public synchronized void add(long nbBytes) {
364                                entry.setSize(entry.getSize()+nbBytes);
365                                entry.setDate(System.currentTimeMillis());
366                            }
367                        });
368                exists = true;
369    
370                return out;
371            }
372            else
373                throw new IOException();
374        }
375    
376        /**
377         * Always returns <code>false</code>: random read access is not available for archive entries.
378         */
379        public boolean hasRandomAccessInputStream() {
380            return false;
381        }
382    
383        /**
384         * Always throws an <code>IOException</code>: random read access is not available for archive entries.
385         */
386        public RandomAccessInputStream getRandomAccessInputStream() throws IOException {
387            throw new IOException();
388        }
389    
390        /**
391         * Always returns <code>false</code>: random write access is not available for archive entries.
392         */
393        public boolean hasRandomAccessOutputStream() {
394            return false;
395        }
396    
397        /**
398         * Always throws an <code>IOException</code>: random write access is not available for archive entries.
399         */
400        public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException {
401            throw new IOException();
402        }
403    
404        /**
405         * Returns the same ArchiveEntry instance as {@link #getEntry()}.
406         */
407        public Object getUnderlyingFileObject() {
408            return entry;
409        }
410    
411        /**
412         * Always returns <code>false</code>: archive entries cannot run processes.
413         */
414        public boolean canRunProcess() {
415            return false;
416        }
417    
418        /**
419         * Always throws an <code>IOException</code>: archive entries cannot run processes.
420         */
421        public com.mucommander.process.AbstractProcess runProcess(String[] tokens) throws IOException {
422            throw new IOException();
423        }
424    
425        
426        ////////////////////////
427        // Overridden methods //
428        ////////////////////////
429    
430        /**
431         * This method is overridden to return the separator of the {@link #getArchiveFile() archive file} that contains
432         * this entry.
433         *
434         * @return the separator of the archive file that contains this entry
435         */
436        public String getSeparator() {
437            return archiveFile.getSeparator();
438        }
439    
440        /**
441         * This method is overridden to use the archive file's canonical path as the base path of this entry file.
442         */
443        public String getCanonicalPath() {
444            // Use the archive file's canonical path and append it with the entry's relative path
445            return archiveFile.getCanonicalPath(true)+getRelativeEntryPath();
446        }
447    
448        /**
449         * Always returns <code>false</code> only if the archive file that contains this entry is not writable.
450         */
451        public boolean changePermissions(int permissions) {
452            if(!(exists && archiveFile.isWritableArchive()))
453                return false;
454    
455            FilePermissions oldPermissions = entry.getPermissions();
456            FilePermissions newPermissions = new SimpleFilePermissions(permissions, oldPermissions.getMask());
457            entry.setPermissions(newPermissions);
458    
459            boolean success = updateEntryAttributes();
460            if(!success)        // restore old permissions if attributes could not be updated
461                entry.setPermissions(oldPermissions);
462    
463            return success;
464        }
465    
466        public int getMoveToHint(AbstractFile destFile) {
467            if(archiveFile.isWritableArchive())
468                return SHOULD_NOT_HINT;
469    
470            return MUST_NOT_HINT;
471        }
472    }