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 }