/**
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.file;
import java.awt.Dimension;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.swing.Icon;
import com.mucommander.commons.file.archive.AbstractArchiveEntryFile;
import com.mucommander.commons.file.archive.AbstractArchiveFile;
import com.mucommander.commons.file.archive.AbstractRWArchiveFile;
import com.mucommander.commons.file.compat.CompatURLStreamHandler;
import com.mucommander.commons.file.filter.FileFilter;
import com.mucommander.commons.file.filter.FilenameFilter;
import com.mucommander.commons.io.BufferPool;
import com.mucommander.commons.io.ChecksumInputStream;
import com.mucommander.commons.io.FileTransferError;
import com.mucommander.commons.io.FileTransferException;
import com.mucommander.commons.io.RandomAccessInputStream;
import com.mucommander.commons.io.RandomAccessOutputStream;
import com.mucommander.commons.io.StreamUtils;
/**
* <code>AbstractFile</code> is the superclass of all files.
*
* <p>AbstractFile classes should never be instantiated directly. Instead, the {@link FileFactory} <code>getFile</code>
* methods should be used to get a file instance from a path or {@link FileURL} location.</p>
*
* @see com.mucommander.commons.file.FileFactory
* @see com.mucommander.commons.file.ProxyFile
* @author Maxence Bernard
*/
public abstract class AbstractFile implements FileAttributes {
/** URL representing this file */
protected FileURL fileURL;
/** Default path separator */
public final static String DEFAULT_SEPARATOR = "/";
/** Size of the read/write buffer */
// Note: raising buffer size from 8192 to 65536 makes a huge difference in SFTP read transfer rates but beyond
// 65536, no more gain (not sure why).
public final static int IO_BUFFER_SIZE = 65536;
/** Extension that is specified by the user, not as part of the filename */
private String customExtension;
/**
* Creates a new file instance with the given URL.
*
* @param url the FileURL instance that represents this file's location
*/
protected AbstractFile(FileURL url) {
this.fileURL = url;
}
/////////////////////////
// Overridable methods //
/////////////////////////
/**
* Returns the {@link FileURL} instance that represents this file's location.
*
* @return the FileURL instance that represents this file's location
*/
public FileURL getURL() {
return fileURL;
}
/**
* Creates and returns a <code>java.net.URL</code> referring to the same location as the {@link FileURL} associated
* with this <code>AbstractFile</code>.
* The <code>java.net.URL</code> is created from the string representation of this file's <code>FileURL</code>.
* Thus, any credentials this <code>FileURL</code> contains are preserved, but properties are lost.
*
* <p>The returned <code>URL</code> uses this {@link AbstractFile} to access the associated resource, via the
* underlying <code>URLConnection</code> which delegates to this class.</p>
*
* <p>It is important to note that this method is provided for interoperability purposes, for the sole purpose of
* connecting to APIs that require a <code>java.net.URL</code>.</p>
*
* @return a <code>java.net.URL</code> referring to the same location as this <code>FileURL</code>
* @throws java.net.MalformedURLException if the java.net.URL could not parse the location of this FileURL
*/
public URL getJavaNetURL() throws MalformedURLException {
return new URL(null, getURL().toString(true), new CompatURLStreamHandler(this));
}
/**
* Returns this file's name.
*
* <p>The returned name is the filename extracted from this file's <code>FileURL</code>
* as returned by {@link FileURL#getFilename()}. If the filename is <code>null</code> (e.g. http://google.com), the
* <code>FileURL</code>'s host will be returned instead. If the host is <code>null</code> (e.g. smb://), an empty
* String will be returned. Thus, the returned name will never be <code>null</code>.</p>
*
* <p>This method should be overridden if a special processing (e.g. URL-decoding) needs to be applied to the
* returned filename.</p>
*
* @return this file's name
*/
public String getName() {
String name = fileURL.getFilename();
// If filename is null, use host instead
if(name==null) {
name = fileURL.getHost();
// If host is null, return an empty string
if(name==null)
return "";
}
return name;
}
/**
* Returns this file's extension, <code>null</code> if this file's name doesn't have an extension.
*
* <p>A filename has an extension if and only if:<br/>
* - it contains at least one <code>.</code> character<br/>
* - the last <code>.</code> is not the last character of the filename<br/>
* - the last <code>.</code> is not the first character of the filename</p>
*
* @return this file's extension, <code>null</code> if this file's name doesn't have an extension
*/
public String getExtension() {
return getExtension(getName());
}
/**
* Returns the absolute path to this file:
* <ul>
* <li>For local filesystems, the local file's path should be returned, and <b>not</b> a full URL with the scheme
* and host parts (e.g. /path/to/file, not file://localhost/path/to/file)</li>
* <li>For any other filesystems, the full URL including the protocol and host parts should be returned
* (e.g. smb://192.168.1.1/root/blah)</li>
* </ul>
* <p>
* This default implementation returns the string representation of this file's {@link #getURL() url}, without
* the login and password parts. File implementations overridding this method should always return a path free of
* any login and password, so that it can safely be displayed to the end user or stored, without risking to
* compromise sensitive information.
* </p>
*
* @return the absolute path to this file
*/
public String getAbsolutePath() {
return getURL().toString(false);
}
/**
* Returns the canonical path to this file, resolving any symbolic links or '..' and '.' occurrences.
*
* <p>This implementation simply returns the value of {@link #getAbsolutePath()}, and thus should be overridden
* if canonical path resolution is available.</p>
*
* @return the canonical path to this file
*/
public String getCanonicalPath() {
return getAbsolutePath();
}
/**
* Returns an <code>AbstractFile</code> representing the canonical path of this file, or <code>this</code> if the
* absolute and canonical path of this file are identical.<br/>
* Note that the returned file may or may not exist, for example if this file is a symlink to a file that doesn't
* exist.
*
* @return an <code>AbstractFile representing the canonical path of this file, or this if the absolute and canonical
* path of this file are identical.
*/
public AbstractFile getCanonicalFile() {
String canonicalPath = getCanonicalPath(false);
if(canonicalPath.equals(getAbsolutePath(false)))
return this;
try {
FileURL canonicalURL = FileURL.getFileURL(canonicalPath);
canonicalURL.setCredentials(fileURL.getCredentials());
return FileFactory.getFile(canonicalURL);
}
catch(IOException e) {
return this;
}
}
/**
* Returns the path separator used by this file.
*
* <p>This default implementation returns the default separator "/", this method should be overridden if the path
* separator used by the file implementation is different.</p>
*
* @return the path separator used by this file
*/
public String getSeparator() {
return DEFAULT_SEPARATOR;
}
/**
* Returns <code>true</code> if this file is hidden.
*
* <p>This default implementation is solely based on the filename and returns <code>true</code> if this
* file's name starts with '.'. This method should be overriden if the underlying filesystem has a notion
* of hidden files.</p>
*
* @return true if this file is hidden
*/
public boolean isHidden() {
return getName().startsWith(".");
}
/**
* Return <code>true</code> if the application can read this file.
*
* TODO: need to be overridden with a correct check for each file type.
*
* @return true if the application can read this file.
*/
public boolean canRead() {
return true;
}
/**
* Returns the root folder of this file, i.e. the top-level parent folder that has no parent folder. The returned
* folder necessarily contains this file, directly or indirectly. If this file already is a root folder, the same
* file will be returned.
* <p>
* This default implementation returns the file whose URL has the same scheme as this one, same credentials (if any),
* and a path equal to <code>/</code>.
* </p>
*
* @return the root folder that contains this file
*/
public AbstractFile getRoot() {
FileURL rootURL = (FileURL)getURL().clone();
rootURL.setPath("/");
return FileFactory.getFile(rootURL);
}
/**
* Returns <code>true</code> if this file is a root folder.
* <p>
* This default implementation returns <code>true</code> if this file's URL path is <code>/</code>.
* </p>
*
* @return <code>true</code> if this file is a root folder
*/
public boolean isRoot() {
return getURL().getPath().equals("/");
}
/**
* Returns the volume on which this file is located, or <code>this</code> if this file is itself a volume.
* The returned file may never be <code>null</code>. Furthermore, the returned file may not always
* {@link #exists() exist}, for instance if the returned volume corresponds to a removable drive that's currently
* unavailable. If the returned file does exist, it must always be a {@link #isDirectory() directory}.
* In other words, archive files may not be considered as volumes.
* <p>
* The notion of volume may or may not have a meaning depending on the kind of fileystem. On local filesystems,
* the notion of volume can be assimilated into that of <i>mount point</i> for UNIX-based OSes, or <i>drive</i>
* for the Windows platform. Volumes may also have a meaning for certain network filesystems such as SMB, for which
* shares can be considered as volumes. Filesystems that don't have a notion of volume should return the
* {@link #getRoot() root folder}.
* </p>
* <p>
* This default implementation returns this file's {@link #getRoot() root folder}. This method should be overridden
* if this is not adequate.
* </p>
*
* @return the volume on which this file is located.
*/
public AbstractFile getVolume() {
return getRoot();
}
/**
* Returns an <code>InputStream</code> to read this file's contents, starting at the specified offset (in bytes).
* A <code>java.io.IOException</code> is thrown if the file doesn't exist.
*
* <p>This implementation starts by checking whether the {@link FileOperation#RANDOM_READ_FILE} operation is
* supported or not.
* If it is, a {@link #getRandomAccessInputStream() random input stream} to this file is retrieved and used to seek
* to the specified offset. If it's not, a regular {@link #getInputStream() input stream} is retrieved, and
* {@link java.io.InputStream#skip(long)} is used to position the stream to the specified offset, which on most
* <code>InputStream</code> implementations is very slow as it causes the bytes to be read and discarded.
* For this reason, file implementations that do not provide random read access may want to override this method
* if a more efficient implementation can be provided.</p>
*
* @param offset the offset in bytes from the beginning of the file, must be >0
* @throws IOException if this file cannot be read or is a folder.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
* @return an <code>InputStream</code> to read this file's contents, skipping the specified number of bytes
*/
public InputStream getInputStream(long offset) throws IOException, UnsupportedFileOperationException {
// Use a random access input stream when available
if(isFileOperationSupported(FileOperation.RANDOM_READ_FILE)) {
RandomAccessInputStream rais = getRandomAccessInputStream();
rais.seek(offset);
return rais;
}
InputStream in = getInputStream();
// Skip exactly the specified number of bytes
StreamUtils.skipFully(in, offset);
return in;
}
/**
* Copies the contents of the given <code>InputStream</code> to this file, appending or overwriting the file
* if it exists. It is noteworthy that the provided <code>InputStream</code> will <b>not</b> be closed by this method.
*
* <p>This method should be overridden by filesystems that do not offer a {@link #getOutputStream()}
* implementation, but that can take an <code>InputStream</code> and use it to write the file.
* For this reason, it is recommended to use this method to write a file, rather than copying streams manually using
* {@link #getOutputStream()}</p>
*
* <p>The <code>length</code> parameter is optional. Setting its value help certain protocols which need to know
* the length in advance. This is the case for instance for some HTTP-based protocols like Amazon S3, which require
* the <code>Content-Length</code> header to be set in the request. Callers should thus set the length if it is
* known.</p>
*
* <p>Read and write operations are buffered, with a buffer of {@link #IO_BUFFER_SIZE} bytes. For performance
* reasons, this buffer is provided by {@link BufferPool}. Thus, there is no need to surround the InputStream
* with a {@link java.io.BufferedInputStream}.</p>
*
* <p>Copy progress can optionally be monitored by supplying a {@link com.mucommander.commons.io.CounterInputStream}.</p>
*
* @param in the InputStream to read from
* @param append if true, data written to the OutputStream will be appended to the end of this file. If false, any
* existing data will be overwritten.
* @param length length of the stream before EOF is reached, <code>-1</code> if unknown.
* @throws FileTransferException if something went wrong while reading from the InputStream or writing to this file
*/
public void copyStream(InputStream in, boolean append, long length) throws FileTransferException {
OutputStream out;
try {
out = append?getAppendOutputStream():getOutputStream();
}
catch(IOException e) {
// TODO: re-throw UnsupportedFileOperationException ?
throw new FileTransferException(FileTransferError.OPENING_DESTINATION);
}
try {
StreamUtils.copyStream(in, out, IO_BUFFER_SIZE);
}
finally {
// Close stream even if copyStream() threw an IOException
try {
out.close();
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.CLOSING_DESTINATION);
}
}
}
/**
* Copies this file to a specified destination file, overwriting the destination if it exists. If this file is a
* directory, any file or directory it contains will also be copied.
*
* <p>This method throws an {@link IOException} if the operation failed, for any of the following reasons:
* <ul>
* <li>this file and the destination file are the same</li>
* <li>this file is a directory and a parent of the destination file (the operation would otherwise loop indefinitely)</li>
* <li>this file (or one if its children) cannot be read</li>
* <li>the destination file (or one of its children) can not be written</li>
* <li>an I/O error occurred</li>
* </ul>
* </p>
*
* <p>If this file supports the {@link FileOperation#COPY_REMOTELY} file operation, an attempt to perform a
* {@link #copyRemotelyTo(AbstractFile) remote copy} of the file to the destination is made. If the operation isn't
* supported or wasn't successful, the file is copied manually, by transferring its contents to the destination
* using {@link #copyRecursively(AbstractFile, AbstractFile)}.<br/>
* In that case, no clean up is performed if an error occurs in the midst of a transfer: files that have been copied
* (even partially) are left in the destination.<br/>
* It is also worth noting that symbolic links are not copied to the destination when encountered: neither the link
* nor the linked file is copied</p>
*
* @param destFile the destination file to copy this file to
* @throws IOException in any of the error cases listed above
*/
public final void copyTo(AbstractFile destFile) throws IOException {
// First, try to perform a remote copy of the file if the operation is supported
if(isFileOperationSupported(FileOperation.COPY_REMOTELY)) {
try {
copyRemotelyTo(destFile);
// Operation was a success, all done.
return;
}
catch(IOException e) {
// Fail silently
}
}
// Fall back to copying the file manually
checkCopyPrerequisites(destFile, false);
// Copy the file and its contents if the file is a directory
copyRecursively(this, destFile);
}
/**
* Moves this file to a specified destination file, overwriting the destination if it exists. If this file is a
* directory, any file or directory it contains will also be moved.
* After normal completion, this file will not exist anymore: {@link #exists()} will return <code>false</code>.
*
* <p>This method throws an {@link IOException} if the operation failed, for any of the following reasons:
* <ul>
* <li>this file and the destination file are the same</li>
* <li>this file is a directory and a parent of the destination file (the operation would otherwise loop indefinitely)</li>
* <li>this file (or one if its children) cannot be read</li>
* <li>this file (or one of its children) cannot be written</li>
* <li>the destination file (or one of its children) can not be written</li>
* <li>an I/O error occurred</li>
* </ul>
* </p>
*
* <p>If this file supports the {@link FileOperation#RENAME} file operation, an attempt to
* {@link #renameTo(AbstractFile) rename} the file to the destination is made. If the operation isn't supported
* or wasn't successful, the file is moved manually, by transferring its contents to the destination using
* {@link #copyTo(AbstractFile)} and then deleting the source.<br/>
* In that case, deletion of the source occurs only after all files have been successfully transferred.
* No clean up is performed if an error occurs in the midst of a transfer: files that have been copied
* (even partially) are left in the destination.<br/>
* It is also worth noting that symbolic links are not moved to the destination when encountered: neither the link
* nor the linked file is moved, and the symlink file is deleted.</p>
*
* @param destFile the destination file to move this file to
* @throws IOException in any of the error cases listed above
*/
public final void moveTo(AbstractFile destFile) throws IOException {
// First, try to rename the file if the operation is supported
if(isFileOperationSupported(FileOperation.RENAME)) {
try {
renameTo(destFile);
// Rename was a success, all done.
return;
}
catch(IOException e) {
// Fail silently
}
}
// Fall back to moving the file manually
copyTo(destFile);
// Delete the source file and its contents now that it has been copied OK.
// Note that the file won't be deleted if copyTo() failed (threw an IOException)
try {
deleteRecursively();
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.DELETING_SOURCE);
}
}
/**
* Creates this file as an empty, non-directory file. This method will fail (throw an <code>IOException</code>)
* if this file already exists. Note that this method may not always yield a zero-byte file (see below).
*
* <p>This generic implementation simply creates a zero-byte file. {@link AbstractRWArchiveFile} implementations
* may want to override this method so that it creates a valid archive with no entry. To illustrate, an empty Zip
* file with proper headers is 22-byte long.</p>
*
* @throws IOException if the file could not be created, either because it already exists or because of an I/O error
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public void mkfile() throws IOException, UnsupportedFileOperationException {
if(exists())
throw new IOException();
if(isFileOperationSupported(FileOperation.WRITE_FILE))
getOutputStream().close();
else
copyStream(new ByteArrayInputStream(new byte[]{}), false, 0);
}
/**
* Returns the children files that this file contains, filtering out files that do not match the specified FileFilter.
* For this operation to be successful, this file must be 'browsable', i.e. {@link #isBrowsable()} must return
* <code>true</code>.
*
* @param filter the FileFilter to be used to filter files out from the list, may be <code>null</code>
* @return the children files that this file contains
* @throws IOException if this operation is not possible (file is not browsable) or if an error occurred.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public AbstractFile[] ls(FileFilter filter) throws IOException, UnsupportedFileOperationException {
return filter==null?ls():filter.filter(ls());
}
/**
* Returns the children files that this file contains, filtering out files that do not match the specified FilenameFilter.
* For this operation to be successful, this file must be 'browsable', i.e. {@link #isBrowsable()} must return
* <code>true</code>.
*
* <p>This default implementation filters out files *after* they have been created. This method
* should be overridden if a more efficient implementation can be provided by subclasses.</p>
*
* @param filter the FilenameFilter to be used to filter out files from the list, may be <code>null</code>
* @return the children files that this file contains
* @throws IOException if this operation is not possible (file is not browsable) or if an error occurred.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public AbstractFile[] ls(FilenameFilter filter) throws IOException, UnsupportedFileOperationException {
return filter==null?ls():filter.filter(ls());
}
/**
* Changes this file's permissions to the specified permissions int.
* The permissions int should be constructed using the permission types and accesses defined in
* {@link com.mucommander.commons.file.PermissionType} and {@link com.mucommander.commons.file.PermissionAccess}.
*
* <p>Implementation note: the default implementation of this method calls sequentially {@link #changePermission(int, int, boolean)},
* for each permission and access (that's a total 9 calls). This may affect performance on filesystems which need
* to perform an I/O request to change each permission individually. In that case, and if the fileystem allows
* to change all permissions at once, this method should be overridden.</p>
*
* @param permissions new permissions for this file
* @throws IOException if the permissions couldn't be changed, either because of insufficient permissions or because
* of an I/O error.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public void changePermissions(int permissions) throws IOException, UnsupportedFileOperationException {
int bitShift = 0;
PermissionBits mask = getChangeablePermissions();
for(PermissionAccess a : PermissionAccess.values()) {
for(PermissionType p : PermissionType.values()) {
if(mask.getBitValue(a, p))
changePermission(a, p, (permissions & (1<<bitShift))!=0);
bitShift++;
}
}
}
/**
* Returns a string representation of this file's permissions.
*
* <p>The first character is 'l' if this file is a symbolic link,'d' if it is a directory, '-' otherwise. Then
* the string contains up to 3 character triplets, for each of the 'user', 'group' and 'other' access types, each
* containing the following characters:
* <ul>
* <li>'r' if this file has read permission, '-' otherwise
* <li>'w' if this file has write permission, '-' otherwise
* <li>'x' if this file has executable permission, '-' otherwise
* </ul>
* </p>
*
* <p>The first character triplet for 'user' access will always be added to the permissions. Then the 'group' and
* 'other' triplets will only be added if at least one of the user permission bits is supported, as tested with
* this file's permissions mask.
* Here are a couple examples to illustrate:
* <ul>
* <li>a directory for which the file permissions' mask is 0 will return the string <code>d---</code>, no matter
* what permission values the FilePermissions returned by {@link #getPermissions()} contains</li>.
* <li>a regular file for which the file permissions' mask returns 777 (full permissions support) and which
* has read/write/executable permissions for all three 'user', 'group' and 'other' access types will return
* <code>-rwxrwxrwx</code></li>.
* </ul>
* </p>
*
* @return a string representation of this file's permissions
*/
public String getPermissionsString() {
FilePermissions permissions = getPermissions();
int supportedPerms = permissions.getMask().getIntValue();
String s = "";
s += isSymlink()?'l':isDirectory()?'d':'-';
int perms = permissions.getIntValue();
int bitShift = PermissionAccess.USER.toInt() *3;
// Permissions go by triplets (rwx), there are 3 of them for respectively 'owner', 'group' and 'other' accesses.
// The first one ('owner') will always be displayed, regardless of the permission bit mask. 'Group' and 'other'
// will be displayed only if the permission mask contains information about them (at least one permission bit).
for(PermissionAccess a : PermissionAccess.reverseValues()) {
if(a==PermissionAccess.USER || (supportedPerms & (7<<bitShift))!=0) {
for(PermissionType p : PermissionType.reverseValues()) {
if((perms & (p.toInt()<<bitShift))==0)
s += '-';
else
s += p==PermissionType.READ?'r':p==PermissionType.WRITE?'w':'x';
}
}
bitShift -= 3;
}
return s;
}
/**
* Deletes this file. If the file is a directory, enclosing files are deleted recursively.
* Symbolic links to directories are simply deleted, without deleting the contents of the linked directory.
*
* @throws IOException if an error occurred while deleting a file or listing a directory's contents
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public void deleteRecursively() throws IOException, UnsupportedFileOperationException {
deleteRecursively(this);
}
/**
* Returns <code>true</code> if the specified file operation and corresponding method is supported by this
* file implementation. See the {@link FileOperation} enum for a complete list of file operations and their
* corresponding <code>AbstractFile</code> methods.
* <p>
* Note that even if <code>true</code> is returned, this doesn't ensure that the file operation will succeed:
* additional conditions may be required for the operation to succeed and the corresponding method may throw an
* <code>IOException</code> if those conditions are not met.
* </p>
*
* @param op a file operation
* @return <code>true</code> if the specified file operation is supported by this filesystem.
* @see FileOperation
*/
public boolean isFileOperationSupported(FileOperation op) {
return isFileOperationSupported(op, getClass());
}
///////////////////
// Final methods //
///////////////////
/**
* Returns <code>true</code> if this file is browsable. A file is considered browsable if it contains children files
* that can be retrieved by calling the <code>ls()</code> methods. Archive files will usually return
* <code>true</code>, as will directories (directories are always browsable).
*
* @return true if this file is browsable
*/
public final boolean isBrowsable() {
return isDirectory() || isArchive();
}
/**
* Returns the name of the file without its extension.
*
* <p>A filename has an extension if and only if:<br/>
* - it contains at least one <code>.</code> character<br/>
* - the last <code>.</code> is not the last character of the filename<br/>
* - the last <code>.</code> is not the first character of the filename<br/>
* If this file has no extension, its full name is returned.</p>
*
* @return this file's name, without its extension.
* @see #getName()
* @see #getExtension()
*/
public final String getNameWithoutExtension() {
String name = getName();
int position = name.lastIndexOf('.');
if((position<=0) || (position == name.length() - 1))
return name;
return name.substring(0, position);
}
/**
* Shorthand for {@link #getAbsolutePath()}.
*
* @return the value returned by {@link #getAbsolutePath()}.
*/
public final String getPath() {
return getAbsolutePath();
}
/**
* Returns the absolute path to this file.
* A separator character will be appended to the returned path if <code>true</code> is passed.
*
* @param appendSeparator if true, a separator will be appended to the returned path
* @return the absolute path to this file
*/
public final String getAbsolutePath(boolean appendSeparator) {
String path = getAbsolutePath();
return appendSeparator?addTrailingSeparator(path): removeTrailingSeparator(path);
}
/**
* Returns the canonical path to this file, resolving any symbolic links or '..' and '.' occurrences.
* A separator character will be appended to the returned path if <code>true</code> is passed.
*
* @param appendSeparator if true, a separator will be appended to the returned path
* @return the canonical path to this file
*/
public final String getCanonicalPath(boolean appendSeparator) {
String path = getCanonicalPath();
return appendSeparator?addTrailingSeparator(path): removeTrailingSeparator(path);
}
/**
* Returns a child of this file, whose path is the concatenation of this file's path and the given relative path.
* Although this method does not enforce it, the specified path should be relative, i.e. should not start with
* a separator.<br/>
* An <code>IOException</code> may be thrown if the child file could not be instantiated but the returned file
* instance should never be <code>null</code>.
*
* @param relativePath the child's path, relative to this file's path
* @return an AbstractFile representing the requested child file, never null
* @throws IOException if the child file could not be instantiated
*/
public final AbstractFile getChild(String relativePath) throws IOException {
FileURL childURL = (FileURL)getURL().clone();
childURL.setPath(addTrailingSeparator(childURL.getPath())+ relativePath);
return FileFactory.getFile(childURL, true);
}
/**
* Convenience method that acts as {@link #getChild(String)} except that it does not throw {@link IOException} but
* returns <code>null</code> if the child could not be instantiated.
*
* @param relativePath the child's path, relative to this file's path
* @return an AbstractFile representing the requested child file, <code>null</code> if it could not be instantiated
*/
public final AbstractFile getChildSilently(String relativePath) {
try {
return getChild(relativePath);
}
catch(IOException e) {
return null;
}
}
/**
* Returns a direct child of this file, whose path is the concatenation of this file's path and the given filename.
* An <code>IOException</code> will be thrown in any of the following cases:
* <ul>
* <li>if the filename contains one or several path separator (the file would not be a direct child)</li>
* <li>if the child file could not be instantiated</li>
* </ul>
* This method never returns <<code>null</code>.
*
* <p>Although {@link #getChild} can be used to retrieve a direct child file, this method should be favored because
* it allows to use this file instance as the parent of the returned child file.</p>
*
* @param filename the name of the child file to be created
* @return an AbstractFile representing the requested direct child file, never null
* @throws IOException in any of the cases listed above
*/
public final AbstractFile getDirectChild(String filename) throws IOException {
if(filename.indexOf(getSeparator())!=-1)
throw new IOException();
AbstractFile childFile = getChild(filename);
// Use this file as the child's parent, it avoids creating a new AbstractFile instance when getParent() is called
childFile.setParent(this);
return childFile;
}
/**
* Convenience method that creates a directory as a direct child of this directory.
* This method will fail if this file is not a directory.
*
* @param name name of the directory to create
* @throws IOException if the directory could not be created, either because the file already exists or for any
* other reason.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public final void mkdir(String name) throws IOException, UnsupportedFileOperationException {
getChild(name).mkdir();
}
/**
* Creates this file as a directory and any parent directory that does not already exist. This method will fail
* (throw an <code>IOException</code>) if this file already exists. It may also fail because of an I/O error ;
* in this case, this method will not remove the parent directories it has created (if any).
*
* @throws IOException if this file already exists or if an I/O error occurred.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public final void mkdirs() throws IOException, UnsupportedFileOperationException {
AbstractFile parent;
if(((parent=getParent())!=null) && !parent.exists())
parent.mkdirs();
mkdir();
}
/**
* Convenience method that creates a file as a direct child of this directory.
* This method will fail if this file is not a directory.
*
* @param name name of the file to create
* @throws IOException if the file could not be created, either because the file already exists or for any
* other reason.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public final void mkfile(String name) throws IOException, UnsupportedFileOperationException {
getChild(name).mkfile();
}
/**
* Returns the immediate ancestor of this <code>AbstractFile</code> if it has one, <code>this</code> otherwise:
* <ul>
* <li>if this file is a {@link ProxyFile}, returns the return value of {@link ProxyFile#getProxiedFile()}
* <li>if this file is not a <code>ProxyFile</code>, returns <code>this</code>
* </ul>
*
* @return the immediate ancestor of this <code>AbstractFile</code> if it has one, <code>this</code> otherwise
*/
public final AbstractFile getAncestor() {
if(this instanceof ProxyFile)
return ((ProxyFile)this).getProxiedFile();
return this;
}
/**
* Returns the first ancestor of this file that is an instance of the given Class or of a subclass of it,
* or <code>this</code> if this instance's class matches those criteria. Returns <code>null</code> if this
* file has no such ancestor.
* <br>
* Note that this method will always return <code>this</code> if <code>AbstractFile.class</code> is specified.
*
* @param abstractFileClass a Class corresponding to an AbstractFile subclass
* @return the first ancestor of this file that is an instance of the given Class or of a subclass of the given
* Class, or <code>this</code> if this instance's class matches those criteria. Returns <code>null</code> if this
* file has no such ancestor.
*/
public final <T extends AbstractFile> T getAncestor(Class<T> abstractFileClass) {
AbstractFile ancestor = this;
AbstractFile lastAncestor;
do {
if(abstractFileClass.isAssignableFrom(ancestor.getClass()))
return (T) ancestor;
lastAncestor = ancestor;
ancestor = ancestor.getAncestor();
}
while(lastAncestor!=ancestor);
return null;
}
/**
* Iterates through the ancestors returned by {@link #getAncestor()} until the top-most ancestor is reached and
* returns it. If this file has no ancestor, <code>this</code> will be returned.
*
* @return returns the top-most ancestor of this file, <code>this</code> if this file has no ancestor
*/
public final AbstractFile getTopAncestor() {
AbstractFile topAncestor = this;
while(topAncestor.hasAncestor())
topAncestor = topAncestor.getAncestor();
return topAncestor;
}
/**
* Returns <code>true</code> if this <code>AbstractFile</code> has an ancestor, i.e. if this file is a
* {@link ProxyFile}, <code>false</code> otherwise.
*
* @return <code>true</code> if this <code>AbstractFile</code> has an ancestor, <code>false</code> otherwise.
*/
public final boolean hasAncestor() {
return this instanceof ProxyFile;
}
/**
* Returns <code>true</code> if this file is or has an ancestor (immediate or not) that is an instance of the given
* <code>Class</code> or of a subclass of the <code>Class</code>. Note that the specified must correspond to an
* <code>AbstractFile</code> subclass. Specifying any other Class will always yield to this method returning
* <code>false</code>. Also note that this method will always return <code>true</code> if
* <code>AbstractFile.class</code> is specified.
*
* @param abstractFileClass a Class corresponding to an AbstractFile subclass
* @return <code>true</code> if this file has an ancestor (immediate or not) that is an instance of the given Class
* or of a subclass of the given Class.
*/
public final boolean hasAncestor(Class<? extends AbstractFile> abstractFileClass) {
AbstractFile ancestor = this;
AbstractFile lastAncestor;
do {
if(abstractFileClass.isAssignableFrom(ancestor.getClass()))
return true;
lastAncestor = ancestor;
ancestor = ancestor.getAncestor();
}
while(lastAncestor!=ancestor);
return false;
}
/**
* Returns <code>true</code> if this file is a parent folder of the given file, or if the two files are equal.
*
* @param file the AbstractFile to test
* @return true if this file is a parent folder of the given file, or if the two files are equal
*/
public final boolean isParentOf(AbstractFile file) {
return isBrowsable() && file.getCanonicalPath(true).startsWith(getCanonicalPath(true));
}
/**
* Convenience method that returns the parent {@link AbstractArchiveFile} that contains this file. If this file
* is an {@link AbstractArchiveFile} or an ancestor of {@link AbstractArchiveFile}, <code>this</code> is returned.
* If this file is neither contained by an archive nor is an archive, <code>null</code> is returned.
*
* <p>
* <b>Important note:</b> the returned {@link AbstractArchiveFile}, if any, may not necessarily be an
* archive, as specified by {@link #isArchive()}. This is the case for files that were resolved as
* {@link AbstractArchiveFile} instances based on their path, but that do not yet exist or were created as
* directories. On the contrary, an existing archive will necessarily return a non-null value.
* </p>
*
* @return the parent {@link AbstractArchiveFile} that contains this file
*/
public final AbstractArchiveFile getParentArchive() {
if(hasAncestor(AbstractArchiveFile.class))
return getAncestor(AbstractArchiveFile.class);
else if(hasAncestor(AbstractArchiveEntryFile.class))
return getAncestor(AbstractArchiveEntryFile.class).getArchiveFile();
return null;
}
/**
* Returns an icon representing this file, using the default {@link com.mucommander.commons.file.icon.FileIconProvider}
* registered in {@link FileFactory}. The specified preferred resolution will be used as a hint, but the returned
* icon may have different dimension; see {@link com.mucommander.commons.file.icon.FileIconProvider#getFileIcon(AbstractFile, java.awt.Dimension)}
* for full details.
* This method may return <code>null</code> if the JVM is running on a headless environment.
*
* @param preferredResolution the preferred icon resolution
* @return an icon representing this file, <code>null</code> if the JVM is running on a headless environment
* @see com.mucommander.commons.file.FileFactory#getDefaultFileIconProvider()
* @see com.mucommander.commons.file.icon.FileIconProvider#getFileIcon(AbstractFile, java.awt.Dimension)
*/
public final Icon getIcon(Dimension preferredResolution) {
return FileFactory.getDefaultFileIconProvider().getFileIcon(this, preferredResolution);
}
/**
* Returns an icon representing this file, using the default {@link com.mucommander.commons.file.icon.FileIconProvider}
* registered in {@link FileFactory}. The default preferred resolution for the icon is 16x16 pixels.
* This method may return <code>null</code> if the JVM is running on a headless environment.
*
* @return an icon representing this file, <code>null</code> if the JVM is running on a headless environment
* @see com.mucommander.commons.file.FileFactory#getDefaultFileIconProvider()
* @see com.mucommander.commons.file.icon.FileIconProvider#getFileIcon(AbstractFile, java.awt.Dimension)
*/
public final Icon getIcon() {
// Note: the Dimension object is created here instead of returning a final static field, because creating
// a Dimension object triggers the AWT and Swing classes loading. Since these classes are not
// needed in a headless environment, we want them to be loaded only if strictly necessary.
return getIcon(new java.awt.Dimension(16, 16));
}
/**
* Returns a checksum of this file (also referred to as <i>hash</i> or <i>digest</i>) calculated by reading this
* file's contents and feeding the bytes to the given <code>MessageDigest</code>, until EOF is reached.
*
* <p>The checksum is returned as an hexadecimal string, such as "6d75636f0a". The length of this string depends on
* the kind of algorithm.</p>
*
* <p>Note: this method does not reset the <code>MessageDigest</code> after the checksum has been calculated.</p>
*
* @param algorithm the algorithm to use for calculating the checksum
* @return this file's checksum, as an hexadecimal string
* @throws IOException if an I/O error occurred while calculating the checksum
* @throws NoSuchAlgorithmException if the specified algorithm does not correspond to any MessageDigest registered
* with the Java Cryptography Extension.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public final String calculateChecksum(String algorithm) throws IOException, NoSuchAlgorithmException, UnsupportedFileOperationException {
return calculateChecksum(MessageDigest.getInstance(algorithm));
}
/**
* Returns a checksum of this file (also referred to as <i>hash</i> or <i>digest</i>) calculated by reading this
* file's contents and feeding the bytes to the given <code>MessageDigest</code>, until EOF is reached.
*
* <p>The checksum is returned as an hexadecimal string, such as "6d75636f0a". The length of this string depends on
* the kind of <code>MessageDigest</code>.</p>
*
* <p>Note: this method does not reset the <code>MessageDigest</code> after the checksum has been calculated.</p>
*
* @param messageDigest the MessageDigest to use for calculating the checksum
* @return this file's checksum, as an hexadecimal string
* @throws IOException if an I/O error occurred while calculating the checksum
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public final String calculateChecksum(MessageDigest messageDigest) throws IOException, UnsupportedFileOperationException {
InputStream in = getInputStream();
try {
return calculateChecksum(in, messageDigest);
}
finally {
in.close();
}
}
/**
* Tests if the given path contains a trailing separator, and if not, adds one to the returned path.
* The separator used is the one returned by {@link #getSeparator()}.
*
* @param path the path for which to add a trailing separator
* @return the path with a trailing separator
*/
public final String addTrailingSeparator(String path) {
// Even though getAbsolutePath() is not supposed to return a trailing separator, root folders ('/', 'c:\' ...)
// are exceptions that's why we still have to test if path ends with a separator
String separator = getSeparator();
if(!path.endsWith(separator))
return path+separator;
return path;
}
/**
* Tests if the given path contains a trailing separator, and if it does, removes it from the returned path.
* The separator used is the one returned by {@link #getSeparator()}.
*
* @param path the path for which to remove the trailing separator
* @return the path free of a trailing separator
*/
protected final String removeTrailingSeparator(String path) {
// Remove trailing slash if path is not '/' or trailing backslash if path does not end with ':\'
// (Reminder: C: is C's current folder, while C:\ is C's root)
String separator = getSeparator();
if(path.endsWith(separator)
&& !((separator.equals("/") && path.length()==1) || (separator.equals("\\") && path.charAt(path.length()-2)==':')))
path = path.substring(0, path.length()-1);
return path;
}
/**
* Checks the prerequisites of a copy (or move) operation.
* Throws a {@link FileTransferException} if any of the following conditions are true, does nothing otherwise:
* <ul>
* <li>this file does not exist</li>
* <li>this file and the destination file are the same, unless <code>allowCaseVariations</code> is <code>true</code>
* and the destination filename is a case variation of the source</li>
* <li>this file is a parent of the destination file</li>
* </ul>
*
* @param destFile the destination file to copy this file to
* @param allowCaseVariations prevents throwing an exception if both file names are a case variation of one another
* @throws FileTransferException in any of the cases listed above, use {@link FileTransferException#getReason()} to
* know the reason.
*/
protected final void checkCopyPrerequisites(AbstractFile destFile, boolean allowCaseVariations) throws FileTransferException {
boolean isAllowedCaseVariation = false;
// Throw an exception of a specific kind if the source and destination files refer to the same file
boolean filesEqual = this.equalsCanonical(destFile);
if(filesEqual) {
// If case variations are allowed and the destination filename is a case variation of the source,
// do not throw an exception.
if(allowCaseVariations) {
String sourceFileName = getName();
String destFileName = destFile.getName();
if(sourceFileName.equalsIgnoreCase(destFileName) && !sourceFileName.equals(destFileName))
isAllowedCaseVariation = true;
}
if(!isAllowedCaseVariation)
throw new FileTransferException(FileTransferError.SOURCE_AND_DESTINATION_IDENTICAL);
}
// Throw an exception if source is a parent of destination
if(!filesEqual && isParentOf(destFile)) // Note: isParentOf(destFile) returns true if both files are equal
throw new FileTransferException(FileTransferError.SOURCE_PARENT_OF_DESTINATION);
// Throw an exception if the source file does not exist
if(!exists())
throw new FileTransferException(FileTransferError.FILE_NOT_FOUND);
}
/**
* Checks the prerequisites of a {@link #copyRemotelyTo(AbstractFile)} operation.
* This method starts by verifying the following requirements and throws an <code>IOException</code> if one of them
* isn't met:
* <ul>
* <li>both files' schemes are equal</li>
* <li>both files' {@link #getTopAncestor() top ancestors} are equal</li>
* <li>both files' hosts are equal, or <code>allowDifferentHosts</code> is <code>true</code></li>
* </ul>
* If all those requirements are met, {@link #checkCopyPrerequisites(AbstractFile, boolean)} is called with the
* destination file and <code>allowCaseVariations</code> flag to perform prerequisites verifications.
*
* @param destFile the destination file to copy this file to
* @param allowCaseVariations prevents throwing an exception if both file names are a case variation of one another
* @param allowDifferentHosts prevents throwing an exception if both files have the same host
* @throws FileTransferException in any of the cases listed above, use {@link FileTransferException#getReason()} to
* know the reason.
* @see #checkCopyPrerequisites(AbstractFile, boolean)
*/
protected final void checkCopyRemotelyPrerequisites(AbstractFile destFile, boolean allowCaseVariations, boolean allowDifferentHosts) throws IOException, FileTransferException {
if(!fileURL.schemeEquals(fileURL)
|| !destFile.getTopAncestor().getClass().equals(getTopAncestor().getClass())
|| (!allowDifferentHosts && !destFile.getURL().hostEquals(fileURL)))
throw new IOException();
checkCopyPrerequisites(destFile, allowCaseVariations);
}
/**
* Checks the prerequisites of a {@link #renameTo(AbstractFile)} operation.
* This method starts by verifying the following requirements and throws an <code>IOException</code> if one of them
* isn't met:
* <ul>
* <li>both files' schemes are equal</li>
* <li>both files' {@link #getTopAncestor() top ancestors} are equal</li>
* <li>both files' hosts are equal, or <code>allowDifferentHosts</code> is <code>true</code></li>
* </ul>
* If all those requirements are met, {@link #checkCopyPrerequisites(AbstractFile, boolean)} is called with the
* destination file and <code>allowCaseVariations</code> flag to perform further prerequisites verifications.
*
* @param destFile the destination file to copy this file to
* @param allowCaseVariations prevents throwing an exception if both file names are a case variation of one another
* @param allowDifferentHosts prevents throwing an exception if both files have the same host
* @throws FileTransferException in any of the cases listed above, use {@link FileTransferException#getReason()} to
* know the reason.
* @see #checkCopyPrerequisites(AbstractFile, boolean)
*/
protected final void checkRenamePrerequisites(AbstractFile destFile, boolean allowCaseVariations, boolean allowDifferentHosts) throws IOException, FileTransferException {
checkCopyRemotelyPrerequisites(destFile, allowCaseVariations, allowDifferentHosts);
}
/**
* Copies the source file to the destination one and recurses on directory contents.
* This method assumes that the destination file does not exists, this must be checked prior to calling this method.
* Symbolic links are skipped when encountered: neither the link nor the linked file are copied.
*
* @param sourceFile the file to copy
* @param destFile the destination file
* @throws FileTransferException if an error occurred while copying the file
*/
protected final void copyRecursively(AbstractFile sourceFile, AbstractFile destFile) throws FileTransferException {
if(sourceFile.isSymlink())
return;
if(sourceFile.isDirectory()) {
try {
destFile.mkdir();
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.WRITING_DESTINATION);
}
AbstractFile children[];
try {
children = sourceFile.ls();
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.READING_SOURCE);
}
AbstractFile destChild;
for (AbstractFile child : children) {
try {
destChild = destFile.getDirectChild(child.getName());
}
catch (IOException e) {
throw new FileTransferException(FileTransferError.OPENING_DESTINATION);
}
copyRecursively(child, destChild);
}
}
else {
InputStream in;
try {
in = sourceFile.getInputStream();
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.OPENING_SOURCE);
}
try {
destFile.copyStream(in, false, sourceFile.getSize());
}
finally {
// Close stream even if copyStream() threw an IOException
try {
in.close();
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.CLOSING_SOURCE);
}
}
}
}
/**
* Deletes the given file. If the file is a directory, enclosing files are deleted recursively.
* Symbolic links to directories are simply deleted, without deleting the contents of the linked directory.
*
* @param file the file to delete
* @throws IOException if an error occurred while deleting a file or listing a directory's contents
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
protected final void deleteRecursively(AbstractFile file) throws IOException, UnsupportedFileOperationException {
if(file.isDirectory() && !file.isSymlink()) {
AbstractFile children[] = file.ls();
for (AbstractFile child : children)
deleteRecursively(child);
}
file.delete();
}
/**
* Convenience method that calls {@link #changePermissions(int)} with the given permissions' int value.
*
* @param permissions new permissions for this file
* @throws IOException if the permissions couldn't be changed, either because of insufficient permissions or because
* of an I/O error.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public final void changePermissions(FilePermissions permissions) throws IOException, UnsupportedFileOperationException {
changePermissions(permissions.getIntValue());
}
/**
* This method is a shorthand for {@link #importPermissions(AbstractFile, FilePermissions)} called with
* {@link FilePermissions#DEFAULT_DIRECTORY_PERMISSIONS} if this file is a directory or
* {@link FilePermissions#DEFAULT_FILE_PERMISSIONS} if this file is a regular file.
*
* @param sourceFile the file from which to import permissions
* @throws IOException if the permissions couldn't be changed, either because of insufficient permissions or because
* of an I/O error.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
*/
public final void importPermissions(AbstractFile sourceFile) throws IOException, UnsupportedFileOperationException {
importPermissions(sourceFile,isDirectory()
? FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS
: FilePermissions.DEFAULT_FILE_PERMISSIONS);
}
/**
* Imports the given source file's permissions, overwriting this file's permissions. Only the bits that are
* supported by the source file (as reported by the permissions' mask) are preserved. Other bits are be
* set to those of the specified default permissions.
* See {@link SimpleFilePermissions#padPermissions(FilePermissions, FilePermissions)} for more information about
* permissions padding.
*
* @param sourceFile the file from which to import permissions
* @param defaultPermissions default permissions to use
* @throws IOException if the permissions couldn't be changed, either because of insufficient permissions or because
* of an I/O error.
* @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported
* or not implemented by the underlying filesystem.
* @see SimpleFilePermissions#padPermissions(FilePermissions, FilePermissions)
*/
public final void importPermissions(AbstractFile sourceFile, FilePermissions defaultPermissions) throws IOException, UnsupportedFileOperationException {
changePermissions(SimpleFilePermissions.padPermissions(sourceFile.getPermissions(), defaultPermissions).getIntValue());
}
////////////////////
// Static methods //
////////////////////
/**
* Returns <code>true</code> if the specified file operation and corresponding method is supported by the
* given <code>AbstractFile</code> implementation.<br>
* See the {@link FileOperation} enum for a complete list of file operations and their corresponding
* <code>AbstractFile</code> methods.
*
* @param op a file operation
* @param c the file implementation to test
* @return <code>true</code> if the specified file operation is supported by this filesystem.
* @see FileOperation
*/
public static boolean isFileOperationSupported(FileOperation op, Class<? extends AbstractFile> c) {
return !op.getCorrespondingMethod(c).isAnnotationPresent(UnsupportedFileOperation.class);
}
/**
* Returns the given filename's extension, <code>null</code> if the filename doesn't have an extension.
*
* <p>A filename has an extension if and only if:<br/>
* - it contains at least one <code>.</code> character<br/>
* - the last <code>.</code> is not the last character of the filename<br/>
* - the last <code>.</code> is not the first character of the filename</p>
*
* <p>
* The returned extension (if any) is free of any extension separator character (<code>.</code>). For instance,
* this method will return <code>"ext"</code> for a file named <code>"name.ext"</code>, <b>not</b> <code>".ext"</code>.
* </p>
*
* @param filename a filename, not a full path
* @return the given filename's extension, <code>null</code> if the filename doesn't have an extension
*/
public static String getExtension(String filename) {
int lastDotPos = filename.lastIndexOf('.');
int len;
if(lastDotPos<=0 || lastDotPos==(len=filename.length())-1)
return null;
return filename.substring(lastDotPos+1, len);
}
/**
* Returns the given filename without its extension (base name). if the filename doesn't have an extension, returns the filename as received
*
* <p>A filename has an extension if and only if:<br/>
* - it contains at least one <code>.</code> character<br/>
* - the last <code>.</code> is not the last character of the filename<br/>
* - the last <code>.</code> is not the first character of the filename</p>
*
* @return the file's base name - without its extension, if the filename doesn't have an extension returns the filename as received
*/
public String getBaseName() {
String fileName = getName();
int lastDotPos = fileName.lastIndexOf('.');
if(lastDotPos<=0 || lastDotPos==fileName.length()-1)
return fileName;
return fileName.substring(0, lastDotPos);
}
/**
* Returns the checksum (also referred to as <i>hash</i> or <i>digest</i>) of the given <code>InputStream</code>
* calculated by reading the stream and feeding the bytes to the given <code>MessageDigest</code> until EOF is
* reached.
*
* <p><b>Important:</b> this method does not close the <code>InputStream</code>, and does not reset the
* <code>MessageDigest</code> after the checksum has been calculated.</p>
*
* @param in the InputStream for which to calculate the checksum
* @param messageDigest the MessageDigest to use for calculating the checksum
* @return the given InputStream's checksum, as an hexadecimal string
* @throws IOException if an I/O error occurred while calculating the checksum
*/
public static String calculateChecksum(InputStream in, MessageDigest messageDigest) throws IOException {
ChecksumInputStream cin = new ChecksumInputStream(in, messageDigest);
try {
StreamUtils.readUntilEOF(cin);
return cin.getChecksumString();
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.READING_SOURCE);
}
}
////////////////////////
// Overridden methods //
////////////////////////
/**
* Tests a file for equality by comparing both files' {@link #getURL() URL}. Returns <code>true</code> if the URL
* of this file and the specified one are equal according to {@link FileURL#equals(Object, boolean, boolean)} called
* with credentials and properties comparison enabled.
*
* <p>
* Unlike {@link #equalsCanonical(Object)}, this method <b>is not</b> allowed to perform I/O operations and block
* the caller thread.
* </p>
*
* @param o the object to compare against this instance
* @return Returns <code>true</code> if the URL of this file and the specified one are equal
* @see FileURL#equals(Object, boolean, boolean)
* @see #equalsCanonical(Object)
*/
public boolean equals(Object o) {
if(o==null || !(o instanceof AbstractFile))
return false;
return getURL().equals(((AbstractFile)o).getURL(), true, true);
}
/**
* Tests a file for equality by comparing both files' {@link #getCanonicalPath() canonical path}.
* Returns <code>true</code> if the canonical path of this file and the specified one are equal.
*
* <p>It is noteworthy that this method uses <code>java.lang.String#equals(Object)</code> to compare paths, which
* in some rare cases may return <code>false</code> for non-ascii/Unicode paths that have the same written
* representation but are not equal according to <code>java.lang.String#equals(Object)</code>. Handling such cases
* would require a locale-aware String comparison which is not an option here.</p>
*
* <p>It is also worth noting that hostnames are not resolved, which means this method does not consider
* a hostname and its corresponding IP address as being equal.</p>
*
* <p>Unlike {@link #equals(Object)}, this method <b>is</b> allowed to perform I/O operations and block
* the caller thread.</p>
*
* @param o the object to compare against this instance
* @return <code>true</code> if the canonical path of this file and the specified one are equal.
* @see #equals(Object)
*/
public boolean equalsCanonical(Object o) {
if(o==null || !(o instanceof AbstractFile))
return false;
// TODO: resolve hostnames ?
return getCanonicalPath(false).equals(((AbstractFile)o).getCanonicalPath(false));
}
/**
* Returns the hashCode of this file's {@link #getURL() URL}.
*
* @return the hashCode of this file's {@link #getURL() URL}.
*/
public int hashCode() {
return getURL().hashCode();
}
/**
* Returns a String representation of this file. The returned String is this file's path as returned by
* {@link #getAbsolutePath()}.
*/
public String toString() {
return getAbsolutePath();
}
//////////////////////
// Abstract methods //
//////////////////////
/**
* Returns this file's last modified date, in milliseconds since the epoch (00:00:00 GMT, January 1, 1970).
*
* @return this file's last modified date, in milliseconds since the epoch (00:00:00 GMT, January 1, 1970)
*/
public abstract long getDate();
/**
* Changes this file's last modified date to the specified one. Throws an <code>IOException</code> if the date
* couldn't be changed, either because of insufficient permissions or because of an I/O error.
*
* <p>This {@link FileOperation#CHANGE_DATE file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @param lastModified last modified date, in milliseconds since the epoch (00:00:00 GMT, January 1, 1970)
* @throws IOException if the date couldn't be changed, either because of insufficient permissions or because of
* an I/O error.
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract void changeDate(long lastModified) throws IOException, UnsupportedFileOperationException;
/**
* Returns this file's size in bytes, <code>0</code> if this file doesn't exist, <code>-1</code> if the size is
* undetermined.
*
* @return this file's size in bytes, 0 if this file doesn't exist, -1 if the size is undetermined
*/
public abstract long getSize();
/**
* Returns this file's parent, <code>null</code> if it doesn't have one.
*
* @return this file's parent, <code>null</code> if it doesn't have one
*/
public abstract AbstractFile getParent();
/**
* Sets this file's parent. <code>null</code> can be specified if this file doesn't have a parent.
*
* @param parent the new parent of this file
*/
public abstract void setParent(AbstractFile parent);
/**
* Returns <code>true</code> if this file exists.
*
* @return <code>true</code> if this file exists
*/
public abstract boolean exists();
/**
* Returns this file's permissions, as a {@link FilePermissions} object. Note that this file may only support
* certain permission bits, use the {@link com.mucommander.commons.file.FilePermissions#getMask() permission mask} to find
* out which bits are supported.
*
* <p>This method may return permissions for which none of the bits are supported, but may never return
* <code>null</code>.</p>
*
* @return this file's permissions, as a FilePermissions object
*/
public abstract FilePermissions getPermissions();
/**
* Returns a bit mask describing the permission bits that can be changed on this file when calling
* {@link #changePermission(int, int, boolean)} and {@link #changePermissions(int)}.
*
* @return a bit mask describing the permission bits that can be changed on this file
*/
public abstract PermissionBits getChangeablePermissions();
/**
* Changes the specified permission bit.
*
* <p>This {@link FileOperation#CHANGE_PERMISSION file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @param access see {@link PermissionType} for allowed values
* @param permission see {@link PermissionAccess} for allowed values
* @param enabled true to enable the flag, false to disable it
* @throws IOException if the permission couldn't be changed, either because of insufficient permissions or because
* of an I/O error.
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
* @see #getChangeablePermissions()
*/
public abstract void changePermission(PermissionAccess access, PermissionType permission, boolean enabled) throws IOException, UnsupportedFileOperationException;
/**
* Returns information about the owner of this file. The kind of information that is returned is implementation-dependant.
* It may typically be a username (e.g. 'bob') or a user ID (e.g. '501').
* If the owner information is not available to the <code>AbstractFile</code> implementation (cannot be retrieved or
* the filesystem doesn't have any notion of owner) or not available for this particular file, <code>null</code>
* will be returned.
*
* @return information about the owner of this file
*/
public abstract String getOwner();
/**
* Returns <code>true</code> if this file implementation is able to return some information about file owners, not
* necessarily for all files or this file in particular but at least for some of them. In other words, a
* <code>true</code> return value doesn't mean that {@link #getOwner()} will necessarily return a non-null value,
* but rather that there is a chance that it does.
*
* @return true if this file implementation is able to return information about file owners
*/
public abstract boolean canGetOwner();
/**
* Returns information about the group this file belongs to. The kind of information that is returned is implementation-dependant.
* It may typically be a group name (e.g. 'www-data') or a group ID (e.g. '501').
* If the group information is not available to the <code>AbstractFile</code> implementation (cannot be retrieved or
* the filesystem doesn't have any notion of owner) or not available for this particular file, <code>null</code>
* will be returned.
*
* @return information about the owner of this file
*/
public abstract String getGroup();
/**
* Returns <code>true</code> if this file implementation is able to return some information about file groups, not
* necessarily for all files or this file in particular but at least for some of them. In other words, a
* <code>true</code> return value doesn't mean that {@link #getGroup()} will necessarily return a non-null value,
* but rather that there is a chance that it does.
*
* @return true if this file implementation is able to return information about file groups
*/
public abstract boolean canGetGroup();
/**
* Returns <code>true</code> if this file is a directory, <code>false</code> in any of the following cases:
* <ul>
* <li>this file does not exist</li>
* <li>this file is a regular file</li>
* <li>this file is an {@link #isArchive() archive}</li>
* </ul>
*
* @return <code>true</code> if this file is a directory, <code>false</code> in any of the cases listed above
*/
public abstract boolean isDirectory();
/**
* Returns <code>true</code> if this file is an archive.
* <p>
* An archive is a file container that can be {@link #isBrowsable() browsed}. Archive files may not be
* {@link #isDirectory() directories}, and vice-versa.
* </p>.
*
* @return <code>true</code> if this file is an archive.
*/
public abstract boolean isArchive();
/**
* Returns <code>true</code> if this file is a symbolic link. Symbolic links need to be handled with special care,
* especially when manipulating files recursively.
*
* @return <code>true</code> if this file is a symbolic link
*/
public abstract boolean isSymlink();
/**
* Returns <code>true</code> if this file is a system file.
* Note that system file attribute depends on the OS, so we can know it only for local files:
* - For MAC OS, {@link MacOsSystemFolder} defines the group of system files
* - On Windows, files has special attribute that mark them as system files
*
* @return <code>true</code> if this file is a system file
*/
public abstract boolean isSystem();
/**
* Returns the children files that this file contains. For this operation to be successful, this file must be
* 'browsable', i.e. {@link #isBrowsable()} must return <code>true</code>.
* This method may return a zero-length array if it has no children but may never return <code>null</code>.
*
* <p>This {@link FileOperation#LIST_CHILDREN file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @return the children files that this file contains
* @throws IOException if this operation is not possible (file is not browsable) or if an error occurred.
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract AbstractFile[] ls() throws IOException, UnsupportedFileOperationException;
/**
* Creates this file as a directory. This method will fail (throw an <code>IOException</code>) if this file
* already exists.
*
* <p>This {@link FileOperation#CREATE_DIRECTORY file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @throws IOException if the directory could not be created, either because this file already exists or for any
* other reason.
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract void mkdir() throws IOException, UnsupportedFileOperationException;
/**
* Returns an <code>InputStream</code> to read the contents of this file.
* Throws an <code>IOException</code> in any of the following cases:
* <ul>
* <li>this file does not exist</li>
* <li>this file is a directory</li>
* <li>this file cannot be read</li>
* <li>an I/O error occurs</li>
* </ul>
* This method may never return <code>null</code>.
*
* <p>This {@link FileOperation#READ_FILE file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @return an <code>InputStream</code> to read the contents of this file
* @throws IOException in any of the cases listed above
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract InputStream getInputStream() throws IOException, UnsupportedFileOperationException;
/**
* Returns an <code>OuputStream</code> to write the contents of this file, overwriting the existing contents, if any.
* This file will be created as a zero-byte file if it does not yet exist.
* <p>
* This method may throw an <code>IOException</code> in any of the following cases, but may never return
* <code>null</code>:
* <ul>
* <li>this file is a directory</li>
* <li>this file cannot be written</li>
* <li>an I/O error occurs</li>
* </ul>
* </p>
*
* <p>This {@link FileOperation#WRITE_FILE file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @return an <code>OuputStream</code> to write the contents of this file
* @throws IOException in any of the cases listed above
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract OutputStream getOutputStream() throws IOException, UnsupportedFileOperationException;
/**
* Returns an <code>OuputStream</code> to write the contents of this file, appending the existing contents, if any.
* This file will be created as a zero-byte file if it does not yet exist.
* <p>
* This method may throw an <code>IOException</code> in any of the following cases, but may never return
* <code>null</code>:
* <ul>
* <li>this file is a directory</li>
* <li>this file cannot be written</li>
* <li>an I/O error occurs</li>
* </ul>
* </p>
*
* <p>This {@link FileOperation#APPEND_FILE file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @return an <code>OuputStream</code> to write the contents of this file
* @throws IOException in any of the cases listed above
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract OutputStream getAppendOutputStream() throws IOException, UnsupportedFileOperationException;
/**
* Returns a {@link RandomAccessInputStream} to read the contents of this file with random access.
* Throws an <code>IOException</code> in any of the following cases:
* <ul>
* <li>this file does not exist</li>
* <li>this file is a directory</li>
* <li>this file cannot be read</li>
* <li>an I/O error occurs</li>
* </ul>
* This method may never return <code>null</code>.
*
* <p>This {@link FileOperation#RANDOM_READ_FILE file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @return a <code>RandomAccessInputStream</code> to read the contents of this file with random access
* @throws IOException in any of the cases listed above
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract RandomAccessInputStream getRandomAccessInputStream() throws IOException, UnsupportedFileOperationException;
/**
* Returns a {@link RandomAccessOutputStream} to write the contents of this file with random access.
* This file will be created as a zero-byte file if it does not yet exist.
* Throws an <code>IOException</code> in any of the following cases:
* <ul>
* <li>this file is a directory</li>
* <li>this file cannot be written</li>
* <li>an I/O error occurs</li>
* </ul>
* This method may never return <code>null</code>.
*
* <p>This {@link FileOperation#RANDOM_WRITE_FILE file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @return a <code>RandomAccessOutputStream</code> to write the contents of this file with random access
* @throws IOException in any of the cases listed above
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract RandomAccessOutputStream getRandomAccessOutputStream() throws IOException, UnsupportedFileOperationException;
/**
* Deletes this file and this file only (does not recurse on folders).
* Throws an <code>IOException</code> in any of the following cases:
* <ul>
* <li>if this file does not exist</li>
* <li>if this file is a non-empty directory</li>
* <li>if this file could not be deleted, for example because of insufficient permissions</li>
* <li>if an I/O error occurred</li>
* </ul>
*
* <p>This {@link FileOperation#DELETE file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @throws IOException if this file does not exist or could not be deleted
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract void delete() throws IOException, UnsupportedFileOperationException;
/**
* Renames this file to a specified destination file, overwriting the destination if it exists. If this file is a
* directory, any file or directory it contains will also be moved.
* After normal completion, this file will not exist anymore: {@link #exists()} will return <code>false</code>.
*
* <p>This method throws an {@link IOException} if the operation failed, for any of the following reasons:
* <ul>
* <li>this file and the destination file are the same</li>
* <li>this file is a directory and a parent of the destination file (the operation would otherwise loop indefinitely)</li>
* <li>this file cannot be read</li>
* <li>this file cannot be written</li>
* <li>the destination file can not be written</li>
* <li>an I/O error occurred</li>
* </ul>
* </p>
*
* <p>This {@link FileOperation#RENAME file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @param destFile file to rename this file to
* @throws IOException in any of the error cases listed above
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract void renameTo(AbstractFile destFile) throws IOException, UnsupportedFileOperationException;
/**
* Remotely copies this file to a specified destination file, overwriting the destination if it exists.
* If this file is a directory, any file or directory it contains will also be copied.
*
* <p>This method differs from {@link #copyTo(AbstractFile)} in that it performs a server-to-server copy of the
* file(s), without having the file's contents go through to the local process. This operation should only be
* implemented if it offers a performance advantage over a regular client-driven copy like
* {@link #copyTo(AbstractFile)}, or if {@link FileOperation#WRITE_FILE} is not supported (output streams cannot be
* retrieved) and thus a regular copy cannot succeed.</p>.
*
* <p>This method throws an {@link IOException} if the operation failed, for any of the following reasons:
* <ul>
* <li>this file and the destination file are the same</li>
* <li>this file is a directory and a parent of the destination file (the operation would otherwise loop indefinitely)</li>
* <li>this file (or one if its children) cannot be read</li>
* <li>the destination file (or one of its children) can not be written</li>
* <li>an I/O error occurred</li>
* </ul>
* </p>
*
* <p>The behavior in the case of an error occurring in the midst of the transfer is unspecified: files that have
* been copied (even partially) may or may not be left in the destination.<p/>
*
* @param destFile the destination file to copy this file to
* @throws IOException in any of the error cases listed above
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract void copyRemotelyTo(AbstractFile destFile) throws IOException, UnsupportedFileOperationException;
/**
* Returns the free space (in bytes) on the disk/volume where this file is, <code>-1</code> if this information is
* not available.
*
* <p>This {@link FileOperation#GET_FREE_SPACE file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @return the free space (in bytes) on the disk/volume where this file is, <code>-1</code> if this information is
* not available.
* @throws IOException if an I/O error occurred
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract long getFreeSpace() throws IOException, UnsupportedFileOperationException;
/**
* Returns the total space (in bytes) of the disk/volume where this file is.
*
* <p>This {@link FileOperation#GET_TOTAL_SPACE file operation} may or may not be supported by the underlying filesystem
* -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't
* supported, a {@link UnsupportedFileOperation} will be thrown when this method is called.</p>
*
* @return the total space (in bytes) of the disk/volume where this file is
* @throws IOException if an I/O error occurred
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
public abstract long getTotalSpace() throws IOException, UnsupportedFileOperationException;
/**
* Returns the file Object of the underlying API providing access to the filesystem. The returned Object may expose
* filesystem-specific functionalities that are not available in <code>AbstractFile</code>. Note however that the
* returned Object type may change over time, if the underlying API used to provide access to the filesystem
* changes, so this method should be used only as a last resort.
*
* <p>If the implemented filesystem has no such Object, <code>null</code> is returned.</p>
*
* @return the file Object of the underlying API providing access to the filesystem, <code>null</code> if there
* is none
*/
public abstract Object getUnderlyingFileObject();
/**
* Returns the extension that was specified by the user for the file using "open as" operation.
*
* @return an extension specified by the user, null if unspecified
*/
public String getCustomExtension() {
return customExtension;
}
/**
* Sets a custom extension for the file that should be used when opening the file rather than the one indicated by its name.
*
* @param customExtension The extension that should be used when opening the file
*/
public void setCustomExtension(String customExtension) {
this.customExtension = customExtension;
}
}