integrate our own copy of mock ftp server for JPMS
This commit is contained in:
parent
651417ce00
commit
0d0fbe167b
246 changed files with 18589 additions and 359 deletions
|
@ -10,7 +10,10 @@ import java.nio.channels.ReadableByteChannel;
|
||||||
import java.nio.channels.WritableByteChannel;
|
import java.nio.channels.WritableByteChannel;
|
||||||
import java.nio.file.CopyOption;
|
import java.nio.file.CopyOption;
|
||||||
import java.nio.file.DirectoryStream;
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.FileVisitOption;
|
import java.nio.file.FileVisitOption;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
import java.nio.file.OpenOption;
|
import java.nio.file.OpenOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
@ -19,9 +22,10 @@ import java.nio.file.StandardOpenOption;
|
||||||
import java.nio.file.attribute.FileAttribute;
|
import java.nio.file.attribute.FileAttribute;
|
||||||
import java.nio.file.attribute.FileTime;
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.nio.file.attribute.PosixFileAttributeView;
|
import java.nio.file.attribute.PosixFileAttributeView;
|
||||||
|
import java.nio.file.attribute.PosixFileAttributes;
|
||||||
import java.nio.file.attribute.PosixFilePermission;
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
import java.nio.file.attribute.PosixFilePermissions;
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
import java.nio.file.attribute.UserPrincipal;
|
import java.time.Instant;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -31,9 +35,7 @@ import java.util.stream.Stream;
|
||||||
|
|
||||||
public class DefaultFileService implements FileService {
|
public class DefaultFileService implements FileService {
|
||||||
|
|
||||||
private static final int READ_BUFFER_SIZE = 128 * 1024;
|
private static final int BUFFER_SIZE = 128 * 1024;
|
||||||
|
|
||||||
private static final int WRITE_BUFFER_SIZE = 128 * 1024;
|
|
||||||
|
|
||||||
private static final Set<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
|
private static final Set<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
|
||||||
PosixFilePermissions.fromString("rwxr-xr-x");
|
PosixFilePermissions.fromString("rwxr-xr-x");
|
||||||
|
@ -110,23 +112,61 @@ public class DefaultFileService implements FileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setOwner(String path, UserPrincipal userPrincipal) throws IOException {
|
public void setPosixFileAttributes(String path,
|
||||||
Files.setOwner(Paths.get(path), userPrincipal);
|
String owner,
|
||||||
|
String group,
|
||||||
|
Instant lastModifiedTime,
|
||||||
|
Instant lastAccessTime,
|
||||||
|
Instant createTime) throws IOException {
|
||||||
|
FileSystem fileSystem = FileSystems.getDefault();
|
||||||
|
PosixFileAttributeView view = Files.getFileAttributeView(fileSystem.getPath(path),
|
||||||
|
PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||||
|
view.setOwner(fileSystem.getUserPrincipalLookupService().lookupPrincipalByName(owner));
|
||||||
|
view.setGroup(fileSystem.getUserPrincipalLookupService().lookupPrincipalByGroupName(group));
|
||||||
|
view.setTimes(FileTime.from(lastModifiedTime),
|
||||||
|
FileTime.from(lastAccessTime),
|
||||||
|
FileTime.from(createTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserPrincipal getOwner(String path) throws IOException {
|
public PosixFileAttributes getPosixFileAttributes(String path) throws IOException {
|
||||||
return Files.getOwner(Paths.get(path));
|
return Files.getFileAttributeView(FileSystems.getDefault().getPath(path),
|
||||||
|
PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setLastModifiedTime(String path, FileTime fileTime) throws IOException {
|
public void setOwner(String path, String owner) throws IOException {
|
||||||
Files.setLastModifiedTime(Paths.get(path), fileTime);
|
Files.setOwner(Paths.get(path),
|
||||||
|
FileSystems.getDefault().getUserPrincipalLookupService().lookupPrincipalByName(owner));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FileTime getLastModifiedTime(String path) throws IOException {
|
public String getOwner(String path) throws IOException {
|
||||||
return Files.getLastModifiedTime(Paths.get(path));
|
return Files.getOwner(Paths.get(path)).getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setGroup(String path, String group) throws IOException {
|
||||||
|
FileSystem fileSystem = FileSystems.getDefault();
|
||||||
|
PosixFileAttributeView view = Files.getFileAttributeView(fileSystem.getPath(path),
|
||||||
|
PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||||
|
view.setGroup(fileSystem.getUserPrincipalLookupService().lookupPrincipalByGroupName(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGroup(String path) throws IOException {
|
||||||
|
return Files.getFileAttributeView(FileSystems.getDefault().getPath(path),
|
||||||
|
PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes().group().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLastModifiedTime(String path, Instant lastModified) throws IOException {
|
||||||
|
Files.setLastModifiedTime(Paths.get(path), FileTime.from(lastModified));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant getLastModifiedTime(String path) throws IOException {
|
||||||
|
return Files.getLastModifiedTime(Paths.get(path)).toInstant();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -145,43 +185,87 @@ public class DefaultFileService implements FileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void upload(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
public void upload(Path source,
|
||||||
|
String target,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void upload(Path source, String target, CopyOption... copyOptions) throws IOException {
|
public void upload(Path source,
|
||||||
upload(source, Paths.get(target), DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
String target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(Files.newByteChannel(source), Paths.get(target), dirPerms, filePerms, copyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void upload(InputStream source, Path target, CopyOption... copyOptions) throws IOException {
|
public void upload(Path source,
|
||||||
|
Path target,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void upload(InputStream source, String target, CopyOption... copyOptions) throws IOException {
|
public void upload(Path source,
|
||||||
upload(source, Paths.get(target), DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
Path target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(Files.newByteChannel(source), target, dirPerms, filePerms, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source,
|
||||||
|
String target,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source,
|
||||||
|
String target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(Channels.newChannel(source), Paths.get(target), dirPerms, filePerms, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source,
|
||||||
|
Path target,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source,
|
||||||
|
Path target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(Channels.newChannel(source), target, dirPerms, filePerms, copyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void download(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
public void download(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
download(source, target, READ_BUFFER_SIZE, copyOptions);
|
download(source, target, copyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void download(String source, Path target, CopyOption... copyOptions) throws IOException {
|
public void download(String source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
download(Paths.get(source), target, READ_BUFFER_SIZE, copyOptions);
|
download(Paths.get(source), target, copyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void download(Path source, OutputStream target) throws IOException {
|
public void download(Path source, OutputStream target) throws IOException {
|
||||||
download(source, target, READ_BUFFER_SIZE);
|
download(source, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void download(String source, OutputStream target) throws IOException {
|
public void download(String source, OutputStream target) throws IOException {
|
||||||
download(Paths.get(source), target, READ_BUFFER_SIZE);
|
download(Paths.get(source), target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -224,28 +308,13 @@ public class DefaultFileService implements FileService {
|
||||||
return Files.walk(Paths.get(path), maxdepth, options);
|
return Files.walk(Paths.get(path), maxdepth, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upload(Path source, Path target,
|
|
||||||
Set<PosixFilePermission> dirPerms,
|
|
||||||
Set<PosixFilePermission> filePerms,
|
|
||||||
CopyOption... copyOptions) throws IOException {
|
|
||||||
upload(Files.newByteChannel(source), target, WRITE_BUFFER_SIZE, dirPerms, filePerms, copyOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void upload(InputStream source, Path target,
|
|
||||||
Set<PosixFilePermission> dirPerms,
|
|
||||||
Set<PosixFilePermission> filePerms,
|
|
||||||
CopyOption... copyOptions) throws IOException {
|
|
||||||
upload(Channels.newChannel(source), target, WRITE_BUFFER_SIZE, dirPerms, filePerms, copyOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void upload(ReadableByteChannel source,
|
private void upload(ReadableByteChannel source,
|
||||||
Path target,
|
Path target,
|
||||||
int bufferSize,
|
|
||||||
Set<PosixFilePermission> dirPerms,
|
Set<PosixFilePermission> dirPerms,
|
||||||
Set<PosixFilePermission> filePerms,
|
Set<PosixFilePermission> filePerms,
|
||||||
CopyOption... copyOptions) throws IOException {
|
CopyOption... copyOptions) throws IOException {
|
||||||
prepareForWrite(target, dirPerms, filePerms);
|
prepareForWrite(target, dirPerms, filePerms);
|
||||||
transfer(source, Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
|
transfer(source, Files.newByteChannel(target, prepareWriteOptions(copyOptions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void download(Path source,
|
private void download(Path source,
|
||||||
|
@ -257,8 +326,7 @@ public class DefaultFileService implements FileService {
|
||||||
private void download(Path source,
|
private void download(Path source,
|
||||||
WritableByteChannel writableByteChannel,
|
WritableByteChannel writableByteChannel,
|
||||||
int bufferSize) throws IOException {
|
int bufferSize) throws IOException {
|
||||||
transfer(Files.newByteChannel(source, prepareReadOptions()), writableByteChannel,
|
transfer(Files.newByteChannel(source, prepareReadOptions()), writableByteChannel);
|
||||||
bufferSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void download(Path source,
|
private void download(Path source,
|
||||||
|
@ -267,7 +335,7 @@ public class DefaultFileService implements FileService {
|
||||||
CopyOption... copyOptions) throws IOException {
|
CopyOption... copyOptions) throws IOException {
|
||||||
prepareForWrite(target);
|
prepareForWrite(target);
|
||||||
transfer(Files.newByteChannel(source, prepareReadOptions(copyOptions)),
|
transfer(Files.newByteChannel(source, prepareReadOptions(copyOptions)),
|
||||||
Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
|
Files.newByteChannel(target, prepareWriteOptions(copyOptions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareForWrite(Path path) throws IOException {
|
private void prepareForWrite(Path path) throws IOException {
|
||||||
|
@ -328,9 +396,8 @@ public class DefaultFileService implements FileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void transfer(ReadableByteChannel readableByteChannel,
|
private void transfer(ReadableByteChannel readableByteChannel,
|
||||||
WritableByteChannel writableByteChannel,
|
WritableByteChannel writableByteChannel) throws IOException {
|
||||||
int bufferSize) throws IOException {
|
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
|
|
||||||
int read;
|
int read;
|
||||||
while ((read = readableByteChannel.read(buffer)) > 0) {
|
while ((read = readableByteChannel.read(buffer)) > 0) {
|
||||||
buffer.flip();
|
buffer.flip();
|
||||||
|
|
|
@ -10,9 +10,9 @@ import java.nio.file.FileVisitOption;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.attribute.FileAttribute;
|
import java.nio.file.attribute.FileAttribute;
|
||||||
import java.nio.file.attribute.FileTime;
|
import java.nio.file.attribute.PosixFileAttributes;
|
||||||
import java.nio.file.attribute.PosixFilePermission;
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
import java.nio.file.attribute.UserPrincipal;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -72,13 +72,26 @@ public interface FileService {
|
||||||
|
|
||||||
Set<PosixFilePermission> getPermissions(String path) throws IOException;
|
Set<PosixFilePermission> getPermissions(String path) throws IOException;
|
||||||
|
|
||||||
void setOwner(String path, UserPrincipal userPrincipal) throws IOException;
|
void setPosixFileAttributes(String path,
|
||||||
|
String owner,
|
||||||
|
String group,
|
||||||
|
Instant lastModifiedTime,
|
||||||
|
Instant lastAccessTime,
|
||||||
|
Instant createTime) throws IOException;
|
||||||
|
|
||||||
UserPrincipal getOwner(String path) throws IOException;
|
PosixFileAttributes getPosixFileAttributes(String path) throws IOException;
|
||||||
|
|
||||||
void setLastModifiedTime(String path, FileTime fileTime) throws IOException;
|
void setOwner(String path, String owner) throws IOException;
|
||||||
|
|
||||||
FileTime getLastModifiedTime(String path) throws IOException;
|
String getOwner(String path) throws IOException;
|
||||||
|
|
||||||
|
void setGroup(String path, String group) throws IOException;
|
||||||
|
|
||||||
|
String getGroup(String path) throws IOException;
|
||||||
|
|
||||||
|
void setLastModifiedTime(String path, Instant lastModified) throws IOException;
|
||||||
|
|
||||||
|
Instant getLastModifiedTime(String path) throws IOException;
|
||||||
|
|
||||||
void createFile(String path, FileAttribute<?>... attributes) throws IOException;
|
void createFile(String path, FileAttribute<?>... attributes) throws IOException;
|
||||||
|
|
||||||
|
@ -86,21 +99,59 @@ public interface FileService {
|
||||||
|
|
||||||
void createDirectories(String path, FileAttribute<?>... attributes) throws IOException;
|
void createDirectories(String path, FileAttribute<?>... attributes) throws IOException;
|
||||||
|
|
||||||
void upload(Path source, Path target, CopyOption... copyOptions) throws IOException;
|
void upload(Path source,
|
||||||
|
String target,
|
||||||
|
CopyOption... copyOptions) throws Exception;
|
||||||
|
|
||||||
void upload(Path source, String target, CopyOption... copyOptions) throws Exception;
|
void upload(Path source,
|
||||||
|
String target,
|
||||||
|
Set<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> filePermissions,
|
||||||
|
CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
void upload(InputStream source, Path target, CopyOption... copyOptions) throws IOException;
|
void upload(Path source,
|
||||||
|
Path target,
|
||||||
|
CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
void upload(InputStream source, String target, CopyOption... copyOptions) throws IOException;
|
void upload(Path source,
|
||||||
|
Path target,
|
||||||
|
Set<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> filePermissions,
|
||||||
|
CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
void download(Path source, Path target, CopyOption... copyOptions) throws IOException;
|
void upload(InputStream source,
|
||||||
|
String target,
|
||||||
|
CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
void download(String source, Path target, CopyOption... copyOptions) throws IOException;
|
void upload(InputStream source,
|
||||||
|
String target,
|
||||||
|
Set<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> filePermissions,
|
||||||
|
CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
void download(Path source, OutputStream target) throws IOException;
|
void upload(InputStream source,
|
||||||
|
Path target,
|
||||||
|
CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
void download(String source, OutputStream target) throws IOException;
|
void upload(InputStream source,
|
||||||
|
Path target,
|
||||||
|
Set<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> filePermissions,
|
||||||
|
CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
void download(String source,
|
||||||
|
Path target,
|
||||||
|
CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
void download(Path source,
|
||||||
|
Path target,
|
||||||
|
CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
void download(String source,
|
||||||
|
OutputStream target) throws IOException;
|
||||||
|
|
||||||
|
void download(Path source,
|
||||||
|
OutputStream target) throws IOException;
|
||||||
|
|
||||||
void rename(String source, String target, CopyOption... copyOptions) throws IOException;
|
void rename(String source, String target, CopyOption... copyOptions) throws IOException;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(':files-api')
|
api project(':files-api')
|
||||||
api project(':files-ftp')
|
api project(':files-ftp')
|
||||||
testImplementation testLibs.mockftpserver
|
|
||||||
testImplementation testLibs.junit.jupiter.params
|
testImplementation testLibs.junit.jupiter.params
|
||||||
testImplementation testLibs.mockito.core
|
testImplementation testLibs.mockito.core
|
||||||
testImplementation testLibs.mockito.junit.jupiter
|
testImplementation testLibs.mockito.junit.jupiter
|
||||||
testImplementation testLibs.slf4j
|
testImplementation testLibs.slf4j
|
||||||
|
testImplementation project(':files-ftp-mock')
|
||||||
}
|
}
|
||||||
|
|
||||||
def moduleName = 'org.xbib.io.ftp.test'
|
def moduleName = 'org.xbib.io.ftp.test'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import java.nio.file.spi.FileSystemProvider;
|
import java.nio.file.spi.FileSystemProvider;
|
||||||
import org.xbib.files.FileServiceProvider;
|
import org.xbib.files.FileServiceProvider;
|
||||||
import org.xbib.io.ftp.fs.FTPFileSystemProvider;
|
import org.xbib.io.ftp.fs.FTPFileSystemProvider;
|
||||||
import org.xbib.io.ftp.fs.spi.FTPFilesProvider;
|
import org.xbib.io.ftp.fs.spi.FTPFileServiceProvider;
|
||||||
|
|
||||||
module org.xbib.files.ftp.fs {
|
module org.xbib.files.ftp.fs {
|
||||||
requires org.xbib.files;
|
requires org.xbib.files;
|
||||||
|
@ -9,5 +9,5 @@ module org.xbib.files.ftp.fs {
|
||||||
exports org.xbib.io.ftp.fs;
|
exports org.xbib.io.ftp.fs;
|
||||||
exports org.xbib.io.ftp.fs.spi;
|
exports org.xbib.io.ftp.fs.spi;
|
||||||
provides FileSystemProvider with FTPFileSystemProvider;
|
provides FileSystemProvider with FTPFileSystemProvider;
|
||||||
provides FileServiceProvider with FTPFilesProvider;
|
provides FileServiceProvider with FTPFileServiceProvider;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
package org.xbib.io.ftp.fs.spi;
|
package org.xbib.io.ftp.fs.spi;
|
||||||
|
|
||||||
import org.xbib.files.FileService;
|
|
||||||
import org.xbib.files.FileWalker;
|
|
||||||
import org.xbib.files.WrappedDirectoryStream;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
@ -16,6 +12,7 @@ import java.nio.file.CopyOption;
|
||||||
import java.nio.file.DirectoryStream;
|
import java.nio.file.DirectoryStream;
|
||||||
import java.nio.file.FileVisitOption;
|
import java.nio.file.FileVisitOption;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
import java.nio.file.OpenOption;
|
import java.nio.file.OpenOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
|
@ -23,19 +20,21 @@ import java.nio.file.StandardOpenOption;
|
||||||
import java.nio.file.attribute.FileAttribute;
|
import java.nio.file.attribute.FileAttribute;
|
||||||
import java.nio.file.attribute.FileTime;
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.nio.file.attribute.PosixFileAttributeView;
|
import java.nio.file.attribute.PosixFileAttributeView;
|
||||||
|
import java.nio.file.attribute.PosixFileAttributes;
|
||||||
import java.nio.file.attribute.PosixFilePermission;
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
import java.nio.file.attribute.PosixFilePermissions;
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
import java.nio.file.attribute.UserPrincipal;
|
import java.time.Instant;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
import org.xbib.files.FileService;
|
||||||
|
import org.xbib.files.FileWalker;
|
||||||
|
import org.xbib.files.WrappedDirectoryStream;
|
||||||
|
|
||||||
public class FTPFiles implements FileService {
|
public class FTPFileService implements FileService {
|
||||||
|
|
||||||
private static final int READ_BUFFER_SIZE = 128 * 1024;
|
private static final int BUFFER_SIZE = 128 * 1024;
|
||||||
|
|
||||||
private static final int WRITE_BUFFER_SIZE = 128 * 1024;
|
|
||||||
|
|
||||||
private static final Set<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
|
private static final Set<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
|
||||||
PosixFilePermissions.fromString("rwxr-xr-x");
|
PosixFilePermissions.fromString("rwxr-xr-x");
|
||||||
|
@ -47,7 +46,7 @@ public class FTPFiles implements FileService {
|
||||||
|
|
||||||
private final Map<String, ?> env;
|
private final Map<String, ?> env;
|
||||||
|
|
||||||
public FTPFiles(URI uri, Map<String, ?> env) {
|
public FTPFileService(URI uri, Map<String, ?> env) {
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.env = env;
|
this.env = env;
|
||||||
}
|
}
|
||||||
|
@ -133,30 +132,176 @@ public class FTPFiles implements FileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setLastModifiedTime(String path, FileTime fileTime) throws IOException {
|
public void setLastModifiedTime(String path, Instant lastModified) throws IOException {
|
||||||
performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime));
|
performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), FileTime.from(lastModified)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FileTime getLastModifiedTime(String path) throws IOException{
|
public Instant getLastModifiedTime(String path) throws IOException{
|
||||||
return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)));
|
return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)).toInstant());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setOwner(String path, UserPrincipal userPrincipal) throws IOException {
|
public void setPosixFileAttributes(String path,
|
||||||
performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal));
|
String owner,
|
||||||
|
String group,
|
||||||
|
Instant lastModifiedTime,
|
||||||
|
Instant lastAccessTime,
|
||||||
|
Instant createTime) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
PosixFileAttributeView view = Files.getFileAttributeView(ctx.fileSystem.getPath(path),
|
||||||
|
PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||||
|
view.setOwner(ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByName(owner));
|
||||||
|
view.setGroup(ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByGroupName(group));
|
||||||
|
view.setTimes(FileTime.from(lastModifiedTime),
|
||||||
|
FileTime.from(lastAccessTime),
|
||||||
|
FileTime.from(createTime));
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserPrincipal getOwner(String path) throws IOException {
|
public PosixFileAttributes getPosixFileAttributes(String path) throws IOException {
|
||||||
return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path)));
|
return performWithContext(ctx -> Files.getFileAttributeView(ctx.fileSystem.getPath(path),
|
||||||
|
PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void upload(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
public void setOwner(String path, String owner) throws IOException {
|
||||||
|
performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path),
|
||||||
|
ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByName(owner)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOwner(String path) throws IOException {
|
||||||
|
return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path)).getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setGroup(String path, String group) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
PosixFileAttributeView view = Files.getFileAttributeView(ctx.fileSystem.getPath(path),
|
||||||
|
PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||||
|
view.setGroup(ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByGroupName(group));
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGroup(String path) throws IOException {
|
||||||
|
return performWithContext(ctx -> Files.getFileAttributeView(ctx.fileSystem.getPath(path),
|
||||||
|
PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes().group().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(Path source,
|
||||||
|
Path target,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(Path source,
|
||||||
|
Path target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Files.newByteChannel(source), target, dirPerms, filePerms, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(Path source,
|
||||||
|
String target,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(Path source, String target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target),
|
||||||
|
dirPerms, filePerms, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source,
|
||||||
|
Path target,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source,
|
||||||
|
Path target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Channels.newChannel(source), target, dirPerms, filePerms, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source,
|
||||||
|
String target,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(InputStream source, String target,
|
||||||
|
Set<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> filePerms,
|
||||||
|
CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target),
|
||||||
|
dirPerms, filePerms, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
download(ctx, source, target, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(String source, Path target, CopyOption... copyOptions) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
download(ctx, ctx.fileSystem.getPath(source), target, copyOptions);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(Path source, OutputStream target) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
download(ctx, source, target);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(String source, OutputStream target) throws IOException {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
Files.copy(ctx.fileSystem.getPath(source), target);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DirectoryStream<Path> stream(String path, String glob) throws IOException {
|
public DirectoryStream<Path> stream(String path, String glob) throws IOException {
|
||||||
FTPContext ctx = new FTPContext(uri, env);
|
FTPContext ctx = new FTPContext(uri, env);
|
||||||
|
@ -187,90 +332,7 @@ public class FTPFiles implements FileService {
|
||||||
return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), maxdepth, options);
|
return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), maxdepth, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void upload(Path source, Path target,
|
@Override
|
||||||
Set<PosixFilePermission> dirPerms,
|
|
||||||
Set<PosixFilePermission> filePerms,
|
|
||||||
CopyOption... copyOptions) throws IOException {
|
|
||||||
performWithContext(ctx -> {
|
|
||||||
upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE,
|
|
||||||
dirPerms, filePerms, copyOptions);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void upload(Path source, String target, CopyOption... copyOptions) throws IOException {
|
|
||||||
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void upload(Path source, String target,
|
|
||||||
Set<PosixFilePermission> dirPerms,
|
|
||||||
Set<PosixFilePermission> filePerms,
|
|
||||||
CopyOption... copyOptions) throws IOException {
|
|
||||||
performWithContext(ctx -> {
|
|
||||||
upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
|
|
||||||
dirPerms, filePerms, copyOptions);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void upload(InputStream source, Path target, CopyOption... copyOptions) throws IOException {
|
|
||||||
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void upload(InputStream source, Path target,
|
|
||||||
Set<PosixFilePermission> dirPerms,
|
|
||||||
Set<PosixFilePermission> filePerms,
|
|
||||||
CopyOption... copyOptions) throws IOException {
|
|
||||||
performWithContext(ctx -> {
|
|
||||||
upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE,
|
|
||||||
dirPerms, filePerms, copyOptions);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void upload(InputStream source, String target, CopyOption... copyOptions) throws IOException {
|
|
||||||
upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void upload(InputStream source, String target,
|
|
||||||
Set<PosixFilePermission> dirPerms,
|
|
||||||
Set<PosixFilePermission> filePerms,
|
|
||||||
CopyOption... copyOptions) throws IOException {
|
|
||||||
performWithContext(ctx -> {
|
|
||||||
upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE,
|
|
||||||
dirPerms, filePerms, copyOptions);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void download(Path source, Path target, CopyOption... copyOptions) throws IOException {
|
|
||||||
performWithContext(ctx -> {
|
|
||||||
download(ctx, source, target, READ_BUFFER_SIZE, copyOptions);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void download(String source, Path target, CopyOption... copyOptions) throws IOException {
|
|
||||||
performWithContext(ctx -> {
|
|
||||||
download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void download(Path source, OutputStream target) throws IOException {
|
|
||||||
performWithContext(ctx -> {
|
|
||||||
download(ctx, source, target, READ_BUFFER_SIZE);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void download(String source, OutputStream target) throws IOException {
|
|
||||||
performWithContext(ctx -> {
|
|
||||||
Files.copy(ctx.fileSystem.getPath(source), target);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void copy(String source, String target, CopyOption... copyOptions) throws IOException {
|
public void copy(String source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
performWithContext(ctx -> {
|
performWithContext(ctx -> {
|
||||||
Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
|
Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
|
||||||
|
@ -278,6 +340,7 @@ public class FTPFiles implements FileService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void rename(String source, String target, CopyOption... copyOptions) throws IOException {
|
public void rename(String source, String target, CopyOption... copyOptions) throws IOException {
|
||||||
performWithContext(ctx -> {
|
performWithContext(ctx -> {
|
||||||
Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
|
Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
|
||||||
|
@ -285,6 +348,7 @@ public class FTPFiles implements FileService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void remove(String source) throws IOException {
|
public void remove(String source) throws IOException {
|
||||||
performWithContext(ctx -> {
|
performWithContext(ctx -> {
|
||||||
Files.deleteIfExists(ctx.fileSystem.getPath(source));
|
Files.deleteIfExists(ctx.fileSystem.getPath(source));
|
||||||
|
@ -295,37 +359,32 @@ public class FTPFiles implements FileService {
|
||||||
private void upload(FTPContext ctx,
|
private void upload(FTPContext ctx,
|
||||||
ReadableByteChannel source,
|
ReadableByteChannel source,
|
||||||
Path target,
|
Path target,
|
||||||
int bufferSize,
|
|
||||||
Set<PosixFilePermission> dirPerms,
|
Set<PosixFilePermission> dirPerms,
|
||||||
Set<PosixFilePermission> filePerms,
|
Set<PosixFilePermission> filePerms,
|
||||||
CopyOption... copyOptions) throws IOException {
|
CopyOption... copyOptions) throws IOException {
|
||||||
prepareForWrite(target, dirPerms, filePerms);
|
prepareForWrite(target, dirPerms, filePerms);
|
||||||
transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
|
transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void download(FTPContext ctx,
|
private void download(FTPContext ctx,
|
||||||
Path source,
|
Path source,
|
||||||
OutputStream outputStream,
|
OutputStream outputStream) throws IOException {
|
||||||
int bufferSize) throws IOException {
|
download(ctx, source, Channels.newChannel(outputStream));
|
||||||
download(ctx, source, Channels.newChannel(outputStream), bufferSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void download(FTPContext ctx,
|
private void download(FTPContext ctx,
|
||||||
Path source,
|
Path source,
|
||||||
WritableByteChannel writableByteChannel,
|
WritableByteChannel writableByteChannel) throws IOException {
|
||||||
int bufferSize) throws IOException {
|
transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel);
|
||||||
transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel,
|
|
||||||
bufferSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void download(FTPContext ctx,
|
private void download(FTPContext ctx,
|
||||||
Path source,
|
Path source,
|
||||||
Path target,
|
Path target,
|
||||||
int bufferSize,
|
|
||||||
CopyOption... copyOptions) throws IOException {
|
CopyOption... copyOptions) throws IOException {
|
||||||
prepareForWrite(target);
|
prepareForWrite(target);
|
||||||
transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)),
|
transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)),
|
||||||
Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
|
Files.newByteChannel(target, prepareWriteOptions(copyOptions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareForWrite(Path path) throws IOException {
|
private void prepareForWrite(Path path) throws IOException {
|
||||||
|
@ -386,9 +445,8 @@ public class FTPFiles implements FileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void transfer(ReadableByteChannel readableByteChannel,
|
private void transfer(ReadableByteChannel readableByteChannel,
|
||||||
WritableByteChannel writableByteChannel,
|
WritableByteChannel writableByteChannel) throws IOException {
|
||||||
int bufferSize) throws IOException {
|
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
|
|
||||||
int read;
|
int read;
|
||||||
while ((read = readableByteChannel.read(buffer)) > 0) {
|
while ((read = readableByteChannel.read(buffer)) > 0) {
|
||||||
buffer.flip();
|
buffer.flip();
|
|
@ -6,9 +6,10 @@ import org.xbib.files.FileServiceProvider;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class FTPFilesProvider implements FileServiceProvider {
|
public class FTPFileServiceProvider implements FileServiceProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FileService provide(URI uri, Map<String, ?> env) {
|
public FileService provide(URI uri, Map<String, ?> env) {
|
||||||
return uri.isAbsolute() && uri.getScheme().equals("ftp") ? new FTPFiles(uri, env) : null;
|
return uri.isAbsolute() && uri.getScheme().equals("ftp") ? new FTPFileService(uri, env) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1 +1 @@
|
||||||
org.xbib.io.ftp.fs.spi.FTPFilesProvider
|
org.xbib.io.ftp.fs.spi.FTPFileServiceProvider
|
|
@ -3,10 +3,11 @@ module org.xbib.files.ftp.fs.test {
|
||||||
requires org.junit.jupiter.api;
|
requires org.junit.jupiter.api;
|
||||||
requires org.junit.jupiter.params;
|
requires org.junit.jupiter.params;
|
||||||
requires org.mockito;
|
requires org.mockito;
|
||||||
requires MockFtpServer;
|
|
||||||
requires org.slf4j;
|
requires org.slf4j;
|
||||||
requires org.xbib.files.ftp;
|
requires org.xbib.files.ftp;
|
||||||
requires org.xbib.files.ftp.fs;
|
requires org.xbib.files.ftp.fs;
|
||||||
|
requires org.xbib.files.ftp.mock;
|
||||||
|
requires java.logging;
|
||||||
exports org.xbib.io.ftp.fs.test;
|
exports org.xbib.io.ftp.fs.test;
|
||||||
exports org.xbib.io.ftp.fs.test.server;
|
exports org.xbib.io.ftp.fs.test.server;
|
||||||
opens org.xbib.io.ftp.fs.test to org.junit.platform.commons;
|
opens org.xbib.io.ftp.fs.test to org.junit.platform.commons;
|
||||||
|
|
|
@ -4,14 +4,14 @@ import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.mockftpserver.fake.FakeFtpServer;
|
|
||||||
import org.mockftpserver.fake.UserAccount;
|
|
||||||
import org.mockftpserver.fake.filesystem.DirectoryEntry;
|
|
||||||
import org.mockftpserver.fake.filesystem.FileEntry;
|
|
||||||
import org.mockftpserver.fake.filesystem.FileSystem;
|
|
||||||
import org.mockftpserver.fake.filesystem.FileSystemEntry;
|
|
||||||
import org.mockftpserver.fake.filesystem.UnixFakeFileSystem;
|
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
import org.xbib.files.ftp.mock.fake.FakeFtpServer;
|
||||||
|
import org.xbib.files.ftp.mock.fake.UserAccount;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.DirectoryEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystem;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.UnixFakeFileSystem;
|
||||||
import org.xbib.io.ftp.fs.DefaultFileSystemExceptionFactory;
|
import org.xbib.io.ftp.fs.DefaultFileSystemExceptionFactory;
|
||||||
import org.xbib.io.ftp.fs.FTPEnvironment;
|
import org.xbib.io.ftp.fs.FTPEnvironment;
|
||||||
import org.xbib.io.ftp.fs.FTPFileSystem;
|
import org.xbib.io.ftp.fs.FTPFileSystem;
|
||||||
|
|
|
@ -3,12 +3,12 @@ package org.xbib.io.ftp.fs.test;
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockftpserver.fake.filesystem.FileEntry;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
|
||||||
|
|
||||||
public class FTPFileSystemInputStreamTest extends AbstractFTPFileSystemTest {
|
public class FTPFileSystemInputStreamTest extends AbstractFTPFileSystemTest {
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ package org.xbib.io.ftp.fs.test;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockftpserver.fake.filesystem.FileEntry;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
|
||||||
|
|
||||||
public class FTPFileSystemOutputStreamTest extends AbstractFTPFileSystemTest {
|
public class FTPFileSystemOutputStreamTest extends AbstractFTPFileSystemTest {
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package org.xbib.io.ftp.fs.test;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockftpserver.fake.filesystem.FileEntry;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
@ -18,6 +17,7 @@ import java.nio.file.attribute.PosixFileAttributeView;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
|
||||||
import org.xbib.io.ftp.fs.FTPFileSystem;
|
import org.xbib.io.ftp.fs.FTPFileSystem;
|
||||||
import org.xbib.io.ftp.fs.FTPFileSystemProvider;
|
import org.xbib.io.ftp.fs.FTPFileSystemProvider;
|
||||||
import org.xbib.io.ftp.fs.FTPPath;
|
import org.xbib.io.ftp.fs.FTPPath;
|
||||||
|
|
|
@ -2,10 +2,10 @@ package org.xbib.io.ftp.fs.test;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockftpserver.fake.filesystem.DirectoryEntry;
|
|
||||||
import org.mockftpserver.fake.filesystem.FileEntry;
|
|
||||||
import org.mockftpserver.fake.filesystem.FileSystemEntry;
|
|
||||||
import org.mockito.verification.VerificationMode;
|
import org.mockito.verification.VerificationMode;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.DirectoryEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
import org.xbib.io.ftp.client.FTPFile;
|
import org.xbib.io.ftp.client.FTPFile;
|
||||||
import org.xbib.io.ftp.fs.FTPFileSystem;
|
import org.xbib.io.ftp.fs.FTPFileSystem;
|
||||||
import org.xbib.io.ftp.fs.FTPFileSystemException;
|
import org.xbib.io.ftp.fs.FTPFileSystemException;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package org.xbib.io.ftp.fs.test.server;
|
package org.xbib.io.ftp.fs.test.server;
|
||||||
|
|
||||||
import org.mockftpserver.fake.filesystem.FileSystemEntry;
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
import org.mockftpserver.fake.filesystem.UnixDirectoryListingFormatter;
|
import org.xbib.files.ftp.mock.fake.filesystem.UnixDirectoryListingFormatter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An extended version of {@link UnixDirectoryListingFormatter} that supports symbolic links.
|
* An extended version of {@link UnixDirectoryListingFormatter} that supports symbolic links.
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package org.xbib.io.ftp.fs.test.server;
|
package org.xbib.io.ftp.fs.test.server;
|
||||||
|
|
||||||
import org.mockftpserver.fake.filesystem.FileSystemEntry;
|
|
||||||
import org.mockftpserver.fake.filesystem.UnixFakeFileSystem;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.UnixFakeFileSystem;
|
||||||
|
import static org.xbib.io.ftp.client.parser.FTPTimestampParserImpl.getEntry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An extended version of {@link UnixFakeFileSystem} that supports symbolic links.
|
* An extended version of {@link UnixFakeFileSystem} that supports symbolic links.
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
package org.xbib.io.ftp.fs.test.server;
|
package org.xbib.io.ftp.fs.test.server;
|
||||||
|
|
||||||
import org.mockftpserver.core.command.Command;
|
|
||||||
import org.mockftpserver.core.command.ReplyCodes;
|
|
||||||
import org.mockftpserver.core.session.Session;
|
|
||||||
import org.mockftpserver.core.util.StringUtil;
|
|
||||||
import org.mockftpserver.fake.command.ListCommandHandler;
|
|
||||||
import org.mockftpserver.fake.filesystem.DirectoryEntry;
|
|
||||||
import org.mockftpserver.fake.filesystem.FileSystemEntry;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.StringUtil;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.ListCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.DirectoryEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command handler for LIST that supports the {@code -a} flag.
|
* A command handler for LIST that supports the {@code -a} flag.
|
||||||
|
@ -69,7 +69,7 @@ public class ListHiddenFilesCommandHandler extends ListCommandHandler {
|
||||||
sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);
|
sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);
|
||||||
|
|
||||||
session.openDataConnection();
|
session.openDataConnection();
|
||||||
LOG.info("Sending [" + result + "]");
|
LOG.log(Level.INFO, "Sending [" + result + "]");
|
||||||
session.sendData(result.getBytes(), result.length());
|
session.sendData(result.getBytes(), result.length());
|
||||||
session.closeDataConnection();
|
session.closeDataConnection();
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
package org.xbib.io.ftp.fs.test.server;
|
package org.xbib.io.ftp.fs.test.server;
|
||||||
|
|
||||||
import org.mockftpserver.core.command.Command;
|
|
||||||
import org.mockftpserver.core.command.ReplyCodes;
|
|
||||||
import org.mockftpserver.core.session.Session;
|
|
||||||
import org.mockftpserver.fake.command.AbstractFakeCommandHandler;
|
|
||||||
import org.mockftpserver.fake.filesystem.FileSystemEntry;
|
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.AbstractFakeCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command handler for the MDTM command.
|
* A command handler for the MDTM command.
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
package org.xbib.io.ftp.fs.test.server;
|
package org.xbib.io.ftp.fs.test.server;
|
||||||
|
|
||||||
import org.mockftpserver.fake.filesystem.FileSystemEntry;
|
|
||||||
import org.mockftpserver.fake.filesystem.Permissions;
|
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.Permissions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A representation of symbolic links.
|
* A representation of symbolic links.
|
||||||
|
|
5
files-ftp-mock/build.gradle
Normal file
5
files-ftp-mock/build.gradle
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
dependencies {
|
||||||
|
testImplementation testLibs.junit.jupiter.params
|
||||||
|
testImplementation project(':files-ftp')
|
||||||
|
testImplementation testLibs.mockito.core
|
||||||
|
}
|
14
files-ftp-mock/src/main/java/module-info.java
Normal file
14
files-ftp-mock/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module org.xbib.files.ftp.mock {
|
||||||
|
exports org.xbib.files.ftp.mock.core;
|
||||||
|
exports org.xbib.files.ftp.mock.fake;
|
||||||
|
exports org.xbib.files.ftp.mock.stub;
|
||||||
|
exports org.xbib.files.ftp.mock.stub.command;
|
||||||
|
exports org.xbib.files.ftp.mock.core.util;
|
||||||
|
exports org.xbib.files.ftp.mock.core.server;
|
||||||
|
exports org.xbib.files.ftp.mock.core.socket;
|
||||||
|
exports org.xbib.files.ftp.mock.core.command;
|
||||||
|
exports org.xbib.files.ftp.mock.core.session;
|
||||||
|
exports org.xbib.files.ftp.mock.fake.filesystem;
|
||||||
|
exports org.xbib.files.ftp.mock.fake.command;
|
||||||
|
requires java.logging;
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error indicating that a server command has been received that
|
||||||
|
* has invalid syntax. For instance, the command may be missing a required parameter.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class CommandSyntaxException extends MockFtpServerException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param message - the exception message
|
||||||
|
*/
|
||||||
|
public CommandSyntaxException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cause - the cause exception
|
||||||
|
*/
|
||||||
|
public CommandSyntaxException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param message - the exception message
|
||||||
|
* @param cause - the cause exception
|
||||||
|
*/
|
||||||
|
public CommandSyntaxException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error indicating that the server is in an illegal state, or
|
||||||
|
* that a server command is invoked when its preconditions have not been met.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class IllegalStateException extends MockFtpServerException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param message - the exception message
|
||||||
|
*/
|
||||||
|
public IllegalStateException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cause - the cause exception
|
||||||
|
*/
|
||||||
|
public IllegalStateException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param message - the exception message
|
||||||
|
* @param cause - the cause exception
|
||||||
|
*/
|
||||||
|
public IllegalStateException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error specific to the MockFtpServer project.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class MockFtpServerException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance with the specified detail message and no cause
|
||||||
|
* @param message - the exception detail message
|
||||||
|
*/
|
||||||
|
public MockFtpServerException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance with the specified detail message and no cause
|
||||||
|
* @param cause - the Throwable cause
|
||||||
|
*/
|
||||||
|
public MockFtpServerException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance with the specified detail message and cause
|
||||||
|
* @param message - the exception detail message
|
||||||
|
* @param cause - the Throwable cause
|
||||||
|
*/
|
||||||
|
public MockFtpServerException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error indicating that the current user has not yet logged in, but
|
||||||
|
* is required to in order to invoke the requested command.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class NotLoggedInException extends MockFtpServerException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param message - the exception message
|
||||||
|
*/
|
||||||
|
public NotLoggedInException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cause - the cause exception
|
||||||
|
*/
|
||||||
|
public NotLoggedInException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param message - the exception message
|
||||||
|
* @param cause - the cause exception
|
||||||
|
*/
|
||||||
|
public NotLoggedInException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract superclass for CommandHandler classes.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public abstract class AbstractCommandHandler implements CommandHandler, ReplyTextBundleAware {
|
||||||
|
|
||||||
|
protected final Logger LOG = Logger.getLogger(getClass().getName());
|
||||||
|
|
||||||
|
private ResourceBundle replyTextBundle;
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// Support for reply text ResourceBundle
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the ResourceBundle containing the reply text messages
|
||||||
|
*
|
||||||
|
* @return the replyTextBundle
|
||||||
|
* @see ReplyTextBundleAware#getReplyTextBundle()
|
||||||
|
*/
|
||||||
|
public ResourceBundle getReplyTextBundle() {
|
||||||
|
return replyTextBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the ResourceBundle containing the reply text messages
|
||||||
|
*
|
||||||
|
* @param replyTextBundle - the replyTextBundle to set
|
||||||
|
* @see ReplyTextBundleAware#setReplyTextBundle(ResourceBundle)
|
||||||
|
*/
|
||||||
|
public void setReplyTextBundle(ResourceBundle replyTextBundle) {
|
||||||
|
this.replyTextBundle = replyTextBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Utility methods for subclasses
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the specified text surrounded with double quotes
|
||||||
|
*
|
||||||
|
* @param text - the text to surround with quotes
|
||||||
|
* @return the text with leading and trailing double quotes
|
||||||
|
* @throws AssertFailedException
|
||||||
|
* - if text is null
|
||||||
|
*/
|
||||||
|
public static String quotes(String text) {
|
||||||
|
Assert.notNull(text, "text");
|
||||||
|
final String QUOTES = "\"";
|
||||||
|
return QUOTES + text + QUOTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the specified number is a valid reply code
|
||||||
|
*
|
||||||
|
* @param replyCode - the reply code to check
|
||||||
|
* @throws AssertFailedException
|
||||||
|
* - if the replyCode is invalid
|
||||||
|
*/
|
||||||
|
public void assertValidReplyCode(int replyCode) {
|
||||||
|
Assert.isTrue(replyCode > 0, "The number [" + replyCode + "] is not a valid reply code");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract superclass for CommandHandler classes that default to sending
|
||||||
|
* back a configured reply code and text. You can customize the returned reply
|
||||||
|
* code by setting the required <code>replyCode</code> property. If only the
|
||||||
|
* <code>replyCode</code> property is set, then the default reply text corresponding to that
|
||||||
|
* reply code is used in the response. You can optionally configure the reply text by setting
|
||||||
|
* the <code>replyMessageKey</code> or <code>replyText</code> property.
|
||||||
|
*
|
||||||
|
* <p>Subclasses can optionally override the reply code and/or text for the reply by calling
|
||||||
|
* {@link #setReplyCode(int)}, {@link #setReplyMessageKey(String)} and {@link #setReplyText(String)}.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public abstract class AbstractStaticReplyCommandHandler extends AbstractTrackingCommandHandler {
|
||||||
|
|
||||||
|
// Defaults to zero; must be set to non-zero
|
||||||
|
protected int replyCode = 0;
|
||||||
|
|
||||||
|
// Defaults to null; if set to non-null, this value will override the default reply text associated with
|
||||||
|
// the replyCode.
|
||||||
|
protected String replyText = null;
|
||||||
|
|
||||||
|
// The message key for the reply text. Defaults to null. If null, use the default message associated
|
||||||
|
// with the reply code
|
||||||
|
protected String replyMessageKey = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the reply code.
|
||||||
|
*
|
||||||
|
* @param replyCode - the replyCode
|
||||||
|
*
|
||||||
|
* @throws AssertFailedException - if the replyCode is not valid
|
||||||
|
*/
|
||||||
|
public void setReplyCode(int replyCode) {
|
||||||
|
assertValidReplyCode(replyCode);
|
||||||
|
this.replyCode = replyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the reply text. If null, then use the (default) message key for the replyCode.
|
||||||
|
*
|
||||||
|
* @param replyText - the replyText
|
||||||
|
*/
|
||||||
|
public void setReplyText(String replyText) {
|
||||||
|
this.replyText = replyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the message key for the reply text. If null, then use the default message key.
|
||||||
|
*
|
||||||
|
* @param replyMessageKey - the replyMessageKey to set
|
||||||
|
*/
|
||||||
|
public void setReplyMessageKey(String replyMessageKey) {
|
||||||
|
this.replyMessageKey = replyMessageKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Utility methods for subclasses
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the reply using the replyCode and message key/text configured for this command handler.
|
||||||
|
* @param session - the Session
|
||||||
|
*
|
||||||
|
* @throws AssertFailedException if the replyCode is not valid
|
||||||
|
*/
|
||||||
|
public void sendReply(Session session) {
|
||||||
|
sendReply(session, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the reply using the replyCode and message key/text configured for this command handler.
|
||||||
|
* @param session - the Session
|
||||||
|
* @param messageParameter - message parameter; may be null
|
||||||
|
*
|
||||||
|
* @throws AssertFailedException if the replyCode is not valid
|
||||||
|
*/
|
||||||
|
public void sendReply(Session session, Object messageParameter) {
|
||||||
|
Object[] parameters = (messageParameter == null) ? null : new Object[] { messageParameter };
|
||||||
|
sendReply(session, replyCode, replyMessageKey, replyText, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.xbib.files.ftp.mock.core.CommandSyntaxException;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.MissingResourceException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract superclass for CommandHandler classes that manage the List of InvocationRecord
|
||||||
|
* objects corresponding to each invocation of the command handler, and provide helper methods for subclasses.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public abstract class AbstractTrackingCommandHandler extends AbstractCommandHandler implements InvocationHistory {
|
||||||
|
|
||||||
|
private List invocations = new ArrayList();
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Template Method
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the specified command for the session. This method is declared to throw Exception,
|
||||||
|
* allowing CommandHandler implementations to avoid unnecessary exception-handling. All checked
|
||||||
|
* exceptions are expected to be wrapped and handled by the caller.
|
||||||
|
*
|
||||||
|
* @param command - the Command to be handled
|
||||||
|
* @param session - the session on which the Command was submitted
|
||||||
|
* @throws Exception - if an error occurs
|
||||||
|
* @throws AssertFailedException - if the command or session is null
|
||||||
|
* @see CommandHandler#handleCommand(Command,
|
||||||
|
* Session)
|
||||||
|
*/
|
||||||
|
public final void handleCommand(Command command, Session session) throws Exception {
|
||||||
|
Assert.notNull(command, "command");
|
||||||
|
Assert.notNull(session, "session");
|
||||||
|
InvocationRecord invocationRecord = new InvocationRecord(command, session.getClientHost());
|
||||||
|
invocations.add(invocationRecord);
|
||||||
|
try {
|
||||||
|
handleCommand(command, session, invocationRecord);
|
||||||
|
}
|
||||||
|
catch (CommandSyntaxException e) {
|
||||||
|
sendReply(session, ReplyCodes.COMMAND_SYNTAX_ERROR, null, null, null);
|
||||||
|
}
|
||||||
|
invocationRecord.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the specified command for the session. This method is declared to throw Exception,
|
||||||
|
* allowing CommandHandler implementations to avoid unnecessary exception-handling. All checked
|
||||||
|
* exceptions are expected to be wrapped and handled by the caller.
|
||||||
|
*
|
||||||
|
* @param command - the Command to be handled
|
||||||
|
* @param session - the session on which the Command was submitted
|
||||||
|
* @param invocationRecord - the InvocationRecord; CommandHandlers are expected to add
|
||||||
|
* handler-specific data to the InvocationRecord, as appropriate
|
||||||
|
* @throws Exception - if an error occurs
|
||||||
|
*/
|
||||||
|
protected abstract void handleCommand(Command command, Session session, InvocationRecord invocationRecord)
|
||||||
|
throws Exception;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Utility methods for subclasses
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a reply for this command on the control connection.
|
||||||
|
*
|
||||||
|
* <p>The reply code is designated by the <code>replyCode</code> property, and the reply text
|
||||||
|
* is determined by the following rules:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the <code>replyText</code> property is non-null, then use that.</li>
|
||||||
|
* <li>Otherwise, if <code>replyMessageKey</code> is non-null, the use that to retrieve a
|
||||||
|
* localized message from the <code>replyText</code> ResourceBundle.</li>
|
||||||
|
* <li>Otherwise, retrieve the reply text from the <code>replyText</code> ResourceBundle,
|
||||||
|
* using the reply code as the key.</li>
|
||||||
|
* </ol>
|
||||||
|
* If the arguments Object[] is not null, then these arguments are substituted within the
|
||||||
|
* reply text using the {@link MessageFormat} class.
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
* @param replyCode - the reply code
|
||||||
|
* @param replyMessageKey - if not null (and replyText is null), this is used as the ResourceBundle
|
||||||
|
* message key instead of the reply code.
|
||||||
|
* @param replyText - if non-null, this is used as the reply text
|
||||||
|
* @param arguments - the array of arguments to be formatted and substituted within the reply
|
||||||
|
* text; may be null
|
||||||
|
* @throws AssertFailedException - if session is null
|
||||||
|
* @see MessageFormat
|
||||||
|
*/
|
||||||
|
public void sendReply(Session session, int replyCode, String replyMessageKey, String replyText,
|
||||||
|
Object[] arguments) {
|
||||||
|
|
||||||
|
Assert.notNull(session, "session");
|
||||||
|
assertValidReplyCode(replyCode);
|
||||||
|
|
||||||
|
String key = (replyMessageKey != null) ? replyMessageKey : Integer.toString(replyCode);
|
||||||
|
String text = getTextForReplyCode(replyCode, key, replyText, arguments);
|
||||||
|
String replyTextToLog = (text == null) ? "" : " " + text;
|
||||||
|
LOG.info("Sending reply [" + replyCode + replyTextToLog + "]");
|
||||||
|
session.sendReply(replyCode, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// InvocationHistory - Support for command history
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the number of invocation records stored for this command handler instance
|
||||||
|
* @see InvocationHistory#numberOfInvocations()
|
||||||
|
*/
|
||||||
|
public int numberOfInvocations() {
|
||||||
|
return invocations.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the InvocationRecord representing the command invoction data for the nth invocation
|
||||||
|
* for this command handler instance. One InvocationRecord should be stored for each invocation
|
||||||
|
* of the CommandHandler.
|
||||||
|
*
|
||||||
|
* @param index - the index of the invocation record to return. The first record is at index zero.
|
||||||
|
* @return the InvocationRecord for the specified index
|
||||||
|
* @throws AssertFailedException - if there is no invocation record corresponding to the specified index
|
||||||
|
* @see InvocationHistory#getInvocation(int)
|
||||||
|
*/
|
||||||
|
public InvocationRecord getInvocation(int index) {
|
||||||
|
return (InvocationRecord) invocations.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear out the invocation history for this CommandHandler. After invoking this method, the
|
||||||
|
* <code>numberOfInvocations()</code> method will return zero.
|
||||||
|
*
|
||||||
|
* @see InvocationHistory#clearInvocations()
|
||||||
|
*/
|
||||||
|
public void clearInvocations() {
|
||||||
|
invocations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Internal Helper Methods
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the text for the specified reply code, formatted using the message arguments, if
|
||||||
|
* supplied. If overrideText is not null, then return that. Otherwise, return the text mapped to
|
||||||
|
* the code from the replyText ResourceBundle. If the ResourceBundle contains no mapping, then
|
||||||
|
* return null.
|
||||||
|
*
|
||||||
|
* <p>If arguments is not null, then the returned reply text if formatted using the
|
||||||
|
* {@link MessageFormat} class.
|
||||||
|
*
|
||||||
|
* @param code - the reply code
|
||||||
|
* @param messageKey - the key used to retrieve the reply text from the replyTextBundle
|
||||||
|
* @param overrideText - if not null, this is used instead of the text from the replyTextBundle.
|
||||||
|
* @param arguments - the array of arguments to be formatted and substituted within the reply
|
||||||
|
* text; may be null
|
||||||
|
* @return the text for the reply code; may be null
|
||||||
|
*/
|
||||||
|
private String getTextForReplyCode(int code, String messageKey, String overrideText, Object[] arguments) {
|
||||||
|
try {
|
||||||
|
String t = (overrideText == null) ? getReplyTextBundle().getString(messageKey) : overrideText;
|
||||||
|
String formattedMessage = MessageFormat.format(t, arguments);
|
||||||
|
return (formattedMessage == null) ? null : formattedMessage.trim();
|
||||||
|
}
|
||||||
|
catch (MissingResourceException e) {
|
||||||
|
// No reply text is mapped for the specified key
|
||||||
|
LOG.log(Level.WARNING, "No reply text defined for reply code [" + code + "]");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,171 @@
|
||||||
|
/*
|
||||||
|
* Copyright 20078 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.CommandSyntaxException;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a command received from an FTP client, containing a command name and parameters.
|
||||||
|
* Objects of this class are immutable.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class Command {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String[] parameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new immutable instance with the specified command name and parameters
|
||||||
|
*
|
||||||
|
* @param name - the command name; may not be null
|
||||||
|
* @param parameters - the command parameters; may be empty; may not be null
|
||||||
|
*/
|
||||||
|
public Command(String name, String[] parameters) {
|
||||||
|
Assert.notNull(name, "name");
|
||||||
|
Assert.notNull(parameters, "parameters");
|
||||||
|
this.name = name;
|
||||||
|
this.parameters = copy(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new immutable instance with the specified command name and parameters
|
||||||
|
*
|
||||||
|
* @param name - the command name; may not be null
|
||||||
|
* @param parameters - the command parameters; may be empty; may not be null
|
||||||
|
*/
|
||||||
|
public Command(String name, List parameters) {
|
||||||
|
this(name, (String[]) parameters.toArray(new String[parameters.size()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the parameters
|
||||||
|
*/
|
||||||
|
public String[] getParameters() {
|
||||||
|
return copy(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the String value of the parameter at the specified index
|
||||||
|
*
|
||||||
|
* @param index - the index
|
||||||
|
* @return the parameter value as a String
|
||||||
|
* @throws AssertFailedException
|
||||||
|
* if the parameter index is invalid or the value is not a valid String
|
||||||
|
*/
|
||||||
|
public String getRequiredParameter(int index) {
|
||||||
|
assertValidIndex(index);
|
||||||
|
return parameters[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the String value of the parameter at the specified index; return null if no parameter exists for the index
|
||||||
|
*
|
||||||
|
* @param index - the index
|
||||||
|
* @return the parameter value as a String, or null if this Command does not have a parameter for that index
|
||||||
|
*/
|
||||||
|
public String getParameter(int index) {
|
||||||
|
return (parameters.length > index) ? parameters[index] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the String value of the parameter at the specified index; return null if no
|
||||||
|
* parameter exists for the index. This is an alias for {@link #getParameter(int)}.
|
||||||
|
*
|
||||||
|
* @param index - the index
|
||||||
|
* @return the parameter value as a String, or null if this Command does not have a parameter for that index
|
||||||
|
*/
|
||||||
|
public String getOptionalString(int index) {
|
||||||
|
return getParameter(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Object#equals(Object)
|
||||||
|
*/
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || !(obj instanceof Command)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.hashCode() == obj.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Object#hashCode()
|
||||||
|
*/
|
||||||
|
public int hashCode() {
|
||||||
|
String str = name + Arrays.asList(parameters);
|
||||||
|
return str.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the String representation of this object
|
||||||
|
*
|
||||||
|
* @see Object#toString()
|
||||||
|
*/
|
||||||
|
public String toString() {
|
||||||
|
return "Command[" + name + ":" + Arrays.asList(parameters) + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the name, normalized to a common format - convert to upper case.
|
||||||
|
* @param name - the command name
|
||||||
|
* @return the name converted to upper case
|
||||||
|
*/
|
||||||
|
public static String normalizeName(String name) {
|
||||||
|
return name.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a shallow copy of the specified array
|
||||||
|
*
|
||||||
|
* @param array - the array to copy
|
||||||
|
* @return a new array with the same contents
|
||||||
|
*/
|
||||||
|
private static String[] copy(String[] array) {
|
||||||
|
String[] newArray = new String[array.length];
|
||||||
|
System.arraycopy(array, 0, newArray, 0, array.length);
|
||||||
|
return newArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the index is valid
|
||||||
|
*
|
||||||
|
* @param index - the index
|
||||||
|
* @throws CommandSyntaxException
|
||||||
|
* - if the parameter index is invalid
|
||||||
|
*/
|
||||||
|
private void assertValidIndex(int index) {
|
||||||
|
if (index < 0 || index >= parameters.length) {
|
||||||
|
throw new CommandSyntaxException("The parameter index " + index + " is not valid for " + this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for classes that can handle an FTP command.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public interface CommandHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the specified command for the session. This method is declared to throw
|
||||||
|
* Exception, allowing CommandHandler implementations to avoid unnecessary
|
||||||
|
* exception-handling. All checked exceptions are expected to be wrapped and handled
|
||||||
|
* by the caller.
|
||||||
|
*
|
||||||
|
* @param command - the Command to be handled
|
||||||
|
* @param session - the session on which the Command was submitted
|
||||||
|
*
|
||||||
|
* @throws Exception - if an error occurs
|
||||||
|
*/
|
||||||
|
public void handleCommand(Command command, Session session) throws Exception;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FTP command name constants.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class CommandNames {
|
||||||
|
|
||||||
|
public static final String ABOR = "ABOR";
|
||||||
|
public static final String ACCT = "ACCT";
|
||||||
|
public static final String ALLO = "ALLO";
|
||||||
|
public static final String APPE = "APPE";
|
||||||
|
public static final String CDUP = "CDUP";
|
||||||
|
public static final String CWD = "CWD";
|
||||||
|
public static final String DELE = "DELE";
|
||||||
|
public static final String EPRT = "EPRT";
|
||||||
|
public static final String EPSV = "EPSV";
|
||||||
|
public static final String HELP = "HELP";
|
||||||
|
public static final String LIST = "LIST";
|
||||||
|
public static final String MKD = "MKD";
|
||||||
|
public static final String MODE = "MODE";
|
||||||
|
public static final String NLST = "NLST";
|
||||||
|
public static final String NOOP = "NOOP";
|
||||||
|
public static final String PASS = "PASS";
|
||||||
|
public static final String PASV = "PASV";
|
||||||
|
public static final String PORT = "PORT";
|
||||||
|
public static final String PWD = "PWD";
|
||||||
|
public static final String QUIT = "QUIT";
|
||||||
|
public static final String REIN = "REIN";
|
||||||
|
public static final String REST = "REST";
|
||||||
|
public static final String RETR = "RETR";
|
||||||
|
public static final String RMD = "RMD";
|
||||||
|
public static final String RNFR = "RNFR";
|
||||||
|
public static final String RNTO = "RNTO";
|
||||||
|
public static final String SITE = "SITE";
|
||||||
|
public static final String SIZE = "SIZE";
|
||||||
|
public static final String SMNT = "SMNT";
|
||||||
|
public static final String STAT = "STAT";
|
||||||
|
public static final String STOR = "STOR";
|
||||||
|
public static final String STOU = "STOU";
|
||||||
|
public static final String STRU = "STRU";
|
||||||
|
public static final String SYST = "SYST";
|
||||||
|
public static final String TYPE = "TYPE";
|
||||||
|
public static final String USER = "USER";
|
||||||
|
|
||||||
|
public static final String XPWD = "XPWD";
|
||||||
|
|
||||||
|
// Special commands - not "real" FTP commands
|
||||||
|
public static final String CONNECT = "CONNECT";
|
||||||
|
public static final String UNSUPPORTED = "UNSUPPORTED";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor. This class should not be instantiated.
|
||||||
|
*/
|
||||||
|
private CommandNames() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler that encapsulates the sending of the reply for the initial connection from
|
||||||
|
* the FTP client to the server. Send back a reply code of 220, indicating a successful connection.
|
||||||
|
*
|
||||||
|
* <p>Note that this is a "special" CommandHandler, in that it handles the initial connection from the
|
||||||
|
* client, rather than an explicit FTP command.
|
||||||
|
*
|
||||||
|
* <p>Each invocation record stored by this CommandHandler contains no data elements.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class ConnectCommandHandler extends AbstractStaticReplyCommandHandler implements CommandHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor. Initiate the replyCode.
|
||||||
|
*/
|
||||||
|
public ConnectCommandHandler() {
|
||||||
|
setReplyCode(ReplyCodes.CONNECT_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see AbstractTrackingCommandHandler#handleCommand(Command, Session, InvocationRecord)
|
||||||
|
*/
|
||||||
|
public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {
|
||||||
|
sendReply(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for an object that can retrieve and clear the history of InvocationRecords
|
||||||
|
* for a command handler.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public interface InvocationHistory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the number of invocation records stored for this command handler instance
|
||||||
|
*/
|
||||||
|
int numberOfInvocations();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the InvocationRecord representing the command invoction data for the nth invocation
|
||||||
|
* for this command handler instance. One InvocationRecord should be stored for each invocation
|
||||||
|
* of the CommandHandler.
|
||||||
|
*
|
||||||
|
* @param index - the index of the invocation record to return. The first record is at index zero.
|
||||||
|
* @return the InvocationRecord for the specified index
|
||||||
|
*
|
||||||
|
* @throws AssertFailedException - if there is no invocation record corresponding to the specified index */
|
||||||
|
InvocationRecord getInvocation(int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear out the invocation history for this CommandHandler. After invoking this method, the
|
||||||
|
* <code>numberOfInvocations()</code> method will return zero.
|
||||||
|
*/
|
||||||
|
void clearInvocations();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents information about a single FTP Command invocation. Manages and provides access to
|
||||||
|
* the Command, the host address (<code>InetAddress</code>) of the client that submitted the
|
||||||
|
* Command and the timestamp of the Command submission.
|
||||||
|
*
|
||||||
|
* <p>This class also supports storing zero or more arbitrary mappings of <i>key</i> to value, where <i>key</i> is
|
||||||
|
* a String and <i>value</i> is any Object. Convenience methods are provided that enable retrieving
|
||||||
|
* type-specific data by its <i>key</i>. The data stored in an {@link InvocationRecord} is CommandHandler-specific.
|
||||||
|
*
|
||||||
|
* <p>The {@link #lock()} method makes an instance of this class immutable. After an instance is locked,
|
||||||
|
* calling the {@link #set(String, Object)} method will throw an <code>AssertFailedException</code>.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class InvocationRecord {
|
||||||
|
|
||||||
|
private Command command;
|
||||||
|
private Date time;
|
||||||
|
private InetAddress clientHost;
|
||||||
|
private Map data = new HashMap();
|
||||||
|
private boolean locked = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance
|
||||||
|
*
|
||||||
|
* @param command - the Command
|
||||||
|
* @param clientHost - the client host
|
||||||
|
*/
|
||||||
|
public InvocationRecord(Command command, InetAddress clientHost) {
|
||||||
|
this.command = command;
|
||||||
|
this.time = new Date();
|
||||||
|
this.clientHost = clientHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock this instance, making it immutable. After an instance is locked,
|
||||||
|
* calling the {@link #set(String, Object)} method will throw an
|
||||||
|
* <code>AssertFailedException</code>.
|
||||||
|
*/
|
||||||
|
public void lock() {
|
||||||
|
locked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if this object has been locked, false otherwise. See {@link #lock()}.
|
||||||
|
*
|
||||||
|
* @return true if this object has been locked, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isLocked() {
|
||||||
|
return locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the client host that submitted the command, as an InetAddress
|
||||||
|
*/
|
||||||
|
public InetAddress getClientHost() {
|
||||||
|
return clientHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the Command
|
||||||
|
*/
|
||||||
|
public Command getCommand() {
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the time that the command was processed; this may differ slightly from when the command was received.
|
||||||
|
*/
|
||||||
|
public Date getTime() {
|
||||||
|
// Return a copy of the Date object to preserve immutability
|
||||||
|
return new Date(time.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the value for the specified key. If this object already contained a mapping
|
||||||
|
* for this key, the old value is replaced by the specified value. This method throws
|
||||||
|
* an <code>AssertFailedException</code> if this object has been locked. See {@link #lock()}.
|
||||||
|
*
|
||||||
|
* @param key - the key; must not be null
|
||||||
|
* @param value - the value to store for the specified key
|
||||||
|
* @throws AssertFailedException - if the key is null or this object has been locked.
|
||||||
|
*/
|
||||||
|
public void set(String key, Object value) {
|
||||||
|
Assert.notNull(key, "key");
|
||||||
|
Assert.isFalse(locked, "The InvocationRecord is locked!");
|
||||||
|
data.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <code>true</code> if this object contains a mapping for the specified key.
|
||||||
|
*
|
||||||
|
* @param key - the key; must not be null
|
||||||
|
* @return <code>true</code> if there is a mapping for the key
|
||||||
|
* @throws AssertFailedException - if the key is null
|
||||||
|
*/
|
||||||
|
public boolean containsKey(String key) {
|
||||||
|
Assert.notNull(key, "key");
|
||||||
|
return data.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Set view of the keys for the data stored in this object.
|
||||||
|
* Changes to the returned Set have no effect on the data stored within this object
|
||||||
|
* .
|
||||||
|
*
|
||||||
|
* @return the Set of keys for the data stored within this object
|
||||||
|
*/
|
||||||
|
public Set keySet() {
|
||||||
|
return Collections.unmodifiableSet(data.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the String value associated with the specified key. Returns null if there is
|
||||||
|
* no mapping for this key. A return value of null does not necessarily indicate that
|
||||||
|
* this object contains no mapping for the key; it's also possible that the value was
|
||||||
|
* explicitly set to null for the key. The containsKey operation may be used to
|
||||||
|
* distinguish these two cases.
|
||||||
|
*
|
||||||
|
* @param key - the key; must not be null
|
||||||
|
* @return the String data stored at the specified key; may be null
|
||||||
|
* @throws ClassCastException - if the object for the specified key is not a String
|
||||||
|
* @throws AssertFailedException - if the key is null
|
||||||
|
*/
|
||||||
|
public String getString(String key) {
|
||||||
|
Assert.notNull(key, "key");
|
||||||
|
return (String) data.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Object value associated with the specified key. Returns null if there is
|
||||||
|
* no mapping for this key. A return value of null does not necessarily indicate that
|
||||||
|
* this object contains no mapping for the key; it's also possible that the value was
|
||||||
|
* explicitly set to null for the key. The containsKey operation may be used to
|
||||||
|
* distinguish these two cases.
|
||||||
|
*
|
||||||
|
* @param key - the key; must not be null
|
||||||
|
* @return the data stored at the specified key, as an Object; may be null
|
||||||
|
* @throws AssertFailedException - if the key is null
|
||||||
|
*/
|
||||||
|
public Object getObject(String key) {
|
||||||
|
Assert.notNull(key, "key");
|
||||||
|
return data.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the String representation of this object
|
||||||
|
*
|
||||||
|
* @see Object#toString()
|
||||||
|
*/
|
||||||
|
public String toString() {
|
||||||
|
return "InvocationRecord[time=" + time + " client-host=" + clientHost + " command=" + command + " data=" + data + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reply Code constants.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class ReplyCodes {
|
||||||
|
|
||||||
|
public static final int ABOR_OK = 226;
|
||||||
|
public static final int ACCT_OK = 230;
|
||||||
|
public static final int ALLO_OK = 200;
|
||||||
|
public static final int CDUP_OK = 200;
|
||||||
|
public static final int CWD_OK = 250;
|
||||||
|
public static final int DELE_OK = 250;
|
||||||
|
public static final int EPRT_OK = 200;
|
||||||
|
public static final int EPSV_OK = 229;
|
||||||
|
public static final int HELP_OK = 214;
|
||||||
|
public static final int MKD_OK = 257;
|
||||||
|
public static final int MODE_OK = 200;
|
||||||
|
public static final int NOOP_OK = 200;
|
||||||
|
public static final int PASS_OK = 230;
|
||||||
|
public static final int PASS_NEED_ACCOUNT = 332;
|
||||||
|
public static final int PASS_LOG_IN_FAILED = 530;
|
||||||
|
public static final int PASV_OK = 227;
|
||||||
|
public static final int PORT_OK = 200;
|
||||||
|
public static final int PWD_OK = 257;
|
||||||
|
public static final int QUIT_OK = 221;
|
||||||
|
public static final int REIN_OK = 220;
|
||||||
|
public static final int REST_OK = 350;
|
||||||
|
public static final int RMD_OK = 250;
|
||||||
|
public static final int RNFR_OK = 350;
|
||||||
|
public static final int RNTO_OK = 250;
|
||||||
|
public static final int SITE_OK = 200;
|
||||||
|
public static final int SMNT_OK = 250;
|
||||||
|
public static final int STAT_SYSTEM_OK = 211;
|
||||||
|
public static final int STAT_FILE_OK = 213;
|
||||||
|
public static final int SIZE_OK = 213;
|
||||||
|
public static final int STRU_OK = 200;
|
||||||
|
public static final int SYST_OK = 215;
|
||||||
|
public static final int TYPE_OK = 200;
|
||||||
|
public static final int USER_LOGGED_IN_OK = 230;
|
||||||
|
public static final int USER_NEED_PASSWORD_OK = 331;
|
||||||
|
public static final int USER_NO_SUCH_USER = 530;
|
||||||
|
public static final int USER_ACCOUNT_NOT_VALID = 530;
|
||||||
|
|
||||||
|
public static final int TRANSFER_DATA_INITIAL_OK = 150;
|
||||||
|
public static final int TRANSFER_DATA_FINAL_OK = 226;
|
||||||
|
|
||||||
|
public static final int CONNECT_OK = 220;
|
||||||
|
|
||||||
|
// GENERIC
|
||||||
|
public static final int SYSTEM_ERROR = 451;
|
||||||
|
public static final int COMMAND_SYNTAX_ERROR = 501;
|
||||||
|
public static final int COMMAND_NOT_SUPPORTED = 502;
|
||||||
|
public static final int ILLEGAL_STATE = 503; // Bad sequence
|
||||||
|
public static final int NOT_LOGGED_IN = 530;
|
||||||
|
public static final int READ_FILE_ERROR = 550;
|
||||||
|
public static final int WRITE_FILE_ERROR = 553;
|
||||||
|
public static final int FILENAME_NOT_VALID = 553;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor. This class should not be instantiated.
|
||||||
|
*/
|
||||||
|
private ReplyCodes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for objects that allow getting and setting a reply text ResourceBundle. This
|
||||||
|
* interface is implemented by CommandHandlers so that the StubFtpServer can automatically
|
||||||
|
* set the default reply text ResourceBundle for the CommandHandler.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public interface ReplyTextBundleAware {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the ResourceBundle containing the reply text messages
|
||||||
|
* @return the replyTextBundle
|
||||||
|
*/
|
||||||
|
public ResourceBundle getReplyTextBundle();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the ResourceBundle containing the reply text messages
|
||||||
|
* @param replyTextBundle - the replyTextBundle to set
|
||||||
|
*/
|
||||||
|
public void setReplyTextBundle(ResourceBundle replyTextBundle);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains common utility method to conditionally set the reply text ResourceBundle on a
|
||||||
|
* CommandHandler instance.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class ReplyTextBundleUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the <code>replyTextBundle</code> property of the specified CommandHandler to the
|
||||||
|
* <code>ResourceBundle</code> if and only if the <code>commandHandler</code> implements the
|
||||||
|
* {@link ReplyTextBundleAware} interface AND its <code>replyTextBundle</code> property
|
||||||
|
* has not been set (is null).
|
||||||
|
*
|
||||||
|
* @param commandHandler - the CommandHandler instance
|
||||||
|
* @param replyTextBundle - the ResourceBundle to use for localizing reply text
|
||||||
|
*
|
||||||
|
* @throws AssertFailedException - if the commandHandler is null
|
||||||
|
*/
|
||||||
|
public static void setReplyTextBundleIfAppropriate(CommandHandler commandHandler, ResourceBundle replyTextBundle) {
|
||||||
|
Assert.notNull(commandHandler, "commandHandler");
|
||||||
|
if (commandHandler instanceof ReplyTextBundleAware) {
|
||||||
|
ReplyTextBundleAware replyTextBundleAware = (ReplyTextBundleAware) commandHandler;
|
||||||
|
if (replyTextBundleAware.getReplyTextBundle() == null) {
|
||||||
|
replyTextBundleAware.setReplyTextBundle(replyTextBundle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composite CommandHandler that manages an internal list of CommandHandlers to which it delegates.
|
||||||
|
* The internal CommandHandlers are maintained in an ordered list. Starting with the first
|
||||||
|
* CommandHandler in the list, each invocation of this composite handler will invoke (delegate to)
|
||||||
|
* the current internal CommandHander. Then it moves on the next CommandHandler in the internal list.
|
||||||
|
*
|
||||||
|
* <p>The following example replaces the CWD CommandHandler with a <code>SimpleCompositeCommandHandler</code>.
|
||||||
|
* The first invocation of the CWD command will fail (reply code 500). The seconds will succeed.
|
||||||
|
* <pre><code>
|
||||||
|
*
|
||||||
|
* StubFtpServer stubFtpServer = new StubFtpServer();
|
||||||
|
*
|
||||||
|
* CommandHandler commandHandler1 = new StaticReplyCommandHandler(500);
|
||||||
|
* CommandHandler commandHandler2 = new CwdCommandHandler();
|
||||||
|
*
|
||||||
|
* SimpleCompositeCommandHandler simpleCompositeCommandHandler = new SimpleCompositeCommandHandler();
|
||||||
|
* simpleCompositeCommandHandler.addCommandHandler(commandHandler1);
|
||||||
|
* simpleCompositeCommandHandler.addCommandHandler(commandHandler2);
|
||||||
|
*
|
||||||
|
* stubFtpServer.setCommandHandler("CWD", simpleCompositeCommandHandler);
|
||||||
|
* </code></pre>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class SimpleCompositeCommandHandler implements CommandHandler, ReplyTextBundleAware {
|
||||||
|
|
||||||
|
private List commandHandlers = new ArrayList();
|
||||||
|
private int invocationIndex = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a CommandHandler to the internal list of handlers.
|
||||||
|
*
|
||||||
|
* @param commandHandler - the CommandHandler
|
||||||
|
*
|
||||||
|
* @throws AssertFailedException - if the commandHandler is null
|
||||||
|
*/
|
||||||
|
public void addCommandHandler(CommandHandler commandHandler) {
|
||||||
|
Assert.notNull(commandHandler, "commandHandler");
|
||||||
|
commandHandlers.add(commandHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the List of CommandHandlers to which to delegate. This replaces any CommandHandlers that
|
||||||
|
* have been defined previously.
|
||||||
|
* @param commandHandlers - the complete List of CommandHandlers to which invocations are delegated
|
||||||
|
*/
|
||||||
|
public void setCommandHandlers(List commandHandlers) {
|
||||||
|
Assert.notNull(commandHandlers, "commandHandlers");
|
||||||
|
this.commandHandlers = new ArrayList(commandHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the CommandHandler corresponding to the specified invocation index. In other words, return
|
||||||
|
* the CommandHandler instance to which the Nth {@link #handleCommand(Command, Session)} has been or will
|
||||||
|
* be delegated (where N=index).
|
||||||
|
* @param index - the index of the desired invocation (zero-based).
|
||||||
|
* @return the CommandHandler
|
||||||
|
*
|
||||||
|
* @throws AssertFailedException - if no CommandHandler is defined for the index or the index is not valid
|
||||||
|
*/
|
||||||
|
public CommandHandler getCommandHandler(int index) {
|
||||||
|
Assert.isTrue(index < commandHandlers.size(), "No CommandHandler defined for index " + index);
|
||||||
|
Assert.isTrue(index >= 0, "The index cannot be less than zero: " + index);
|
||||||
|
return (CommandHandler) commandHandlers.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see CommandHandler#handleCommand(Command, Session)
|
||||||
|
*/
|
||||||
|
public void handleCommand(Command command, Session session) throws Exception {
|
||||||
|
Assert.notNull(command, "command");
|
||||||
|
Assert.notNull(session, "session");
|
||||||
|
Assert.isTrue(commandHandlers.size() > invocationIndex, "No CommandHandler defined for invocation #" + invocationIndex);
|
||||||
|
|
||||||
|
CommandHandler commandHandler = (CommandHandler) commandHandlers.get(invocationIndex);
|
||||||
|
invocationIndex++;
|
||||||
|
commandHandler.handleCommand(command, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns null. This is a composite, and has no reply text bundle.
|
||||||
|
*
|
||||||
|
* @see ReplyTextBundleAware#getReplyTextBundle()
|
||||||
|
*/
|
||||||
|
public ResourceBundle getReplyTextBundle() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call <code>setReplyTextBundle()</code> on each of the command handlers within the internal list.
|
||||||
|
*
|
||||||
|
* @see ReplyTextBundleAware#setReplyTextBundle(ResourceBundle)
|
||||||
|
*/
|
||||||
|
public void setReplyTextBundle(ResourceBundle replyTextBundle) {
|
||||||
|
for (Iterator iter = commandHandlers.iterator(); iter.hasNext();) {
|
||||||
|
CommandHandler commandHandler = (CommandHandler) iter.next();
|
||||||
|
ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, replyTextBundle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler that sends back the configured reply code and text. You can customize the
|
||||||
|
* returned reply code by setting the required <code>replyCode</code> property. If only the
|
||||||
|
* <code>replyCode</code> property is set, then the default reply text corresponding to that
|
||||||
|
* reply code is used in the response. You can optionally configure the reply text by setting
|
||||||
|
* the <code>replyMessageKey</code> or <code>replyText</code> property.
|
||||||
|
*
|
||||||
|
* <p>Each invocation record stored by this CommandHandler contains no data elements.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class StaticReplyCommandHandler extends AbstractStaticReplyCommandHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new uninitialized instance
|
||||||
|
*/
|
||||||
|
public StaticReplyCommandHandler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance with the specified replyCode
|
||||||
|
* @param replyCode - the replyCode to use
|
||||||
|
* @throws AssertFailedException - if the replyCode is null
|
||||||
|
*/
|
||||||
|
public StaticReplyCommandHandler(int replyCode) {
|
||||||
|
setReplyCode(replyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance with the specified replyCode and replyText
|
||||||
|
* @param replyCode - the replyCode to use
|
||||||
|
* @param replyText - the replyText
|
||||||
|
* @throws AssertFailedException - if the replyCode is null
|
||||||
|
*/
|
||||||
|
public StaticReplyCommandHandler(int replyCode, String replyText) {
|
||||||
|
setReplyCode(replyCode);
|
||||||
|
setReplyText(replyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see AbstractTrackingCommandHandler#handleCommand(Command, Session, InvocationRecord)
|
||||||
|
*/
|
||||||
|
public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {
|
||||||
|
sendReply(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.command;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler that encapsulates the sending of the reply when a requested command is not
|
||||||
|
* recognized/supported. Send back a reply code of 502, indicating command not implemented.
|
||||||
|
*
|
||||||
|
* <p>Note that this is a "special" CommandHandler, in that it handles any unrecognized command,
|
||||||
|
* rather than an explicit FTP command.
|
||||||
|
*
|
||||||
|
* <p>Each invocation record stored by this CommandHandler contains no data elements.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class UnsupportedCommandHandler extends AbstractStaticReplyCommandHandler implements CommandHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor. Initiate the replyCode.
|
||||||
|
*/
|
||||||
|
public UnsupportedCommandHandler() {
|
||||||
|
setReplyCode(ReplyCodes.COMMAND_NOT_SUPPORTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see AbstractTrackingCommandHandler#handleCommand(Command, Session, InvocationRecord)
|
||||||
|
*/
|
||||||
|
public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {
|
||||||
|
LOG.log(Level.WARNING, "No CommandHandler is defined for command [" + command.getName() + "]");
|
||||||
|
sendReply(session, command.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,394 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.server;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import org.xbib.files.ftp.mock.core.MockFtpServerException;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.CommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.DefaultSession;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.socket.DefaultServerSocketFactory;
|
||||||
|
import org.xbib.files.ftp.mock.core.socket.ServerSocketFactory;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyTextBundleAware;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
import org.xbib.files.ftp.mock.fake.FakeFtpServer;
|
||||||
|
import org.xbib.files.ftp.mock.stub.StubFtpServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the abstract superclass for "mock" implementations of an FTP Server,
|
||||||
|
* suitable for testing FTP client code or standing in for a live FTP server. It supports
|
||||||
|
* the main FTP commands by defining handlers for each of the corresponding low-level FTP
|
||||||
|
* server commands (e.g. RETR, DELE, LIST). These handlers implement the {@link CommandHandler}
|
||||||
|
* interface.
|
||||||
|
*
|
||||||
|
* <p>By default, mock FTP Servers bind to the server control port of 21. You can use a different server control
|
||||||
|
* port by setting the <code>serverControlPort</code> property. If you specify a value of <code>0</code>,
|
||||||
|
* then a free port number will be chosen automatically; call <code>getServerControlPort()</code> AFTER
|
||||||
|
* <code>start()</code> has been called to determine the actual port number being used. Using a non-default
|
||||||
|
* port number is usually necessary when running on Unix or some other system where that port number is
|
||||||
|
* already in use or cannot be bound from a user process.
|
||||||
|
*
|
||||||
|
* <p><b>Command Handlers</b></p>
|
||||||
|
* You can set the existing {@link CommandHandler} defined for an FTP server command
|
||||||
|
* by calling the {@link #setCommandHandler(String, CommandHandler)} method, passing
|
||||||
|
* in the FTP server command name and {@link CommandHandler} instance.
|
||||||
|
* You can also replace multiple command handlers at once by using the {@link #setCommandHandlers(Map)}
|
||||||
|
* method. That is especially useful when configuring the server through the <b>Spring Framework</b>.
|
||||||
|
*
|
||||||
|
* <p>You can retrieve the existing {@link CommandHandler} defined for an FTP server command by
|
||||||
|
* calling the {@link #getCommandHandler(String)} method, passing in the FTP server command name.
|
||||||
|
*
|
||||||
|
* <p><b>FTP Command Reply Text ResourceBundle</b></p>
|
||||||
|
* The default text asociated with each FTP command reply code is contained within the
|
||||||
|
* "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a
|
||||||
|
* locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of
|
||||||
|
* the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can
|
||||||
|
* completely replace the ResourceBundle file by calling the calling the
|
||||||
|
* {@link #setReplyTextBaseName(String)} method.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
* @see FakeFtpServer
|
||||||
|
* @see StubFtpServer
|
||||||
|
*/
|
||||||
|
public abstract class AbstractFtpServer implements Runnable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default basename for reply text ResourceBundle
|
||||||
|
*/
|
||||||
|
public static final String REPLY_TEXT_BASENAME = "ReplyText";
|
||||||
|
private static final int DEFAULT_SERVER_CONTROL_PORT = 21;
|
||||||
|
|
||||||
|
protected Logger LOG = Logger.getLogger(getClass().getName());
|
||||||
|
|
||||||
|
// Simple value object that holds the socket and thread for a single session
|
||||||
|
private static class SessionInfo {
|
||||||
|
Socket socket;
|
||||||
|
Thread thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();
|
||||||
|
private ServerSocket serverSocket = null;
|
||||||
|
private ResourceBundle replyTextBundle;
|
||||||
|
private volatile boolean terminate = false;
|
||||||
|
private Map commandHandlers;
|
||||||
|
private Thread serverThread;
|
||||||
|
private int serverControlPort = DEFAULT_SERVER_CONTROL_PORT;
|
||||||
|
private final Object startLock = new Object();
|
||||||
|
|
||||||
|
// Map of Session -> SessionInfo
|
||||||
|
private Map sessions = new HashMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance. Initialize the default command handlers and
|
||||||
|
* reply text ResourceBundle.
|
||||||
|
*/
|
||||||
|
public AbstractFtpServer() {
|
||||||
|
replyTextBundle = ResourceBundle.getBundle(REPLY_TEXT_BASENAME);
|
||||||
|
commandHandlers = new HashMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new Thread for this server instance
|
||||||
|
*/
|
||||||
|
public void start() {
|
||||||
|
serverThread = new Thread(this);
|
||||||
|
|
||||||
|
synchronized (startLock) {
|
||||||
|
try {
|
||||||
|
// Start here in case server thread runs faster than main thread.
|
||||||
|
// See https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=1925590&group_id=208647
|
||||||
|
serverThread.start();
|
||||||
|
|
||||||
|
// Wait until the server thread is initialized
|
||||||
|
startLock.wait();
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logic for the server thread
|
||||||
|
*
|
||||||
|
* @see Runnable#run()
|
||||||
|
*/
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
LOG.info("Starting the server on port " + serverControlPort);
|
||||||
|
serverSocket = serverSocketFactory.createServerSocket(serverControlPort);
|
||||||
|
if (serverControlPort == 0) {
|
||||||
|
this.serverControlPort = serverSocket.getLocalPort();
|
||||||
|
LOG.info("Actual server port is " + this.serverControlPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify to allow the start() method to finish and return
|
||||||
|
synchronized (startLock) {
|
||||||
|
startLock.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!terminate) {
|
||||||
|
try {
|
||||||
|
cleanupClosedSessions();
|
||||||
|
Socket clientSocket = serverSocket.accept();
|
||||||
|
LOG.info("Connection accepted from host " + clientSocket.getInetAddress());
|
||||||
|
|
||||||
|
Session session = createSession(clientSocket);
|
||||||
|
Thread sessionThread = new Thread(session);
|
||||||
|
sessionThread.start();
|
||||||
|
|
||||||
|
SessionInfo sessionInfo = new SessionInfo();
|
||||||
|
sessionInfo.socket = clientSocket;
|
||||||
|
sessionInfo.thread = sessionThread;
|
||||||
|
sessions.put(session, sessionInfo);
|
||||||
|
}
|
||||||
|
catch (SocketException e) {
|
||||||
|
LOG.log(Level.FINE, "Socket exception: " + e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error", e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
|
||||||
|
LOG.log(Level.FINE, "Cleaning up server...");
|
||||||
|
|
||||||
|
// Ensure that the start() method is not still blocked
|
||||||
|
synchronized (startLock) {
|
||||||
|
startLock.notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (serverSocket != null) {
|
||||||
|
serverSocket.close();
|
||||||
|
}
|
||||||
|
closeSessions();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error cleaning up server", e);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error cleaning up server", e);
|
||||||
|
}
|
||||||
|
LOG.info("Server stopped.");
|
||||||
|
terminate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop this server instance and wait for it to terminate.
|
||||||
|
*/
|
||||||
|
public void stop() {
|
||||||
|
|
||||||
|
LOG.log(Level.FINE, "Stopping the server...");
|
||||||
|
terminate = true;
|
||||||
|
|
||||||
|
if (serverSocket != null) {
|
||||||
|
try {
|
||||||
|
serverSocket.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (serverThread != null) {
|
||||||
|
serverThread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the CommandHandler defined for the specified command name
|
||||||
|
*
|
||||||
|
* @param name - the command name
|
||||||
|
* @return the CommandHandler defined for name
|
||||||
|
*/
|
||||||
|
public CommandHandler getCommandHandler(String name) {
|
||||||
|
return (CommandHandler) commandHandlers.get(Command.normalizeName(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the default CommandHandlers with those in the specified Map of
|
||||||
|
* commandName:CommandHandler. This will only override the default CommandHandlers
|
||||||
|
* for the keys in <code>commandHandlerMapping</code>. All other default CommandHandler
|
||||||
|
* mappings remain unchanged.
|
||||||
|
*
|
||||||
|
* @param commandHandlerMapping - the Map of commandName:CommandHandler; these override the defaults
|
||||||
|
* @throws AssertFailedException
|
||||||
|
* - if the commandHandlerMapping is null
|
||||||
|
*/
|
||||||
|
public void setCommandHandlers(Map commandHandlerMapping) {
|
||||||
|
Assert.notNull(commandHandlerMapping, "commandHandlers");
|
||||||
|
for (Iterator iter = commandHandlerMapping.keySet().iterator(); iter.hasNext();) {
|
||||||
|
String commandName = (String) iter.next();
|
||||||
|
setCommandHandler(commandName, (CommandHandler) commandHandlerMapping.get(commandName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the CommandHandler for the specified command name. If the CommandHandler implements
|
||||||
|
* the {@link ReplyTextBundleAware} interface and its <code>replyTextBundle</code> attribute
|
||||||
|
* is null, then set its <code>replyTextBundle</code> to the <code>replyTextBundle</code> of
|
||||||
|
* this StubFtpServer.
|
||||||
|
*
|
||||||
|
* @param commandName - the command name to which the CommandHandler will be associated
|
||||||
|
* @param commandHandler - the CommandHandler
|
||||||
|
* @throws AssertFailedException
|
||||||
|
* - if the commandName or commandHandler is null
|
||||||
|
*/
|
||||||
|
public void setCommandHandler(String commandName, CommandHandler commandHandler) {
|
||||||
|
Assert.notNull(commandName, "commandName");
|
||||||
|
Assert.notNull(commandHandler, "commandHandler");
|
||||||
|
commandHandlers.put(Command.normalizeName(commandName), commandHandler);
|
||||||
|
initializeCommandHandler(commandHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the reply text ResourceBundle to a new ResourceBundle with the specified base name,
|
||||||
|
* accessible on the CLASSPATH. See {@link ResourceBundle#getBundle(String)}.
|
||||||
|
*
|
||||||
|
* @param baseName - the base name of the resource bundle, a fully qualified class name
|
||||||
|
*/
|
||||||
|
public void setReplyTextBaseName(String baseName) {
|
||||||
|
replyTextBundle = ResourceBundle.getBundle(baseName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the ReplyText ResourceBundle. Set the bundle through the {@link #setReplyTextBaseName(String)} method.
|
||||||
|
*
|
||||||
|
* @return the reply text ResourceBundle
|
||||||
|
*/
|
||||||
|
public ResourceBundle getReplyTextBundle() {
|
||||||
|
return replyTextBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the port number to which the server control connection socket will bind. The default value is 21.
|
||||||
|
*
|
||||||
|
* @param serverControlPort - the port number for the server control connection ServerSocket
|
||||||
|
*/
|
||||||
|
public void setServerControlPort(int serverControlPort) {
|
||||||
|
this.serverControlPort = serverControlPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the port number to which the server control connection socket will bind. The default value is 21.
|
||||||
|
*
|
||||||
|
* @return the port number for the server control connection ServerSocket
|
||||||
|
*/
|
||||||
|
public int getServerControlPort() {
|
||||||
|
return serverControlPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if this server is fully shutdown -- i.e., there is no active (alive) threads and
|
||||||
|
* all sockets are closed. This method is intended for testing only.
|
||||||
|
*
|
||||||
|
* @return true if this server is fully shutdown
|
||||||
|
*/
|
||||||
|
public boolean isShutdown() {
|
||||||
|
boolean shutdown = !serverThread.isAlive() && serverSocket.isClosed();
|
||||||
|
|
||||||
|
for (Iterator iter = sessions.values().iterator(); iter.hasNext();) {
|
||||||
|
SessionInfo sessionInfo = (SessionInfo) iter.next();
|
||||||
|
shutdown = shutdown && sessionInfo.socket.isClosed() && !sessionInfo.thread.isAlive();
|
||||||
|
}
|
||||||
|
return shutdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if this server has started -- i.e., there is an active (alive) server threads
|
||||||
|
* and non-null server socket. This method is intended for testing only.
|
||||||
|
*
|
||||||
|
* @return true if this server has started
|
||||||
|
*/
|
||||||
|
public boolean isStarted() {
|
||||||
|
return serverThread != null && serverThread.isAlive() && serverSocket != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// Internal Helper Methods
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Session instance for the specified client Socket
|
||||||
|
*
|
||||||
|
* @param clientSocket - the Socket associated with the client
|
||||||
|
* @return a Session
|
||||||
|
*/
|
||||||
|
public Session createSession(Socket clientSocket) {
|
||||||
|
return new DefaultSession(clientSocket, commandHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupClosedSessions() {
|
||||||
|
Iterator iter = sessions.keySet().iterator();
|
||||||
|
while(iter.hasNext()) {
|
||||||
|
Session session = (Session) iter.next();
|
||||||
|
if (session.isClosed()) {
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For testing
|
||||||
|
public int numberOfSessions() {
|
||||||
|
return sessions.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeSessions() throws InterruptedException, IOException {
|
||||||
|
for (Iterator iter = sessions.entrySet().iterator(); iter.hasNext();) {
|
||||||
|
Map.Entry entry = (Map.Entry) iter.next();
|
||||||
|
Session session = (Session) entry.getKey();
|
||||||
|
SessionInfo sessionInfo = (SessionInfo) entry.getValue();
|
||||||
|
session.close();
|
||||||
|
sessionInfo.thread.join(500L);
|
||||||
|
Socket sessionSocket = sessionInfo.socket;
|
||||||
|
if (sessionSocket != null) {
|
||||||
|
sessionSocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------------
|
||||||
|
// Abstract method declarations
|
||||||
|
//------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a CommandHandler that has been registered to this server. What "initialization"
|
||||||
|
* means is dependent on the subclass implementation.
|
||||||
|
*
|
||||||
|
* @param commandHandler - the CommandHandler to initialize
|
||||||
|
*/
|
||||||
|
protected abstract void initializeCommandHandler(CommandHandler commandHandler);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,498 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.session;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import org.xbib.files.ftp.mock.core.MockFtpServerException;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.CommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.CommandNames;
|
||||||
|
import org.xbib.files.ftp.mock.core.socket.DefaultServerSocketFactory;
|
||||||
|
import org.xbib.files.ftp.mock.core.socket.DefaultSocketFactory;
|
||||||
|
import org.xbib.files.ftp.mock.core.socket.ServerSocketFactory;
|
||||||
|
import org.xbib.files.ftp.mock.core.socket.SocketFactory;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of the {@link Session} interface.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class DefaultSession implements Session {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(DefaultSession.class.getName());
|
||||||
|
private static final String END_OF_LINE = "\r\n";
|
||||||
|
public static final int DEFAULT_CLIENT_DATA_PORT = 21;
|
||||||
|
|
||||||
|
public SocketFactory socketFactory = new DefaultSocketFactory();
|
||||||
|
public ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();
|
||||||
|
|
||||||
|
public BufferedReader controlConnectionReader; // non-private for testing
|
||||||
|
private Writer controlConnectionWriter;
|
||||||
|
private Socket controlSocket;
|
||||||
|
private Socket dataSocket;
|
||||||
|
public ServerSocket passiveModeDataSocket; // non-private for testing
|
||||||
|
private InputStream dataInputStream;
|
||||||
|
private OutputStream dataOutputStream;
|
||||||
|
private Map commandHandlers;
|
||||||
|
private int clientDataPort = DEFAULT_CLIENT_DATA_PORT;
|
||||||
|
private InetAddress clientHost;
|
||||||
|
private InetAddress serverHost;
|
||||||
|
private Map attributes = new HashMap();
|
||||||
|
private volatile boolean terminate = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new initialized instance
|
||||||
|
*
|
||||||
|
* @param controlSocket - the control connection socket
|
||||||
|
* @param commandHandlers - the Map of command name : CommandHandler. It is assumed that the
|
||||||
|
* command names are all normalized to upper case. See {@link Command#normalizeName(String)}.
|
||||||
|
*/
|
||||||
|
public DefaultSession(Socket controlSocket, Map commandHandlers) {
|
||||||
|
Assert.notNull(controlSocket, "controlSocket");
|
||||||
|
Assert.notNull(commandHandlers, "commandHandlers");
|
||||||
|
|
||||||
|
this.controlSocket = controlSocket;
|
||||||
|
this.commandHandlers = commandHandlers;
|
||||||
|
this.serverHost = controlSocket.getLocalAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the InetAddress representing the client host for this session
|
||||||
|
*
|
||||||
|
* @return the client host
|
||||||
|
* @see Session#getClientHost()
|
||||||
|
*/
|
||||||
|
public InetAddress getClientHost() {
|
||||||
|
return controlSocket.getInetAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the InetAddress representing the server host for this session
|
||||||
|
*
|
||||||
|
* @return the server host
|
||||||
|
* @see Session#getServerHost()
|
||||||
|
*/
|
||||||
|
public InetAddress getServerHost() {
|
||||||
|
return serverHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the specified reply code and text across the control connection.
|
||||||
|
* The reply text is trimmed before being sent.
|
||||||
|
*
|
||||||
|
* @param code - the reply code
|
||||||
|
* @param text - the reply text to send; may be null
|
||||||
|
*/
|
||||||
|
public void sendReply(int code, String text) {
|
||||||
|
assertValidReplyCode(code);
|
||||||
|
|
||||||
|
StringBuffer buffer = new StringBuffer(Integer.toString(code));
|
||||||
|
|
||||||
|
if (text != null && text.length() > 0) {
|
||||||
|
String replyText = text.trim();
|
||||||
|
if (replyText.indexOf("\n") != -1) {
|
||||||
|
int lastIndex = replyText.lastIndexOf("\n");
|
||||||
|
buffer.append("-");
|
||||||
|
for (int i = 0; i < replyText.length(); i++) {
|
||||||
|
char c = replyText.charAt(i);
|
||||||
|
buffer.append(c);
|
||||||
|
if (i == lastIndex) {
|
||||||
|
buffer.append(Integer.toString(code));
|
||||||
|
buffer.append(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer.append(" ");
|
||||||
|
buffer.append(replyText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG.log(Level.FINE, "Sending Reply [" + buffer.toString() + "]");
|
||||||
|
writeLineToControlConnection(buffer.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Session#openDataConnection()
|
||||||
|
*/
|
||||||
|
public void openDataConnection() {
|
||||||
|
try {
|
||||||
|
if (passiveModeDataSocket != null) {
|
||||||
|
LOG.log(Level.FINE, "Waiting for (passive mode) client connection from client host [" + clientHost
|
||||||
|
+ "] on port " + passiveModeDataSocket.getLocalPort());
|
||||||
|
// TODO set socket timeout
|
||||||
|
try {
|
||||||
|
dataSocket = passiveModeDataSocket.accept();
|
||||||
|
LOG.log(Level.FINE, "Successful (passive mode) client connection to port "
|
||||||
|
+ passiveModeDataSocket.getLocalPort());
|
||||||
|
}
|
||||||
|
catch (SocketTimeoutException e) {
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Assert.notNull(clientHost, "clientHost");
|
||||||
|
LOG.log(Level.FINE, "Connecting to client host [" + clientHost + "] on data port [" + clientDataPort
|
||||||
|
+ "]");
|
||||||
|
dataSocket = socketFactory.createSocket(clientHost, clientDataPort);
|
||||||
|
}
|
||||||
|
dataOutputStream = dataSocket.getOutputStream();
|
||||||
|
dataInputStream = dataSocket.getInputStream();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to passive mode
|
||||||
|
*
|
||||||
|
* @return the local port to be connected to by clients for data transfers
|
||||||
|
* @see Session#switchToPassiveMode()
|
||||||
|
*/
|
||||||
|
public int switchToPassiveMode() {
|
||||||
|
try {
|
||||||
|
passiveModeDataSocket = serverSocketFactory.createServerSocket(0);
|
||||||
|
return passiveModeDataSocket.getLocalPort();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new MockFtpServerException("Error opening passive mode server data socket", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Session#closeDataConnection()
|
||||||
|
*/
|
||||||
|
public void closeDataConnection() {
|
||||||
|
try {
|
||||||
|
LOG.log(Level.FINE, "Flushing and closing client data socket");
|
||||||
|
dataOutputStream.flush();
|
||||||
|
dataOutputStream.close();
|
||||||
|
dataInputStream.close();
|
||||||
|
dataSocket.close();
|
||||||
|
|
||||||
|
if (passiveModeDataSocket != null) {
|
||||||
|
passiveModeDataSocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error closing client data socket", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a single line to the control connection, appending a newline
|
||||||
|
*
|
||||||
|
* @param line - the line to write
|
||||||
|
*/
|
||||||
|
private void writeLineToControlConnection(String line) {
|
||||||
|
try {
|
||||||
|
controlConnectionWriter.write(line + END_OF_LINE);
|
||||||
|
controlConnectionWriter.flush();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error writing to control connection", e);
|
||||||
|
throw new MockFtpServerException("Error writing to control connection", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Session#close()
|
||||||
|
*/
|
||||||
|
public void close() {
|
||||||
|
LOG.log(Level.FINE, "close()");
|
||||||
|
terminate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isClosed() {
|
||||||
|
return terminate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Session#sendData(byte[], int)
|
||||||
|
*/
|
||||||
|
public void sendData(byte[] data, int numBytes) {
|
||||||
|
Assert.notNull(data, "data");
|
||||||
|
try {
|
||||||
|
dataOutputStream.write(data, 0, numBytes);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Session#readData()
|
||||||
|
*/
|
||||||
|
public byte[] readData() {
|
||||||
|
return readData(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Session#readData()
|
||||||
|
*/
|
||||||
|
public byte[] readData(int numBytes) {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
int totalBytesReadSoFar = 0;
|
||||||
|
boolean reading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (reading) {
|
||||||
|
int numBytesLeft = numBytes - totalBytesReadSoFar;
|
||||||
|
int numBytesToReadThisTime = Math.min(data.length, numBytesLeft);
|
||||||
|
int numBytesRead = dataInputStream.read(data, 0, numBytesToReadThisTime);
|
||||||
|
reading = numBytesRead != -1;
|
||||||
|
if (reading) {
|
||||||
|
bytes.write(data, 0, numBytesRead);
|
||||||
|
totalBytesReadSoFar += numBytesRead;
|
||||||
|
reading = totalBytesReadSoFar < numBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for and read the command sent from the client on the control connection.
|
||||||
|
*
|
||||||
|
* @return the Command sent from the client; may be null if the session has been closed
|
||||||
|
* <p/>
|
||||||
|
* Package-private to enable testing
|
||||||
|
*/
|
||||||
|
public Command readCommand() {
|
||||||
|
final long socketReadIntervalMilliseconds = 20L;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
if (terminate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Don't block; only read command when it is available
|
||||||
|
if (controlConnectionReader.ready()) {
|
||||||
|
String command = controlConnectionReader.readLine();
|
||||||
|
LOG.info("Received command: [" + command + "]");
|
||||||
|
if (command == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseCommand(command);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(socketReadIntervalMilliseconds);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Read failed", e);
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the command String into a Command object
|
||||||
|
*
|
||||||
|
* @param commandString - the command String
|
||||||
|
* @return the Command object parsed from the command String
|
||||||
|
*/
|
||||||
|
public Command parseCommand(String commandString) {
|
||||||
|
Assert.notNullOrEmpty(commandString, "commandString");
|
||||||
|
|
||||||
|
List parameters = new ArrayList();
|
||||||
|
String name;
|
||||||
|
|
||||||
|
int indexOfFirstSpace = commandString.indexOf(" ");
|
||||||
|
if (indexOfFirstSpace != -1) {
|
||||||
|
name = commandString.substring(0, indexOfFirstSpace);
|
||||||
|
StringTokenizer tokenizer = new StringTokenizer(commandString.substring(indexOfFirstSpace + 1),
|
||||||
|
",");
|
||||||
|
while (tokenizer.hasMoreTokens()) {
|
||||||
|
parameters.add(tokenizer.nextToken());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
name = commandString;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parametersArray = new String[parameters.size()];
|
||||||
|
return new Command(name, (String[]) parameters.toArray(parametersArray));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Session#setClientDataHost(InetAddress)
|
||||||
|
*/
|
||||||
|
public void setClientDataHost(InetAddress clientHost) {
|
||||||
|
this.clientHost = clientHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Session#setClientDataPort(int)
|
||||||
|
*/
|
||||||
|
public void setClientDataPort(int dataPort) {
|
||||||
|
this.clientDataPort = dataPort;
|
||||||
|
|
||||||
|
// Clear out any passive data connection mode information
|
||||||
|
if (passiveModeDataSocket != null) {
|
||||||
|
try {
|
||||||
|
this.passiveModeDataSocket.close();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
passiveModeDataSocket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Runnable#run()
|
||||||
|
*/
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
InputStream inputStream = controlSocket.getInputStream();
|
||||||
|
OutputStream outputStream = controlSocket.getOutputStream();
|
||||||
|
controlConnectionReader = new BufferedReader(new InputStreamReader(inputStream));
|
||||||
|
controlConnectionWriter = new PrintWriter(outputStream, true);
|
||||||
|
|
||||||
|
LOG.log(Level.FINE, "Starting the session...");
|
||||||
|
|
||||||
|
CommandHandler connectCommandHandler = (CommandHandler) commandHandlers.get(CommandNames.CONNECT);
|
||||||
|
connectCommandHandler.handleCommand(new Command(CommandNames.CONNECT, new String[0]), this);
|
||||||
|
|
||||||
|
while (!terminate) {
|
||||||
|
readAndProcessCommand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error:", e);
|
||||||
|
throw new MockFtpServerException(e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
LOG.log(Level.FINE, "Cleaning up the session");
|
||||||
|
try {
|
||||||
|
controlConnectionReader.close();
|
||||||
|
controlConnectionWriter.close();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error:", e);
|
||||||
|
}
|
||||||
|
LOG.log(Level.FINE, "Session stopped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read and process the next command from the control connection
|
||||||
|
*
|
||||||
|
* @throws Exception - if any error occurs
|
||||||
|
*/
|
||||||
|
private void readAndProcessCommand() throws Exception {
|
||||||
|
|
||||||
|
Command command = readCommand();
|
||||||
|
if (command != null) {
|
||||||
|
String normalizedCommandName = Command.normalizeName(command.getName());
|
||||||
|
CommandHandler commandHandler = (CommandHandler) commandHandlers.get(normalizedCommandName);
|
||||||
|
|
||||||
|
if (commandHandler == null) {
|
||||||
|
commandHandler = (CommandHandler) commandHandlers.get(CommandNames.UNSUPPORTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.notNull(commandHandler, "CommandHandler for command [" + normalizedCommandName + "]");
|
||||||
|
commandHandler.handleCommand(command, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the specified number is a valid reply code
|
||||||
|
*
|
||||||
|
* @param replyCode - the reply code to check
|
||||||
|
*/
|
||||||
|
private void assertValidReplyCode(int replyCode) {
|
||||||
|
Assert.isTrue(replyCode > 0, "The number [" + replyCode + "] is not a valid reply code");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the attribute value for the specified name. Return null if no attribute value
|
||||||
|
* exists for that name or if the attribute value is null.
|
||||||
|
*
|
||||||
|
* @param name - the attribute name; may not be null
|
||||||
|
* @return the value of the attribute stored under name; may be null
|
||||||
|
* @see Session#getAttribute(String)
|
||||||
|
*/
|
||||||
|
public Object getAttribute(String name) {
|
||||||
|
Assert.notNull(name, "name");
|
||||||
|
return attributes.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the value under the specified attribute name.
|
||||||
|
*
|
||||||
|
* @param name - the attribute name; may not be null
|
||||||
|
* @param value - the attribute value; may be null
|
||||||
|
* @see Session#setAttribute(String, Object)
|
||||||
|
*/
|
||||||
|
public void setAttribute(String name, Object value) {
|
||||||
|
Assert.notNull(name, "name");
|
||||||
|
attributes.put(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Set of names under which attributes have been stored on this session.
|
||||||
|
* Returns an empty Set if no attribute values are stored.
|
||||||
|
*
|
||||||
|
* @return the Set of attribute names
|
||||||
|
* @see Session#getAttributeNames()
|
||||||
|
*/
|
||||||
|
public Set getAttributeNames() {
|
||||||
|
return attributes.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the attribute value for the specified name. Do nothing if no attribute
|
||||||
|
* value is stored for the specified name.
|
||||||
|
*
|
||||||
|
* @param name - the attribute name; may not be null
|
||||||
|
* @throws AssertFailedException - if name is null
|
||||||
|
* @see Session#removeAttribute(String)
|
||||||
|
*/
|
||||||
|
public void removeAttribute(String name) {
|
||||||
|
Assert.notNull(name, "name");
|
||||||
|
attributes.remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.session;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an FTP session state and behavior
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public interface Session extends Runnable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the session, closing the underlying sockets
|
||||||
|
*/
|
||||||
|
public void close();
|
||||||
|
|
||||||
|
public boolean isClosed();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the specified reply code and text across the control connection.
|
||||||
|
*
|
||||||
|
* @param replyCode - the reply code
|
||||||
|
* @param replyText - the reply text to send; may be null
|
||||||
|
*/
|
||||||
|
public void sendReply(int replyCode, String replyText);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the data connection, attaching to the predefined port number on the client
|
||||||
|
*/
|
||||||
|
public void openDataConnection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the data connection
|
||||||
|
*/
|
||||||
|
public void closeDataConnection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to passive mode
|
||||||
|
* @return the local port to be connected to by clients for data transfers
|
||||||
|
*/
|
||||||
|
public int switchToPassiveMode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the specified data using the data connection
|
||||||
|
*
|
||||||
|
* @param data - the data to write
|
||||||
|
* @param numBytes - the number of bytes from data to send
|
||||||
|
*/
|
||||||
|
public void sendData(byte[] data, int numBytes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read data from the client across the data connection
|
||||||
|
*
|
||||||
|
* @return the data that was read
|
||||||
|
*/
|
||||||
|
public byte[] readData();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read and return (up to) numBytes of data from the client across the data connection
|
||||||
|
* @param numBytes - the maximum number of bytes to read
|
||||||
|
* @return the data that was read; the byte[] will be up to numBytes bytes long
|
||||||
|
*/
|
||||||
|
public byte[] readData(int numBytes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the InetAddress representing the client host for this session
|
||||||
|
* @return the client host
|
||||||
|
*/
|
||||||
|
public InetAddress getClientHost();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the InetAddress representing the server host for this session
|
||||||
|
* @return the server host
|
||||||
|
*/
|
||||||
|
public InetAddress getServerHost();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param clientHost - the client host for the data connection
|
||||||
|
*/
|
||||||
|
public void setClientDataHost(InetAddress clientHost);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param clientDataPort - the port number on the client side for the data connection
|
||||||
|
*/
|
||||||
|
public void setClientDataPort(int clientDataPort);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the attribute value for the specified name. Return null if no attribute value
|
||||||
|
* exists for that name or if the attribute value is null.
|
||||||
|
* @param name - the attribute name; may not be null
|
||||||
|
* @return the value of the attribute stored under name; may be null
|
||||||
|
* @throws AssertFailedException - if name is null
|
||||||
|
*/
|
||||||
|
public Object getAttribute(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the value under the specified attribute name.
|
||||||
|
* @param name - the attribute name; may not be null
|
||||||
|
* @param value - the attribute value; may be null
|
||||||
|
* @throws AssertFailedException - if name is null
|
||||||
|
*/
|
||||||
|
public void setAttribute(String name, Object value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the attribute value for the specified name. Do nothing if no attribute
|
||||||
|
* value is stored for the specified name.
|
||||||
|
* @param name - the attribute name; may not be null
|
||||||
|
* @throws AssertFailedException - if name is null
|
||||||
|
*/
|
||||||
|
public void removeAttribute(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Set of names under which attributes have been stored on this session.
|
||||||
|
* Returns an empty Set if no attribute values are stored.
|
||||||
|
* @return the Set of attribute names
|
||||||
|
*/
|
||||||
|
public Set getAttributeNames();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants for names of properties (attributes) stored in the session.
|
||||||
|
*/
|
||||||
|
public class SessionKeys {
|
||||||
|
|
||||||
|
public static final String USERNAME = "username";
|
||||||
|
public static final String USER_ACCOUNT = "userAccount";
|
||||||
|
public static final String CURRENT_DIRECTORY = "currentDirectory";
|
||||||
|
public static final String RENAME_FROM = "renameFrom";
|
||||||
|
public static final String ACCOUNT_NAME = "accountName";
|
||||||
|
public static final String ASCII_TYPE = "asciiType";
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.socket;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of the {@link ServerSocketFactory}; creates standard {@link ServerSocket} instances.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class DefaultServerSocketFactory implements ServerSocketFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ServerSocket for the specified port.
|
||||||
|
* @param port - the port
|
||||||
|
* @return a new ServerSocket
|
||||||
|
* @throws IOException - if an error occurs
|
||||||
|
|
||||||
|
* @see ServerSocketFactory#createServerSocket(int)
|
||||||
|
*/
|
||||||
|
public ServerSocket createServerSocket(int port) throws IOException {
|
||||||
|
return new ServerSocket(port);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.socket;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of the {@link SocketFactory}; creates standard {@link Socket} instances.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class DefaultSocketFactory implements SocketFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Socket instance for the specified host and port.
|
||||||
|
* @param host - the IP address of the host endpoint to which the socket is connect
|
||||||
|
* @param port - the port number of the enpoint to which the socket is connected
|
||||||
|
* @return a new Socket
|
||||||
|
* @throws IOException - if an error occurs
|
||||||
|
*
|
||||||
|
* @see SocketFactory#createSocket(InetAddress, int)
|
||||||
|
*/
|
||||||
|
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||||
|
return new Socket(host, port);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.socket;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for factory that creates new {@link ServerSocket} instances.
|
||||||
|
* Using this abstraction enables unit testing.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public interface ServerSocketFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ServerSocket for the specified port
|
||||||
|
* @param port - the port
|
||||||
|
* @return a new ServerSocket
|
||||||
|
* @throws IOException - if an error occurs
|
||||||
|
*/
|
||||||
|
public ServerSocket createServerSocket(int port) throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.socket;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for factory that create new {@link Socket} instances.
|
||||||
|
* Using this abstraction enables unit testing.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public interface SocketFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Socket instance for the specified host and port.
|
||||||
|
* @param host - the IP address of the host endpoint to which the socket is connect
|
||||||
|
* @param port - the port number of the enpoint to which the socket is connected
|
||||||
|
* @return a new Socket
|
||||||
|
* @throws IOException - if an error occurs
|
||||||
|
*/
|
||||||
|
public Socket createSocket(InetAddress host, int port) throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.util;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides static helper methods to make runtime assertions. Throws an
|
||||||
|
* <code>AssertFailedException</code> when the assertion fails. All methods are static.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class Assert {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that arg is null. Throw an AssertFailedException if it is not null.
|
||||||
|
* @param arg - the method parameter value
|
||||||
|
* @param argName - the name of the parameter; used in the exception message
|
||||||
|
* @throws AssertFailedException - if arg is not null
|
||||||
|
*/
|
||||||
|
public static void isNull(Object arg, String argName) {
|
||||||
|
if (arg != null) {
|
||||||
|
throw new AssertFailedException("The value for \"" + argName + "\" must be null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that arg is not null. Throw an AssertFailedException if it is null.
|
||||||
|
* @param arg - the method parameter value
|
||||||
|
* @param argName - the name of the parameter; used in the exception message
|
||||||
|
* @throws AssertFailedException - if arg is null
|
||||||
|
*/
|
||||||
|
public static void notNull(Object arg, String argName) {
|
||||||
|
if (arg == null) {
|
||||||
|
throw new AssertFailedException("The value of \"" + argName + "\" is null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that condition is true. Throw an AssertFailedException if it is false.
|
||||||
|
* @param condition - the condition that should be true
|
||||||
|
* @param message - the error message if the condition is false
|
||||||
|
* @throws AssertFailedException - if condition is false
|
||||||
|
*/
|
||||||
|
public static void isTrue(boolean condition, String message) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new AssertFailedException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that condition is false. Throw an AssertFailedException if it is true.
|
||||||
|
* @param condition - the condition that should be false
|
||||||
|
* @param message - the error message if the condition is true
|
||||||
|
* @throws AssertFailedException - if condition is true
|
||||||
|
*/
|
||||||
|
public static void isFalse(boolean condition, String message) {
|
||||||
|
if (condition) {
|
||||||
|
throw new AssertFailedException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the collection is not null or empty. Throw an
|
||||||
|
* AssertFailedException if it is null or empty.
|
||||||
|
* @param collection - the Collection
|
||||||
|
* @param argName - the name of the parameter; used in the exception message
|
||||||
|
* @throws AssertFailedException - if collection is null or empty
|
||||||
|
*/
|
||||||
|
public static void notNullOrEmpty(Collection collection, String argName) {
|
||||||
|
notNull(collection, argName);
|
||||||
|
if (collection.isEmpty()) {
|
||||||
|
throw new AssertFailedException("The \"" + argName + "\" Collection is empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the Map is not null or empty. Throw an AssertFailedException
|
||||||
|
* if it is null or empty.
|
||||||
|
* @param map - the Map
|
||||||
|
* @param argName - the name of the parameter; used in the exception message
|
||||||
|
* @throws AssertFailedException - if map is null or empty
|
||||||
|
*/
|
||||||
|
public static void notNullOrEmpty(Map map, String argName) {
|
||||||
|
notNull(map, argName);
|
||||||
|
if (map.isEmpty()) {
|
||||||
|
throw new AssertFailedException("The \"" + argName + "\" Map is empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the array is not null or empty. Throw an
|
||||||
|
* AssertFailedException if it is null or empty.
|
||||||
|
* @param array - the array
|
||||||
|
* @param argName - the name of the parameter; used in the exception message
|
||||||
|
* @throws AssertFailedException - if array is null or empty
|
||||||
|
*/
|
||||||
|
public static void notNullOrEmpty(Object[] array, String argName) {
|
||||||
|
notNull(array, argName);
|
||||||
|
if (array.length == 0) {
|
||||||
|
throw new AssertFailedException("The \"" + argName + "\" array is empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the String is not null or empty. Throw an
|
||||||
|
* AssertFailedException if it is null or empty.
|
||||||
|
* @param string - the String
|
||||||
|
* @param argName - the name of the parameter; used in the exception message
|
||||||
|
* @throws AssertFailedException - if string is null or empty
|
||||||
|
*/
|
||||||
|
public static void notNullOrEmpty(String string, String argName) {
|
||||||
|
notNull(string, argName);
|
||||||
|
if (string.trim().length() == 0) {
|
||||||
|
throw new AssertFailedException("The \"" + argName + "\" String is empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor. All methods are static
|
||||||
|
*/
|
||||||
|
private Assert() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2007 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception that indicates that a runtime assertion from the
|
||||||
|
* {@link Assert} has failed.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class AssertFailedException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance for the specified message
|
||||||
|
* @param message - the exception message
|
||||||
|
*/
|
||||||
|
public AssertFailedException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2009 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.util;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data-only (transfer) object representing a host (InetAddress) and port number
|
||||||
|
* that together uniquely identify an endpoint for a socket connection.
|
||||||
|
*
|
||||||
|
* This class contains two public properties: host (java.net.InetAddress) and port (int).
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class HostAndPort {
|
||||||
|
public InetAddress host;
|
||||||
|
public int port;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the specified host and port
|
||||||
|
* @param host - the InetAddress host
|
||||||
|
* @param port - the port number
|
||||||
|
*/
|
||||||
|
public HostAndPort(InetAddress host, int port) {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains static I/O-related utility methods.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class IoUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the contents of the InputStream and return as a byte[].
|
||||||
|
*
|
||||||
|
* @param input - the InputStream to read
|
||||||
|
* @return the contents of the InputStream as a byte[]
|
||||||
|
* @throws AssertFailedException - if the InputStream is null
|
||||||
|
* @throws IOException - if an error occurs reading the bytes
|
||||||
|
*/
|
||||||
|
public static byte[] readBytes(InputStream input) throws IOException {
|
||||||
|
Assert.notNull(input, "input");
|
||||||
|
ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
int b = input.read();
|
||||||
|
if (b == -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
outBytes.write(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
input.close();
|
||||||
|
}
|
||||||
|
return outBytes.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to prevent instantiation. All members are static.
|
||||||
|
*/
|
||||||
|
private IoUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains static utility methods related to pattern-matching and regular expressions.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class PatternUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the specified String contains one or more wildcard characters ('?' or '*')
|
||||||
|
*
|
||||||
|
* @param string - the String to check
|
||||||
|
* @return true if the String contains wildcards
|
||||||
|
*/
|
||||||
|
public static boolean containsWildcards(String string) {
|
||||||
|
return string.indexOf("*") != -1 || string.indexOf("?") != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the specified String, optionally containing wildcards (? or *), to a regular expression String
|
||||||
|
*
|
||||||
|
* @param stringWithWildcards - the String to convert, optionally containing wildcards (? or *)
|
||||||
|
* @return an equivalent regex String
|
||||||
|
* @throws AssertionError - if the stringWithWildcards is null
|
||||||
|
*/
|
||||||
|
public static String convertStringWithWildcardsToRegex(String stringWithWildcards) {
|
||||||
|
Assert.notNull(stringWithWildcards, "stringWithWildcards");
|
||||||
|
|
||||||
|
StringBuffer result = new StringBuffer();
|
||||||
|
for (int i = 0; i < stringWithWildcards.length(); i++) {
|
||||||
|
char ch = stringWithWildcards.charAt(i);
|
||||||
|
switch (ch) {
|
||||||
|
case '*':
|
||||||
|
result.append(".*");
|
||||||
|
break;
|
||||||
|
case '?':
|
||||||
|
result.append('.');
|
||||||
|
break;
|
||||||
|
case '$':
|
||||||
|
case '|':
|
||||||
|
case '[':
|
||||||
|
case ']':
|
||||||
|
case '(':
|
||||||
|
case ')':
|
||||||
|
case '.':
|
||||||
|
case ':':
|
||||||
|
case '{':
|
||||||
|
case '}':
|
||||||
|
case '\\':
|
||||||
|
case '^':
|
||||||
|
case '+':
|
||||||
|
result.append('\\');
|
||||||
|
result.append(ch);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result.append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to prevent instantiation. All members are static.
|
||||||
|
*/
|
||||||
|
private PatternUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.util;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.CommandSyntaxException;
|
||||||
|
import org.xbib.files.ftp.mock.core.MockFtpServerException;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for parsing host and port values from command arguments.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public final class PortParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the host address and port number of an extended address. This encoded format is used by
|
||||||
|
* the EPRT FTP command, and supports IPv6.
|
||||||
|
*
|
||||||
|
* <p>The client network address can be in IPv4 format (e.g., "132.235.1.2") or
|
||||||
|
* IPv6 format (e.g., "1080::8:800:200C:417A"). See RFC2428 for more information.
|
||||||
|
*
|
||||||
|
* @param parameter - the single parameter String containing the encoded host and port number
|
||||||
|
* @return the populated HostAndPort object
|
||||||
|
*/
|
||||||
|
public static HostAndPort parseExtendedAddressHostAndPort(String parameter) {
|
||||||
|
if (parameter == null || parameter.length() == 0) {
|
||||||
|
throw new CommandSyntaxException("The parameter string must not be empty or null");
|
||||||
|
}
|
||||||
|
|
||||||
|
String delimiter = parameter.substring(0,1);
|
||||||
|
String[] tokens = parameter.split("\\" + delimiter);
|
||||||
|
|
||||||
|
if (tokens.length < 4) {
|
||||||
|
throw new CommandSyntaxException("Error parsing host and port number [" + parameter + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
int port = Integer.parseInt(tokens[3]);
|
||||||
|
|
||||||
|
InetAddress host;
|
||||||
|
try {
|
||||||
|
host = InetAddress.getByName(tokens[2]);
|
||||||
|
}
|
||||||
|
catch (UnknownHostException e) {
|
||||||
|
throw new CommandSyntaxException("Error parsing host [" + tokens[2] + "]", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HostAndPort(host, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a 32-bit IP address and 16-bit port number from the String[] of FTP command parameters.
|
||||||
|
* This is used by the FTP "PORT" command.
|
||||||
|
*
|
||||||
|
* @param parameters - the String[] of command parameters. It is the concatenation
|
||||||
|
* of a 32-bit internet host address and a 16-bit TCP port address. This address
|
||||||
|
* information is broken into 8-bit fields and the value of each field is encoded
|
||||||
|
* as a separate parameter whose value is a decimal number (in character string
|
||||||
|
* representation). Thus, the six parameters for the port command would be:
|
||||||
|
* h1,h2,h3,h4,p1,p2
|
||||||
|
* where h1 is the high order 8 bits of the internet host address, and p1 is the
|
||||||
|
* high order 8 bits of the port number.
|
||||||
|
* @return the HostAndPort object with the host InetAddres and int port parsed from the parameters
|
||||||
|
* @throws AssertFailedException
|
||||||
|
* - if parameters is null or contains an insufficient number of elements
|
||||||
|
* @throws NumberFormatException - if one of the parameters does not contain a parsable integer
|
||||||
|
*/
|
||||||
|
public static HostAndPort parseHostAndPort(String[] parameters) {
|
||||||
|
verifySufficientParameters(parameters);
|
||||||
|
|
||||||
|
byte host1 = parseByte(parameters[0]);
|
||||||
|
byte host2 = parseByte(parameters[1]);
|
||||||
|
byte host3 = parseByte(parameters[2]);
|
||||||
|
byte host4 = parseByte(parameters[3]);
|
||||||
|
|
||||||
|
byte[] address = {host1, host2, host3, host4};
|
||||||
|
InetAddress inetAddress = null;
|
||||||
|
try {
|
||||||
|
inetAddress = InetAddress.getByAddress(address);
|
||||||
|
}
|
||||||
|
catch (UnknownHostException e) {
|
||||||
|
throw new MockFtpServerException("Error parsing host", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
int port1 = Integer.parseInt(parameters[4]);
|
||||||
|
int port2 = Integer.parseInt(parameters[5]);
|
||||||
|
int port = (port1 << 8) + port2;
|
||||||
|
|
||||||
|
return new HostAndPort(inetAddress, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the InetAddess and port number to a comma-delimited list of byte values,
|
||||||
|
* suitable for the response String from the PASV command.
|
||||||
|
*
|
||||||
|
* @param host - the InetAddress
|
||||||
|
* @param port - the port number
|
||||||
|
* @return the comma-delimited list of byte values, e.g., "196,168,44,55,23,77"
|
||||||
|
*/
|
||||||
|
public static String convertHostAndPortToCommaDelimitedBytes(InetAddress host, int port) {
|
||||||
|
StringBuffer buffer = new StringBuffer();
|
||||||
|
byte[] address = host.getAddress();
|
||||||
|
for (int i = 0; i < address.length; i++) {
|
||||||
|
int positiveValue = (address[i] >= 0) ? address[i] : 256 + address[i];
|
||||||
|
buffer.append(positiveValue);
|
||||||
|
buffer.append(",");
|
||||||
|
}
|
||||||
|
int p1 = port >> 8;
|
||||||
|
int p2 = port % 256;
|
||||||
|
buffer.append(String.valueOf(p1));
|
||||||
|
buffer.append(",");
|
||||||
|
buffer.append(String.valueOf(p2));
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the parameters is not null and contains the required number of elements
|
||||||
|
*
|
||||||
|
* @param parameters - the String[] of command parameters
|
||||||
|
* @throws CommandSyntaxException - if parameters is null or contains an insufficient number of elements
|
||||||
|
*/
|
||||||
|
private static void verifySufficientParameters(String[] parameters) {
|
||||||
|
if (parameters == null || parameters.length < 6) {
|
||||||
|
List parms = parameters == null ? null : Arrays.asList(parameters);
|
||||||
|
throw new CommandSyntaxException("The PORT command must contain least be 6 parameters: " + parms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the specified String as an unsigned decimal byte value (i.e., 0..255). We can't just use
|
||||||
|
* Byte.parseByte(string) because that parses the string as a signed byte.
|
||||||
|
*
|
||||||
|
* @param string - the String containing the decimal byte representation to be parsed
|
||||||
|
* @return the byte value
|
||||||
|
*/
|
||||||
|
private static byte parseByte(String string) {
|
||||||
|
return (byte) (0xFF & Short.parseShort(string));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor. All methods are static.
|
||||||
|
*/
|
||||||
|
private PortParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.core.util;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains static String-related utility methods.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class StringUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pad the specified String with spaces to the right to the specified width. If the length
|
||||||
|
* of string is already equal to or greater than width, then just return string.
|
||||||
|
*
|
||||||
|
* @param string - the String to pad
|
||||||
|
* @param width - the target width
|
||||||
|
* @return a String of at least width characters, padded on the right with spaces as necessary
|
||||||
|
*/
|
||||||
|
public static String padRight(String string, int width) {
|
||||||
|
int numSpaces = width - string.length();
|
||||||
|
return (numSpaces > 0) ? string + spaces(numSpaces) : string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pad the specified String with spaces to the left to the specified width. If the length
|
||||||
|
* of string is already equal to or greater than width, then just return string.
|
||||||
|
*
|
||||||
|
* @param string - the String to pad
|
||||||
|
* @param width - the target width
|
||||||
|
* @return a String of at least width characters, padded on the left with spaces as necessary
|
||||||
|
*/
|
||||||
|
public static String padLeft(String string, int width) {
|
||||||
|
int numSpaces = width - string.length();
|
||||||
|
return (numSpaces > 0) ? spaces(numSpaces) + string : string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join the Strings within the parts Collection, inserting the delimiter in between elements
|
||||||
|
*
|
||||||
|
* @param parts - the Collection of Strings to join
|
||||||
|
* @param delimiter - the delimiter String to insert between the parts
|
||||||
|
* @return the Strings within the parts collection joined together using the specified delimiter
|
||||||
|
*/
|
||||||
|
public static String join(Collection parts, String delimiter) {
|
||||||
|
Assert.notNull(parts, "parts");
|
||||||
|
Assert.notNull(delimiter, "delimiter");
|
||||||
|
|
||||||
|
StringBuffer buf = new StringBuffer();
|
||||||
|
Iterator iter = parts.iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
String component = (String) iter.next();
|
||||||
|
buf.append(component);
|
||||||
|
if (iter.hasNext()) {
|
||||||
|
buf.append(delimiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Internal Helper Methods
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static String spaces(int numSpaces) {
|
||||||
|
StringBuffer buf = new StringBuffer();
|
||||||
|
for (int i = 0; i < numSpaces; i++) {
|
||||||
|
buf.append(" ");
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to prevent instantiation. All members are static.
|
||||||
|
*/
|
||||||
|
private StringUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,332 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.server.AbstractFtpServer;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.AborCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.AcctCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.AlloCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.AppeCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.CdupCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.CwdCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.DeleCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.EprtCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.EpsvCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.HelpCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.ListCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.MkdCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.ModeCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.NlstCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.NoopCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.PassCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.PasvCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.PortCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.PwdCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.QuitCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.ReinCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.RestCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.RetrCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.RmdCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.RnfrCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.RntoCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.SiteCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.SizeCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.SmntCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.StatCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.StorCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.StouCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.StruCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.SystCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.TypeCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.command.UserCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystem;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.CommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.CommandNames;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ConnectCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyTextBundleUtil;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.UnsupportedCommandHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <b>FakeFtpServer</b> is the top-level class for a "fake" implementation of an FTP Server,
|
||||||
|
* suitable for testing FTP client code or standing in for a live FTP server.
|
||||||
|
*
|
||||||
|
* <p><b>FakeFtpServer</b> provides a high-level abstraction for an FTP Server and is suitable
|
||||||
|
* for most testing and simulation scenarios. You define a filesystem (internal, in-memory) containing
|
||||||
|
* an arbitrary set of files and directories. These files and directories can (optionally) have
|
||||||
|
* associated access permissions. You also configure a set of one or more user accounts that
|
||||||
|
* control which users can login to the FTP server, and their home (default) directories. The
|
||||||
|
* user account is also used when assigning file and directory ownership for new files.
|
||||||
|
*
|
||||||
|
* <p><b>FakeFtpServer</b> processes FTP client requests and responds with reply codes and
|
||||||
|
* reply messages consistent with its configuration and the contents of its internal filesystem,
|
||||||
|
* including file and directory permissions, if they have been configured.
|
||||||
|
*
|
||||||
|
* <p><b>FakeFtpServer</b> can be fully configured programmatically or within the
|
||||||
|
* <a href="http://www.springframework.org/">Spring Framework</a> or other dependency-injection container.
|
||||||
|
*
|
||||||
|
* <p>In general the steps for setting up and starting the <b>FakeFtpServer</b> are:
|
||||||
|
* <ol>
|
||||||
|
* <li>Create a new <b>FakeFtpServer</b> instance, and optionally set the server control port.</li>
|
||||||
|
* <li>Create and configure a <b>FileSystem</b>, and attach to the <b>FakeFtpServer</b> instance.</li>
|
||||||
|
* <li>Create and configure one or more <b>UserAccount</b> objects and attach to the <b>FakeFtpServer</b> instance.</li>
|
||||||
|
* <li>Start the <b>FakeFtpServer</b> instance.</li>
|
||||||
|
* </ol>
|
||||||
|
* <p><b>Example Code</b></p>
|
||||||
|
* <pre><code>
|
||||||
|
* FakeFtpServer fakeFtpServer = new FakeFtpServer();
|
||||||
|
*
|
||||||
|
* FileSystem fileSystem = new WindowsFakeFileSystem();
|
||||||
|
* fileSystem.add(new DirectoryEntry("c:\\"));
|
||||||
|
* fileSystem.add(new DirectoryEntry("c:\\data"));
|
||||||
|
* fileSystem.add(new FileEntry("c:\\data\\file1.txt", "abcdef 1234567890"));
|
||||||
|
* fileSystem.add(new FileEntry("c:\\data\\run.exe"));
|
||||||
|
* fakeFtpServer.setFileSystem(fileSystem);
|
||||||
|
*
|
||||||
|
* // Create UserAccount with username, password, home-directory
|
||||||
|
* UserAccount userAccount = new UserAccount("joe", "joe123", "c:\\");
|
||||||
|
* fakeFtpServer.addUserAccounts(userAccount);
|
||||||
|
*
|
||||||
|
* fakeFtpServer.start();
|
||||||
|
* </code></pre>
|
||||||
|
*
|
||||||
|
* <p><b>Example Code with Permissions</b></p>
|
||||||
|
* You can optionally set the permissions and owner/group for each file and directory, as in the following example.
|
||||||
|
* <pre><code>
|
||||||
|
* FileSystem fileSystem = new UnixFakeFileSystem();
|
||||||
|
* DirectoryEntry directoryEntry1 = new DirectoryEntry("/");
|
||||||
|
* directoryEntry1.setPermissions(new Permissions("rwxrwx---"));
|
||||||
|
* directoryEntry1.setOwner("joe");
|
||||||
|
* directoryEntry1.setGroup("dev");
|
||||||
|
*
|
||||||
|
* DirectoryEntry directoryEntry2 = new DirectoryEntry("/data");
|
||||||
|
* directoryEntry2.setPermissions(Permissions.ALL);
|
||||||
|
* directoryEntry2.setOwner("joe");
|
||||||
|
* directoryEntry2.setGroup("dev");
|
||||||
|
*
|
||||||
|
* FileEntry fileEntry1 = new FileEntry("/data/file1.txt", "abcdef 1234567890");
|
||||||
|
* fileEntry1.setPermissionsFromString("rw-rw-rw-");
|
||||||
|
* fileEntry1.setOwner("joe");
|
||||||
|
* fileEntry1.setGroup("dev");
|
||||||
|
*
|
||||||
|
* FileEntry fileEntry2 = new FileEntry("/data/run.exe");
|
||||||
|
* fileEntry2.setPermissionsFromString("rwxrwx---");
|
||||||
|
* fileEntry2.setOwner("mary");
|
||||||
|
* fileEntry2.setGroup("dev");
|
||||||
|
*
|
||||||
|
* fileSystem.add(directoryEntry1);
|
||||||
|
* fileSystem.add(directoryEntry2);
|
||||||
|
* fileSystem.add(fileEntry1);
|
||||||
|
* fileSystem.add(fileEntry2);
|
||||||
|
*
|
||||||
|
* FakeFtpServer fakeFtpServer = new FakeFtpServer();
|
||||||
|
* fakeFtpServer.setFileSystem(fileSystem);
|
||||||
|
*
|
||||||
|
* // Create UserAccount with username, password, home-directory
|
||||||
|
* UserAccount userAccount = new UserAccount("joe", "joe123", "/");
|
||||||
|
* fakeFtpServer.addUserAccounts(userAccount);
|
||||||
|
*
|
||||||
|
* fakeFtpServer.start();
|
||||||
|
* </code></pre>
|
||||||
|
*
|
||||||
|
* <p><b>FTP Server Control Port</b></p>
|
||||||
|
* By default, <b>FakeFtpServer</b> binds to the server control port of 21. You can use a different server control
|
||||||
|
* port by setting the <code>serverControlPort</code> property. If you specify a value of <code>0</code>,
|
||||||
|
* then a free port number will be chosen automatically; call <code>getServerControlPort()</code> AFTER
|
||||||
|
* <code>start()</code> has been called to determine the actual port number being used. Using a non-default
|
||||||
|
* port number is usually necessary when running on Unix or some other system where that port number is
|
||||||
|
* already in use or cannot be bound from a user process.
|
||||||
|
*
|
||||||
|
* <p><b>Other Configuration</b></p>
|
||||||
|
* The <code>systemName</code> property specifies the value returned by the <code>SYST</code>
|
||||||
|
* command. Note that this is typically used by an FTP client to determine how to parse
|
||||||
|
* system-dependent reply text, such as directory listings. By default, the systemName from the
|
||||||
|
* configured FileSystem is used. But if the <code>systemName</code> property is set, then that
|
||||||
|
* will override the FileSystem value.
|
||||||
|
*
|
||||||
|
* <p>The <code>helpText</code> property specifies a <i>Map</i> of help text replies sent by the
|
||||||
|
* <code>HELP</code> command. The keys in that <i>Map</i> correspond to the command names passed as
|
||||||
|
* parameters to the <code>HELP</code> command. An entry with the key of an empty string ("") indicates the
|
||||||
|
* text used as the default help text when no command name parameter is specified for the <code>HELP</code> command.
|
||||||
|
*
|
||||||
|
* <p><b>FTP Command Reply Text ResourceBundle</b></p>
|
||||||
|
* The default text asociated with each FTP command reply code is contained within the
|
||||||
|
* "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a
|
||||||
|
* locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of
|
||||||
|
* the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can
|
||||||
|
* completely replace the ResourceBundle file by calling the calling the
|
||||||
|
* {@link #setReplyTextBaseName(String)} method.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class FakeFtpServer extends AbstractFtpServer implements ServerConfiguration {
|
||||||
|
|
||||||
|
private FileSystem fileSystem;
|
||||||
|
private String systemName;
|
||||||
|
private String systemStatus = "Connected";
|
||||||
|
private Map helpText = new HashMap();
|
||||||
|
private Map userAccounts = new HashMap();
|
||||||
|
|
||||||
|
public FileSystem getFileSystem() {
|
||||||
|
return fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileSystem(FileSystem fileSystem) {
|
||||||
|
this.fileSystem = fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSystemName() {
|
||||||
|
return systemName != null ? systemName : fileSystem.getSystemName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSystemName(String systemName) {
|
||||||
|
this.systemName = systemName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map getHelpText() {
|
||||||
|
return helpText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHelpText(Map helpText) {
|
||||||
|
this.helpText = helpText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FakeFtpServer() {
|
||||||
|
setCommandHandler(CommandNames.ACCT, new AcctCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.ABOR, new AborCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.ALLO, new AlloCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.APPE, new AppeCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.CWD, new CwdCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.CDUP, new CdupCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.DELE, new DeleCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.EPRT, new EprtCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.EPSV, new EpsvCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.HELP, new HelpCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.LIST, new ListCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.MKD, new MkdCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.MODE, new ModeCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.NLST, new NlstCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.NOOP, new NoopCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.PASS, new PassCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.PASV, new PasvCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.PWD, new PwdCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.PORT, new PortCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.QUIT, new QuitCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.REIN, new ReinCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.REST, new RestCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.RETR, new RetrCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.RMD, new RmdCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.RNFR, new RnfrCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.RNTO, new RntoCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.SITE, new SiteCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.SIZE, new SizeCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.SMNT, new SmntCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.STAT, new StatCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.STOR, new StorCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.STOU, new StouCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.STRU, new StruCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.SYST, new SystCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.TYPE, new TypeCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.USER, new UserCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.XPWD, new PwdCommandHandler());
|
||||||
|
|
||||||
|
// "Special" Command Handlers
|
||||||
|
setCommandHandler(CommandNames.CONNECT, new ConnectCommandHandler());
|
||||||
|
setCommandHandler(CommandNames.UNSUPPORTED, new UnsupportedCommandHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a CommandHandler that has been registered to this server.
|
||||||
|
*
|
||||||
|
* If the CommandHandler implements the <code>ServerConfigurationAware</code> interface, then set its
|
||||||
|
* <code>ServerConfiguration</code> property to <code>this</code>.
|
||||||
|
*
|
||||||
|
* If the CommandHandler implements the <code>ReplyTextBundleAware</code> interface, then set its
|
||||||
|
* <code>replyTextBundle</code> property using the reply text bundle for this server.
|
||||||
|
*
|
||||||
|
* @param commandHandler - the CommandHandler to initialize
|
||||||
|
*/
|
||||||
|
protected void initializeCommandHandler(CommandHandler commandHandler) {
|
||||||
|
if (commandHandler instanceof ServerConfigurationAware) {
|
||||||
|
ServerConfigurationAware sca = (ServerConfigurationAware) commandHandler;
|
||||||
|
sca.setServerConfiguration(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, getReplyTextBundle());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the {@link UserAccount} configured for this server for the specified user name
|
||||||
|
*/
|
||||||
|
public UserAccount getUserAccount(String username) {
|
||||||
|
return (UserAccount) userAccounts.get(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the help text for a command or the default help text if no command name is specified
|
||||||
|
*
|
||||||
|
* @param name - the command name; may be empty or null to indicate a request for the default help text
|
||||||
|
* @return the help text for the named command or the default help text if no name is supplied
|
||||||
|
*/
|
||||||
|
public String getHelpText(String name) {
|
||||||
|
String key = name == null ? "" : name;
|
||||||
|
return (String) helpText.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a single UserAccount. If an account with the same <code>username</code> already exists,
|
||||||
|
* it will be replaced.
|
||||||
|
*
|
||||||
|
* @param userAccount - the UserAccount to add
|
||||||
|
*/
|
||||||
|
public void addUserAccount(UserAccount userAccount) {
|
||||||
|
userAccounts.put(userAccount.getUsername(), userAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the UserAccount objects in the <code>userAccountList</code> to the set of UserAccounts.
|
||||||
|
*
|
||||||
|
* @param userAccountList - the List of UserAccount objects to add
|
||||||
|
*/
|
||||||
|
public void setUserAccounts(List userAccountList) {
|
||||||
|
for (int i = 0; i < userAccountList.size(); i++) {
|
||||||
|
UserAccount userAccount = (UserAccount) userAccountList.get(i);
|
||||||
|
userAccounts.put(userAccount.getUsername(), userAccount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the system status description
|
||||||
|
*
|
||||||
|
* @return the system status
|
||||||
|
*/
|
||||||
|
public String getSystemStatus() {
|
||||||
|
return systemStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the system status description text, used by the STAT command handler.
|
||||||
|
*
|
||||||
|
* @param systemStatus - the system status description text
|
||||||
|
*/
|
||||||
|
public void setSystemStatus(String systemStatus) {
|
||||||
|
this.systemStatus = systemStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for objects that provide access to server-specific information.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public interface ServerConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the {@link FileSystem} for this server
|
||||||
|
*/
|
||||||
|
public FileSystem getFileSystem();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param username - the user name
|
||||||
|
* @return the {@link UserAccount} configured for this server for the specified user name
|
||||||
|
*/
|
||||||
|
public UserAccount getUserAccount(String username);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the System Name for this server (used by the SYST command)
|
||||||
|
*/
|
||||||
|
public String getSystemName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the System Status text for this server (used by the STAT command)
|
||||||
|
*/
|
||||||
|
public String getSystemStatus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the help text for a command or the default help text if no command name is specified
|
||||||
|
*
|
||||||
|
* @param name - the command name; may be empty or null to indicate a request for the default help text
|
||||||
|
* @return the help text for the named command or the default help text if no name is supplied
|
||||||
|
*/
|
||||||
|
public String getHelpText(String name);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for classes that provide setter and getter to access a ServerConfiguration instance.
|
||||||
|
*/
|
||||||
|
public interface ServerConfigurationAware {
|
||||||
|
|
||||||
|
public ServerConfiguration getServerConfiguration();
|
||||||
|
|
||||||
|
public void setServerConfiguration(ServerConfiguration serverConfiguration);
|
||||||
|
}
|
|
@ -0,0 +1,295 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.Permissions;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single user account on the server, including the username, password, home
|
||||||
|
* directory, list of groups to which this user belongs, and default permissions applied to
|
||||||
|
* newly-created files and directories.
|
||||||
|
*
|
||||||
|
* <p>The <code>username</code> and <code>homeDirectory</code> property must be non-null
|
||||||
|
* and non-empty. The <code>homeDirectory</code> property must also match the name of an existing
|
||||||
|
* directory within the file system configured for the <code>FakeFtpServer</code>.
|
||||||
|
*
|
||||||
|
* <p>The group name applied to newly created files/directories is determined by the <code>groups</code> property.
|
||||||
|
* If null or empty, then the default group name ("users") is used. Otherwise, the first value in the
|
||||||
|
* <code>groups</code> List is used. The <code>groups</code> property defaults to an empty List.
|
||||||
|
*
|
||||||
|
* <p>The default value for <code>defaultPermissionsForNewFile</code> is read and write permissions for
|
||||||
|
* all (user/group/world). The default value for <code>defaultPermissionsForNewDirectory</code> is read,
|
||||||
|
* write and execute permissions for all (user/group/world).
|
||||||
|
*
|
||||||
|
* <p>The <code>isValidPassword()</code> method returns true if the specified password matches
|
||||||
|
* the password value configured for this user account. This implementation uses the
|
||||||
|
* <code>isEquals()</code> method to compare passwords.
|
||||||
|
*
|
||||||
|
* <p>If you want to provide a custom comparison, for instance using encrypted passwords, you can
|
||||||
|
* subclass this class and override the <code>comparePassword()</code> method to provide your own
|
||||||
|
* custom implementation.
|
||||||
|
*
|
||||||
|
* <p>If the <code>passwordCheckedDuringValidation</code> property is set to false, then the password
|
||||||
|
* value is ignored, and the <code>isValidPassword()</code> method just returns <code>true</code>.
|
||||||
|
*
|
||||||
|
* <p>The <code>accountRequiredForLogin</code> property defaults to false. If it is set to true, then
|
||||||
|
* it is expected that the login for this account will require an ACCOUNT (ACCT) command after the
|
||||||
|
* PASSWORD (PASS) command is completed.
|
||||||
|
*/
|
||||||
|
public class UserAccount {
|
||||||
|
|
||||||
|
public static final String DEFAULT_USER = "system";
|
||||||
|
public static final String DEFAULT_GROUP = "users";
|
||||||
|
public static final Permissions DEFAULT_PERMISSIONS_FOR_NEW_FILE = new Permissions("rw-rw-rw-");
|
||||||
|
public static final Permissions DEFAULT_PERMISSIONS_FOR_NEW_DIRECTORY = Permissions.ALL;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private String homeDirectory;
|
||||||
|
private List groups;
|
||||||
|
private boolean passwordRequiredForLogin = true;
|
||||||
|
private boolean passwordCheckedDuringValidation = true;
|
||||||
|
private boolean accountRequiredForLogin = false;
|
||||||
|
private Permissions defaultPermissionsForNewFile = DEFAULT_PERMISSIONS_FOR_NEW_FILE;
|
||||||
|
private Permissions defaultPermissionsForNewDirectory = DEFAULT_PERMISSIONS_FOR_NEW_DIRECTORY;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new uninitialized instance.
|
||||||
|
*/
|
||||||
|
public UserAccount() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new initialized instance.
|
||||||
|
*
|
||||||
|
* @param username - the user name
|
||||||
|
* @param password - the password
|
||||||
|
* @param homeDirectory - the home directory
|
||||||
|
*/
|
||||||
|
public UserAccount(String username, String password, String homeDirectory) {
|
||||||
|
setUsername(username);
|
||||||
|
setPassword(password);
|
||||||
|
setHomeDirectory(homeDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHomeDirectory() {
|
||||||
|
return homeDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHomeDirectory(String homeDirectory) {
|
||||||
|
this.homeDirectory = homeDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List getGroups() {
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroups(List groups) {
|
||||||
|
this.groups = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPasswordRequiredForLogin() {
|
||||||
|
return passwordRequiredForLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPasswordRequiredForLogin(boolean passwordRequiredForLogin) {
|
||||||
|
this.passwordRequiredForLogin = passwordRequiredForLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPasswordCheckedDuringValidation() {
|
||||||
|
return passwordCheckedDuringValidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPasswordCheckedDuringValidation(boolean passwordCheckedDuringValidation) {
|
||||||
|
this.passwordCheckedDuringValidation = passwordCheckedDuringValidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAccountRequiredForLogin() {
|
||||||
|
return accountRequiredForLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccountRequiredForLogin(boolean accountRequiredForLogin) {
|
||||||
|
this.accountRequiredForLogin = accountRequiredForLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Permissions getDefaultPermissionsForNewFile() {
|
||||||
|
return defaultPermissionsForNewFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultPermissionsForNewFile(Permissions defaultPermissionsForNewFile) {
|
||||||
|
this.defaultPermissionsForNewFile = defaultPermissionsForNewFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Permissions getDefaultPermissionsForNewDirectory() {
|
||||||
|
return defaultPermissionsForNewDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultPermissionsForNewDirectory(Permissions defaultPermissionsForNewDirectory) {
|
||||||
|
this.defaultPermissionsForNewDirectory = defaultPermissionsForNewDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the name of the primary group to which this user belongs. If this account has no associated
|
||||||
|
* groups set, then this method returns the <code>DEFAULT_GROUP</code>. Otherwise, this method
|
||||||
|
* returns the first group name in the <code>groups</code> list.
|
||||||
|
*
|
||||||
|
* @return the name of the primary group for this user
|
||||||
|
*/
|
||||||
|
public String getPrimaryGroup() {
|
||||||
|
return (groups == null || groups.isEmpty()) ? DEFAULT_GROUP : (String) groups.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the specified password is the correct, valid password for this user account.
|
||||||
|
* This implementation uses standard (case-sensitive) String comparison. Subclasses can provide
|
||||||
|
* custom comparison behavior, for instance using encrypted password values, by overriding this
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param password - the password to compare against the configured value
|
||||||
|
* @return true if the password is correct and valid
|
||||||
|
* @throws AssertFailedException
|
||||||
|
* - if the username property is null
|
||||||
|
*/
|
||||||
|
public boolean isValidPassword(String password) {
|
||||||
|
Assert.notNullOrEmpty(username, "username");
|
||||||
|
return !passwordCheckedDuringValidation || comparePassword(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if this UserAccount object is valid; i.e. if the homeDirectory is non-null and non-empty.
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return homeDirectory != null && homeDirectory.length() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the String representation of this object
|
||||||
|
*/
|
||||||
|
public String toString() {
|
||||||
|
return "UserAccount[username=" + username + "; password=" + password + "; homeDirectory="
|
||||||
|
+ homeDirectory + "; passwordRequiredForLogin=" + passwordRequiredForLogin + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if this user has read access to the file/directory represented by the specified FileSystemEntry object.
|
||||||
|
*
|
||||||
|
* @param entry - the FileSystemEntry representing the file or directory
|
||||||
|
* @return true if this use has read access
|
||||||
|
*/
|
||||||
|
public boolean canRead(FileSystemEntry entry) {
|
||||||
|
Permissions permissions = entry.getPermissions();
|
||||||
|
if (permissions == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equalOrBothNull(username, entry.getOwner())) {
|
||||||
|
return permissions.canUserRead();
|
||||||
|
}
|
||||||
|
if (groups != null && groups.contains(entry.getGroup())) {
|
||||||
|
return permissions.canGroupRead();
|
||||||
|
}
|
||||||
|
return permissions.canWorldRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if this user has write access to the file/directory represented by the specified FileSystemEntry object.
|
||||||
|
*
|
||||||
|
* @param entry - the FileSystemEntry representing the file or directory
|
||||||
|
* @return true if this use has write access
|
||||||
|
*/
|
||||||
|
public boolean canWrite(FileSystemEntry entry) {
|
||||||
|
Permissions permissions = entry.getPermissions();
|
||||||
|
if (permissions == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equalOrBothNull(username, entry.getOwner())) {
|
||||||
|
return permissions.canUserWrite();
|
||||||
|
}
|
||||||
|
if (groups != null && groups.contains(entry.getGroup())) {
|
||||||
|
return permissions.canGroupWrite();
|
||||||
|
}
|
||||||
|
return permissions.canWorldWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if this user has execute access to the file/directory represented by the specified FileSystemEntry object.
|
||||||
|
*
|
||||||
|
* @param entry - the FileSystemEntry representing the file or directory
|
||||||
|
* @return true if this use has execute access
|
||||||
|
*/
|
||||||
|
public boolean canExecute(FileSystemEntry entry) {
|
||||||
|
Permissions permissions = entry.getPermissions();
|
||||||
|
if (permissions == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equalOrBothNull(username, entry.getOwner())) {
|
||||||
|
return permissions.canUserExecute();
|
||||||
|
}
|
||||||
|
if (groups != null && groups.contains(entry.getGroup())) {
|
||||||
|
return permissions.canGroupExecute();
|
||||||
|
}
|
||||||
|
return permissions.canWorldExecute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the specified password matches the password configured for this user account.
|
||||||
|
* This implementation uses standard (case-sensitive) String comparison. Subclasses can provide
|
||||||
|
* custom comparison behavior, for instance using encrypted password values, by overriding this
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param password - the password to compare against the configured value
|
||||||
|
* @return true if the passwords match
|
||||||
|
*/
|
||||||
|
protected boolean comparePassword(String password) {
|
||||||
|
return password != null && password.equals(this.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true only if both Strings are null or they are equal (have the same contents).
|
||||||
|
*
|
||||||
|
* @param string1 - the first String
|
||||||
|
* @param string2 - the second String
|
||||||
|
* @return true if both are null or both are equal
|
||||||
|
*/
|
||||||
|
protected boolean equalOrBothNull(String string1, String string2) {
|
||||||
|
return (string1 == null && string2 == null) || (string1 != null && string1.equals(string2));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the ABOR command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>Otherwise, reply with 226</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class AborCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
sendReply(session, ReplyCodes.ABOR_OK, "abor");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,442 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.xbib.files.ftp.mock.core.CommandSyntaxException;
|
||||||
|
import org.xbib.files.ftp.mock.core.IllegalStateException;
|
||||||
|
import org.xbib.files.ftp.mock.core.NotLoggedInException;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.AbstractCommandHandler;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
import org.xbib.files.ftp.mock.fake.ServerConfiguration;
|
||||||
|
import org.xbib.files.ftp.mock.fake.ServerConfigurationAware;
|
||||||
|
import org.xbib.files.ftp.mock.fake.UserAccount;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystem;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemException;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.InvalidFilenameException;
|
||||||
|
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.MissingResourceException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract superclass for CommandHandler classes for the "Fake" server.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public abstract class AbstractFakeCommandHandler extends AbstractCommandHandler implements ServerConfigurationAware {
|
||||||
|
|
||||||
|
protected static final String INTERNAL_ERROR_KEY = "internalError";
|
||||||
|
|
||||||
|
private ServerConfiguration serverConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reply code sent back when a FileSystemException is caught by the {@link #handleCommand(Command, Session)}
|
||||||
|
* This defaults to ReplyCodes.EXISTING_FILE_ERROR (550).
|
||||||
|
*/
|
||||||
|
protected int replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
|
||||||
|
public ServerConfiguration getServerConfiguration() {
|
||||||
|
return serverConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServerConfiguration(ServerConfiguration serverConfiguration) {
|
||||||
|
this.serverConfiguration = serverConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use template method to centralize and ensure common validation
|
||||||
|
*/
|
||||||
|
public void handleCommand(Command command, Session session) {
|
||||||
|
Assert.notNull(serverConfiguration, "serverConfiguration");
|
||||||
|
Assert.notNull(command, "command");
|
||||||
|
Assert.notNull(session, "session");
|
||||||
|
|
||||||
|
try {
|
||||||
|
handle(command, session);
|
||||||
|
}
|
||||||
|
catch (CommandSyntaxException e) {
|
||||||
|
handleException(command, session, e, ReplyCodes.COMMAND_SYNTAX_ERROR);
|
||||||
|
}
|
||||||
|
catch (IllegalStateException e) {
|
||||||
|
handleException(command, session, e, ReplyCodes.ILLEGAL_STATE);
|
||||||
|
}
|
||||||
|
catch (NotLoggedInException e) {
|
||||||
|
handleException(command, session, e, ReplyCodes.NOT_LOGGED_IN);
|
||||||
|
}
|
||||||
|
catch (InvalidFilenameException e) {
|
||||||
|
handleFileSystemException(command, session, e, ReplyCodes.FILENAME_NOT_VALID, e.getPath());
|
||||||
|
}
|
||||||
|
catch (FileSystemException e) {
|
||||||
|
handleFileSystemException(command, session, e, replyCodeForFileSystemException, e.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to return the FileSystem stored in the ServerConfiguration
|
||||||
|
*
|
||||||
|
* @return the FileSystem
|
||||||
|
*/
|
||||||
|
protected FileSystem getFileSystem() {
|
||||||
|
return serverConfiguration.getFileSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the specified command for the session. All checked exceptions are expected to be wrapped or handled
|
||||||
|
* by the caller.
|
||||||
|
*
|
||||||
|
* @param command - the Command to be handled
|
||||||
|
* @param session - the session on which the Command was submitted
|
||||||
|
*/
|
||||||
|
protected abstract void handle(Command command, Session session);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Utility methods for subclasses
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a reply for this command on the control connection.
|
||||||
|
*
|
||||||
|
* <p>The reply code is designated by the <code>replyCode</code> property, and the reply text
|
||||||
|
* is retrieved from the <code>replyText</code> ResourceBundle, using the specified messageKey.
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
* @param replyCode - the reply code
|
||||||
|
* @param messageKey - the resource bundle key for the reply text
|
||||||
|
* @throws AssertionError - if session is null
|
||||||
|
* @see MessageFormat
|
||||||
|
*/
|
||||||
|
protected void sendReply(Session session, int replyCode, String messageKey) {
|
||||||
|
sendReply(session, replyCode, messageKey, Collections.EMPTY_LIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a reply for this command on the control connection.
|
||||||
|
*
|
||||||
|
* <p>The reply code is designated by the <code>replyCode</code> property, and the reply text
|
||||||
|
* is retrieved from the <code>replyText</code> ResourceBundle, using the specified messageKey.
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
* @param replyCode - the reply code
|
||||||
|
* @param messageKey - the resource bundle key for the reply text
|
||||||
|
* @param args - the optional message arguments; defaults to []
|
||||||
|
* @throws AssertionError - if session is null
|
||||||
|
* @see MessageFormat
|
||||||
|
*/
|
||||||
|
protected void sendReply(Session session, int replyCode, String messageKey, List args) {
|
||||||
|
Assert.notNull(session, "session");
|
||||||
|
assertValidReplyCode(replyCode);
|
||||||
|
|
||||||
|
String text = getTextForKey(messageKey);
|
||||||
|
String replyText = (args != null && !args.isEmpty()) ? MessageFormat.format(text, args.toArray()) : text;
|
||||||
|
|
||||||
|
String replyTextToLog = (replyText == null) ? "" : " " + replyText;
|
||||||
|
String argsToLog = (args != null && !args.isEmpty()) ? (" args=" + args) : "";
|
||||||
|
LOG.info("Sending reply [" + replyCode + replyTextToLog + "]" + argsToLog);
|
||||||
|
session.sendReply(replyCode, replyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a reply for this command on the control connection.
|
||||||
|
*
|
||||||
|
* <p>The reply code is designated by the <code>replyCode</code> property, and the reply text
|
||||||
|
* is retrieved from the <code>replyText</code> ResourceBundle, using the reply code as the key.
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
* @param replyCode - the reply code
|
||||||
|
* @throws AssertionError - if session is null
|
||||||
|
* @see MessageFormat
|
||||||
|
*/
|
||||||
|
protected void sendReply(Session session, int replyCode) {
|
||||||
|
sendReply(session, replyCode, Collections.EMPTY_LIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a reply for this command on the control connection.
|
||||||
|
*
|
||||||
|
* <p>The reply code is designated by the <code>replyCode</code> property, and the reply text
|
||||||
|
* is retrieved from the <code>replyText</code> ResourceBundle, using the reply code as the key.
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
* @param replyCode - the reply code
|
||||||
|
* @param args - the optional message arguments; defaults to []
|
||||||
|
* @throws AssertionError - if session is null
|
||||||
|
* @see MessageFormat
|
||||||
|
*/
|
||||||
|
protected void sendReply(Session session, int replyCode, List args) {
|
||||||
|
sendReply(session, replyCode, Integer.toString(replyCode), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the exception caught during handleCommand()
|
||||||
|
*
|
||||||
|
* @param command - the Command
|
||||||
|
* @param session - the Session
|
||||||
|
* @param exception - the caught exception
|
||||||
|
* @param replyCode - the reply code that should be sent back
|
||||||
|
*/
|
||||||
|
private void handleException(Command command, Session session, Throwable exception, int replyCode) {
|
||||||
|
LOG.log(Level.WARNING, "Error handling command: " + command + "; " + exception, exception);
|
||||||
|
sendReply(session, replyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the exception caught during handleCommand()
|
||||||
|
*
|
||||||
|
* @param command - the Command
|
||||||
|
* @param session - the Session
|
||||||
|
* @param exception - the caught exception
|
||||||
|
* @param replyCode - the reply code that should be sent back
|
||||||
|
* @param arg - the arg for the reply (message)
|
||||||
|
*/
|
||||||
|
private void handleFileSystemException(Command command, Session session, FileSystemException exception, int replyCode, Object arg) {
|
||||||
|
LOG.log(Level.WARNING, "Error handling command: " + command + "; " + exception, exception);
|
||||||
|
sendReply(session, replyCode, exception.getMessageKey(), Collections.singletonList(arg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the value of the named attribute within the session.
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
* @param name - the name of the session attribute to retrieve
|
||||||
|
* @return the value of the named session attribute
|
||||||
|
* @throws IllegalStateException - if the Session does not contain the named attribute
|
||||||
|
*/
|
||||||
|
protected Object getRequiredSessionAttribute(Session session, String name) {
|
||||||
|
Object value = session.getAttribute(name);
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalStateException("Session missing required attribute [" + name + "]");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the current user (if any) has already logged in successfully.
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
*/
|
||||||
|
protected void verifyLoggedIn(Session session) {
|
||||||
|
if (getUserAccount(session) == null) {
|
||||||
|
throw new NotLoggedInException("User has not logged in");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param session - the Session
|
||||||
|
* @return the UserAccount stored in the specified session; may be null
|
||||||
|
*/
|
||||||
|
protected UserAccount getUserAccount(Session session) {
|
||||||
|
return (UserAccount) session.getAttribute(SessionKeys.USER_ACCOUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the specified condition related to the file system is true,
|
||||||
|
* otherwise throw a FileSystemException.
|
||||||
|
*
|
||||||
|
* @param condition - the condition that must be true
|
||||||
|
* @param path - the path involved in the operation; this will be included in the
|
||||||
|
* error message if the condition is not true.
|
||||||
|
* @param messageKey - the message key for the exception message
|
||||||
|
* @throws FileSystemException - if the condition is not true
|
||||||
|
*/
|
||||||
|
protected void verifyFileSystemCondition(boolean condition, String path, String messageKey) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new FileSystemException(path, messageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the current user has execute permission to the specified path
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
* @param path - the file system path
|
||||||
|
* @throws FileSystemException - if the condition is not true
|
||||||
|
*/
|
||||||
|
protected void verifyExecutePermission(Session session, String path) {
|
||||||
|
UserAccount userAccount = getUserAccount(session);
|
||||||
|
FileSystemEntry entry = getFileSystem().getEntry(path);
|
||||||
|
verifyFileSystemCondition(userAccount.canExecute(entry), path, "filesystem.cannotExecute");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the current user has write permission to the specified path
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
* @param path - the file system path
|
||||||
|
* @throws FileSystemException - if the condition is not true
|
||||||
|
*/
|
||||||
|
protected void verifyWritePermission(Session session, String path) {
|
||||||
|
UserAccount userAccount = getUserAccount(session);
|
||||||
|
FileSystemEntry entry = getFileSystem().getEntry(path);
|
||||||
|
verifyFileSystemCondition(userAccount.canWrite(entry), path, "filesystem.cannotWrite");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the current user has read permission to the specified path
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
* @param path - the file system path
|
||||||
|
* @throws FileSystemException - if the condition is not true
|
||||||
|
*/
|
||||||
|
protected void verifyReadPermission(Session session, String path) {
|
||||||
|
UserAccount userAccount = getUserAccount(session);
|
||||||
|
FileSystemEntry entry = getFileSystem().getEntry(path);
|
||||||
|
verifyFileSystemCondition(userAccount.canRead(entry), path, "filesystem.cannotRead");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the full, absolute path for the specified abstract pathname.
|
||||||
|
* If path is null, return the current directory (stored in the session). If
|
||||||
|
* path represents an absolute path, then return path as is. Otherwise, path
|
||||||
|
* is relative, so assemble the full path from the current directory
|
||||||
|
* and the specified relative path.
|
||||||
|
*
|
||||||
|
* @param session - the Session
|
||||||
|
* @param path - the abstract pathname; may be null
|
||||||
|
* @return the resulting full, absolute path
|
||||||
|
*/
|
||||||
|
protected String getRealPath(Session session, String path) {
|
||||||
|
String currentDirectory = (String) session.getAttribute(SessionKeys.CURRENT_DIRECTORY);
|
||||||
|
if (path == null) {
|
||||||
|
return currentDirectory;
|
||||||
|
}
|
||||||
|
if (getFileSystem().isAbsolute(path)) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return getFileSystem().path(currentDirectory, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the end-of-line character(s) used when building multi-line responses
|
||||||
|
*
|
||||||
|
* @return "\r\n"
|
||||||
|
*/
|
||||||
|
protected String endOfLine() {
|
||||||
|
return "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTextForKey(String key) {
|
||||||
|
String msgKey = (key != null) ? key : INTERNAL_ERROR_KEY;
|
||||||
|
try {
|
||||||
|
return getReplyTextBundle().getString(msgKey);
|
||||||
|
}
|
||||||
|
catch (MissingResourceException e) {
|
||||||
|
// No reply text is mapped for the specified key
|
||||||
|
LOG.log(Level.WARNING, "No reply text defined for key [" + msgKey + "]");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Login Support (used by USER and PASS commands)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the UserAccount for the specified username. If valid, return true. If the UserAccount does
|
||||||
|
* not exist or is invalid, log an error message, send back a reply code of 530 with an appropriate
|
||||||
|
* error message, and return false. A UserAccount is considered invalid if the homeDirectory property
|
||||||
|
* is not set or is set to a non-existent directory.
|
||||||
|
*
|
||||||
|
* @param username - the username
|
||||||
|
* @param session - the session; used to send back an error reply if necessary
|
||||||
|
* @return true only if the UserAccount for the named user is valid
|
||||||
|
*/
|
||||||
|
protected boolean validateUserAccount(String username, Session session) {
|
||||||
|
UserAccount userAccount = serverConfiguration.getUserAccount(username);
|
||||||
|
if (userAccount == null || !userAccount.isValid()) {
|
||||||
|
LOG.log(Level.SEVERE, "UserAccount missing or not valid for username [" + username + "]: " + userAccount);
|
||||||
|
sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.userAccountNotValid", list(username));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String home = userAccount.getHomeDirectory();
|
||||||
|
if (!getFileSystem().isDirectory(home)) {
|
||||||
|
LOG.log(Level.SEVERE, "Home directory configured for username [" + username + "] is not valid: " + home);
|
||||||
|
sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.homeDirectoryNotValid", list(username, home));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in the specified user for the current session. Send back a reply of 230 with a message indicated
|
||||||
|
* by the replyMessageKey and set the UserAccount and current directory (homeDirectory) in the session.
|
||||||
|
*
|
||||||
|
* @param userAccount - the userAccount for the user to be logged in
|
||||||
|
* @param session - the session
|
||||||
|
* @param replyCode - the reply code to send
|
||||||
|
* @param replyMessageKey - the message key for the reply text
|
||||||
|
*/
|
||||||
|
protected void login(UserAccount userAccount, Session session, int replyCode, String replyMessageKey) {
|
||||||
|
sendReply(session, replyCode, replyMessageKey);
|
||||||
|
session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount);
|
||||||
|
session.setAttribute(SessionKeys.CURRENT_DIRECTORY, userAccount.getHomeDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to return a List with the specified single item
|
||||||
|
*
|
||||||
|
* @param item - the single item in the returned List
|
||||||
|
* @return a new List with that single item
|
||||||
|
*/
|
||||||
|
protected List list(Object item) {
|
||||||
|
return Collections.singletonList(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to return a List with the specified two items
|
||||||
|
*
|
||||||
|
* @param item1 - the first item in the returned List
|
||||||
|
* @param item2 - the second item in the returned List
|
||||||
|
* @return a new List with the specified items
|
||||||
|
*/
|
||||||
|
protected List list(Object item1, Object item2) {
|
||||||
|
List list = new ArrayList(2);
|
||||||
|
list.add(item1);
|
||||||
|
list.add(item2);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the specified string is null or empty
|
||||||
|
*
|
||||||
|
* @param string - the String to check; may be null
|
||||||
|
* @return true only if the specified String is null or empyt
|
||||||
|
*/
|
||||||
|
protected boolean notNullOrEmpty(String string) {
|
||||||
|
return string != null && string.length() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the string unless it is null or empty, in which case return the defaultString.
|
||||||
|
*
|
||||||
|
* @param string - the String to check; may be null
|
||||||
|
* @param defaultString - the value to return if string is null or empty
|
||||||
|
* @return string if not null and not empty; otherwise return defaultString
|
||||||
|
*/
|
||||||
|
protected String defaultIfNullOrEmpty(String string, String defaultString) {
|
||||||
|
return (notNullOrEmpty(string) ? string : defaultString);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract superclass for CommandHandlers that that store a file (STOR, STOU, APPE). Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530 and terminate</li>
|
||||||
|
* <li>If the pathname parameter is required but missing, then reply with 501 and terminate</li>
|
||||||
|
* <li>If the required pathname parameter does not specify a valid filename, then reply with 553 and terminate</li>
|
||||||
|
* <li>If the current user does not have write access to the named file, if it already exists, or else to its
|
||||||
|
* parent directory, then reply with 553 and terminate</li>
|
||||||
|
* <li>If the current user does not have execute access to the parent directory, then reply with 553 and terminate</li>
|
||||||
|
* <li>Send an initial reply of 150</li>
|
||||||
|
* <li>Read all available bytes from the data connection and store/append to the named file in the server file system</li>
|
||||||
|
* <li>If file write/store fails, then reply with 553 and terminate</li>
|
||||||
|
* <li>Send a final reply with 226</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public abstract class AbstractStoreFileCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.WRITE_FILE_ERROR;
|
||||||
|
|
||||||
|
String filename = getOutputFile(command);
|
||||||
|
String path = getRealPath(session, filename);
|
||||||
|
verifyFileSystemCondition(!getFileSystem().isDirectory(path), path, "filesystem.isDirectory");
|
||||||
|
String parentPath = getFileSystem().getParent(path);
|
||||||
|
verifyFileSystemCondition(getFileSystem().isDirectory(parentPath), parentPath, "filesystem.isNotADirectory");
|
||||||
|
|
||||||
|
// User must have write permission to the file, if an existing file, or else to the directory if a new file
|
||||||
|
String pathMustBeWritable = getFileSystem().exists(path) ? path : parentPath;
|
||||||
|
verifyWritePermission(session, pathMustBeWritable);
|
||||||
|
|
||||||
|
// User must have execute permission to the parent directory
|
||||||
|
verifyExecutePermission(session, parentPath);
|
||||||
|
|
||||||
|
sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);
|
||||||
|
|
||||||
|
session.openDataConnection();
|
||||||
|
byte[] contents = session.readData();
|
||||||
|
session.closeDataConnection();
|
||||||
|
|
||||||
|
FileEntry file = (FileEntry) getFileSystem().getEntry(path);
|
||||||
|
if (file == null) {
|
||||||
|
file = new FileEntry(path);
|
||||||
|
getFileSystem().add(file);
|
||||||
|
}
|
||||||
|
file.setPermissions(getUserAccount(session).getDefaultPermissionsForNewFile());
|
||||||
|
|
||||||
|
if (contents != null && contents.length > 0) {
|
||||||
|
OutputStream out = file.createOutputStream(appendToOutputFile());
|
||||||
|
try {
|
||||||
|
out.write(contents);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error writing to file [" + file.getPath() + "]", e);
|
||||||
|
throw new FileSystemException(file.getPath(), null, e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
try {
|
||||||
|
out.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error closing OutputStream for file [" + file.getPath() + "]", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendReply(session, ReplyCodes.TRANSFER_DATA_FINAL_OK, getMessageKey(), list(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the path (absolute or relative) for the output file. The default behavior is to return
|
||||||
|
* the required first parameter for the specified Command. Subclasses may override the default behavior.
|
||||||
|
*
|
||||||
|
* @param command - the Command
|
||||||
|
* @return the output file name
|
||||||
|
*/
|
||||||
|
protected String getOutputFile(Command command) {
|
||||||
|
return command.getRequiredParameter(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if this command should append the transferred contents to the output file; false means
|
||||||
|
* overwrite an existing file. This default implentation returns false.
|
||||||
|
*/
|
||||||
|
protected boolean appendToOutputFile() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the message key for the reply message sent with the final (226) reply
|
||||||
|
*/
|
||||||
|
protected abstract String getMessageKey();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the ACCT command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the required account parameter is missing, then reply with 501</li>
|
||||||
|
* <li>If this command was not preceded by a valid USER command, then reply with 503</li>
|
||||||
|
* <li>Store the account name in the session and reply with 230</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class AcctCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
String accountName = command.getRequiredParameter(0);
|
||||||
|
String username = (String) getRequiredSessionAttribute(session, SessionKeys.USERNAME);
|
||||||
|
|
||||||
|
session.setAttribute(SessionKeys.ACCOUNT_NAME, accountName);
|
||||||
|
sendReply(session, ReplyCodes.ACCT_OK, "acct", list(username));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the ALLO command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>Otherwise, reply with 200</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class AlloCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
sendReply(session, ReplyCodes.ALLO_OK, "allo");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the APPE command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530 and terminate</li>
|
||||||
|
* <li>If the required pathname parameter is missing, then reply with 501 and terminate</li>
|
||||||
|
* <li>If the pathname parameter does not specify a valid filename, then reply with 553 and terminate</li>
|
||||||
|
* <li>If the current user does not have write access to the named file, if it already exists, or else to its
|
||||||
|
* parent directory, then reply with 553 and terminate</li>
|
||||||
|
* <li>If the current user does not have execute access to the parent directory, then reply with 553 and terminate</li>
|
||||||
|
* <li>Send an initial reply of 150</li>
|
||||||
|
* <li>Read all available bytes from the data connection and append to the named file in the server file system</li>
|
||||||
|
* <li>If file write/store fails, then reply with 553 and terminate</li>
|
||||||
|
* <li>Send a final reply with 226</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class AppeCommandHandler extends AbstractStoreFileCommandHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the message key for the reply message sent with the final (226) reply
|
||||||
|
*/
|
||||||
|
protected String getMessageKey() {
|
||||||
|
return "appe";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if this command should append the transferred contents to the output file; false means
|
||||||
|
* overwrite an existing file.
|
||||||
|
*/
|
||||||
|
protected boolean appendToOutputFile() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the CDUP command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>If the current directory has no parent or if the current directory cannot be changed, then reply with 550 and terminate</li>
|
||||||
|
* <li>If the current user does not have execute access to the parent directory, then reply with 550 and terminate</li>
|
||||||
|
* <li>Otherwise, reply with 200 and change the current directory stored in the session to the parent directory</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class CdupCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
String currentDirectory = (String) getRequiredSessionAttribute(session, SessionKeys.CURRENT_DIRECTORY);
|
||||||
|
String path = getFileSystem().getParent(currentDirectory);
|
||||||
|
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
verifyFileSystemCondition(notNullOrEmpty(path), currentDirectory, "filesystem.parentDirectoryDoesNotExist");
|
||||||
|
verifyFileSystemCondition(getFileSystem().isDirectory(path), path, "filesystem.isNotADirectory");
|
||||||
|
|
||||||
|
// User must have execute permission to the parent directory
|
||||||
|
verifyExecutePermission(session, path);
|
||||||
|
|
||||||
|
session.setAttribute(SessionKeys.CURRENT_DIRECTORY, path);
|
||||||
|
sendReply(session, ReplyCodes.CDUP_OK, "cdup", list(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the CWD command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>If the required pathname parameter is missing, then reply with 501 and terminate</li>
|
||||||
|
* <li>If the pathname parameter does not specify an existing directory, then reply with 550 and terminate</li>
|
||||||
|
* <li>If the current user does not have execute access to the directory, then reply with 550 and terminate</li>
|
||||||
|
* <li>Otherwise, reply with 250 and change the current directory stored in the session</li>
|
||||||
|
* </ol>
|
||||||
|
* The supplied pathname may be absolute or relative to the current directory.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class CwdCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
String path = getRealPath(session, command.getRequiredParameter(0));
|
||||||
|
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
verifyFileSystemCondition(getFileSystem().exists(path), path, "filesystem.doesNotExist");
|
||||||
|
verifyFileSystemCondition(getFileSystem().isDirectory(path), path, "filesystem.isNotADirectory");
|
||||||
|
|
||||||
|
// User must have execute permission to the directory
|
||||||
|
verifyExecutePermission(session, path);
|
||||||
|
|
||||||
|
session.setAttribute(SessionKeys.CURRENT_DIRECTORY, path);
|
||||||
|
sendReply(session, ReplyCodes.CWD_OK, "cwd", list(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the DELE command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>If the required pathname parameter is missing, then reply with 501</li>
|
||||||
|
* <li>If the pathname parameter does not specify an existing file then reply with 550</li>
|
||||||
|
* <li>If the current user does not have write access to the parent directory, then reply with 550</li>
|
||||||
|
* <li>Otherwise, delete the named file and reply with 250</li>
|
||||||
|
* </ol>
|
||||||
|
* The supplied pathname may be absolute or relative to the current directory.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class DeleCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
String path = getRealPath(session, command.getRequiredParameter(0));
|
||||||
|
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
verifyFileSystemCondition(getFileSystem().isFile(path), path, "filesystem.isNotAFile");
|
||||||
|
|
||||||
|
// User must have write permission to the parent directory
|
||||||
|
verifyWritePermission(session, getFileSystem().getParent(path));
|
||||||
|
|
||||||
|
getFileSystem().delete(path);
|
||||||
|
sendReply(session, ReplyCodes.DELE_OK, "dele", list(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2009 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.HostAndPort;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.PortParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the EPRT command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>Parse the client network address (InetAddress) and port number from the (single)
|
||||||
|
* parameter string of the form: "EPRT \net-prt\net-addr\tcp-port\".
|
||||||
|
* The client network address can be in IPv4 format (e.g., "132.235.1.2") or
|
||||||
|
* IPv6 format (e.g., "1080::8:800:200C:417A")
|
||||||
|
* <li>Send back a reply of 200</li>
|
||||||
|
* </ol>
|
||||||
|
* See RFC2428 for more information.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class EprtCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
String parameter = command.getRequiredParameter(0);
|
||||||
|
HostAndPort client = PortParser.parseExtendedAddressHostAndPort(parameter);
|
||||||
|
LOG.log(Level.FINE, "host=" + client.host + " port=" + client.port);
|
||||||
|
session.setClientDataHost(client.host);
|
||||||
|
session.setClientDataPort(client.port);
|
||||||
|
sendReply(session, ReplyCodes.EPRT_OK, "eprt");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2009 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the EPSV command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>Otherwise, request the Session to switch to passive data connection mode. Return a reply code
|
||||||
|
* of 229, along with response text including: "<i>(|||PORT|)</i>", where <i>PORT</i> is the 16-bit
|
||||||
|
* TCP port address of the data connection on the server to which the client must connect.</li>
|
||||||
|
* </ol>
|
||||||
|
* See RFC2428 for more information.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class EpsvCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
int port = session.switchToPassiveMode();
|
||||||
|
InetAddress server = session.getServerHost();
|
||||||
|
LOG.log(Level.FINE, "server=" + server + " port=" + port);
|
||||||
|
sendReply(session, ReplyCodes.EPSV_OK, "epsv", list(Integer.toString(port)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.StringUtil;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import org.xbib.files.ftp.mock.fake.FakeFtpServer;
|
||||||
|
import org.xbib.files.ftp.mock.fake.ServerConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the HELP command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the optional command-name parameter is specified, then reply with 214 along with the
|
||||||
|
* help text configured for that command (or empty if none)</li>
|
||||||
|
* <li>Otherwise, reply with 214 along with the configured default help text that has been configured
|
||||||
|
* (or empty if none)</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>The help text is configured within the {@link FakeFtpServer}.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
* @see ServerConfiguration
|
||||||
|
* @see FakeFtpServer
|
||||||
|
*/
|
||||||
|
public class HelpCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
List parameters = Arrays.asList(command.getParameters());
|
||||||
|
String key = StringUtil.join(parameters, " ");
|
||||||
|
String help = getServerConfiguration().getHelpText(key);
|
||||||
|
if (help == null) {
|
||||||
|
sendReply(session, ReplyCodes.HELP_OK, "help.noHelpTextDefined", list(key));
|
||||||
|
} else {
|
||||||
|
sendReply(session, ReplyCodes.HELP_OK, "help", list(help));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.StringUtil;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the LIST command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530 and terminate</li>
|
||||||
|
* <li>If the current user does not have read access to the file or directory to be listed, then reply with 550 and terminate</li>
|
||||||
|
* <li>If an error occurs during processing, then send a reply of 451 and terminate</li>
|
||||||
|
* <li>Send an initial reply of 150</li>
|
||||||
|
* <li>If the optional pathname parameter is missing, then send a directory listing for
|
||||||
|
* the current directory across the data connection</li>
|
||||||
|
* <li>Otherwise, if the optional pathname parameter specifies a directory or group of files,
|
||||||
|
* then send a directory listing for the specified directory across the data connection</li>
|
||||||
|
* <li>Otherwise, if the optional pathname parameter specifies a filename, then send information
|
||||||
|
* for the specified file across the data connection</li>
|
||||||
|
* <li>Send a final reply with 226</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class ListCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
|
||||||
|
String path = getRealPath(session, command.getParameter(0));
|
||||||
|
|
||||||
|
// User must have read permission to the path
|
||||||
|
if (getFileSystem().exists(path)) {
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
verifyReadPermission(session, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.SYSTEM_ERROR;
|
||||||
|
List fileEntries = getFileSystem().listFiles(path);
|
||||||
|
Iterator iter = fileEntries.iterator();
|
||||||
|
List lines = new ArrayList();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
FileSystemEntry entry = (FileSystemEntry) iter.next();
|
||||||
|
lines.add(getFileSystem().formatDirectoryListing(entry));
|
||||||
|
}
|
||||||
|
String result = StringUtil.join(lines, endOfLine());
|
||||||
|
result += result.length() > 0 ? endOfLine() : "";
|
||||||
|
|
||||||
|
sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);
|
||||||
|
|
||||||
|
session.openDataConnection();
|
||||||
|
LOG.info("Sending [" + result + "]");
|
||||||
|
session.sendData(result.getBytes(), result.length());
|
||||||
|
session.closeDataConnection();
|
||||||
|
|
||||||
|
sendReply(session, ReplyCodes.TRANSFER_DATA_FINAL_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.DirectoryEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the MKD command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>If the required pathname parameter is missing, then reply with 501</li>
|
||||||
|
* <li>If the parent directory of the specified pathname does not exist, then reply with 550</li>
|
||||||
|
* <li>If the pathname parameter specifies an existing file or directory, or if the create directory fails, then reply with 550</li>
|
||||||
|
* <li>If the current user does not have write and execute access to the parent directory, then reply with 550</li>
|
||||||
|
* <li>Otherwise, reply with 257</li>
|
||||||
|
* </ol>
|
||||||
|
* The supplied pathname may be absolute or relative to the current directory.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class MkdCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
String path = getRealPath(session, command.getRequiredParameter(0));
|
||||||
|
String parent = getFileSystem().getParent(path);
|
||||||
|
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
verifyFileSystemCondition(getFileSystem().exists(parent), parent, "filesystem.doesNotExist");
|
||||||
|
verifyFileSystemCondition(!getFileSystem().exists(path), path, "filesystem.alreadyExists");
|
||||||
|
|
||||||
|
// User must have write permission to the parent directory
|
||||||
|
verifyWritePermission(session, parent);
|
||||||
|
|
||||||
|
// User must have execute permission to the parent directory
|
||||||
|
verifyExecutePermission(session, parent);
|
||||||
|
|
||||||
|
DirectoryEntry dirEntry = new DirectoryEntry(path);
|
||||||
|
getFileSystem().add(dirEntry);
|
||||||
|
dirEntry.setPermissions(getUserAccount(session).getDefaultPermissionsForNewDirectory());
|
||||||
|
|
||||||
|
sendReply(session, ReplyCodes.MKD_OK, "mkd", list(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the MODE command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>Otherwise, reply with 200</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class ModeCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
sendReply(session, ReplyCodes.MODE_OK, "mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.StringUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the NLST command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530 and terminate</li>
|
||||||
|
* <li>If the current user does not have read access to the file or directory to be listed, then reply with 550 and terminate</li>
|
||||||
|
* <li>If an error occurs during processing, then send a reply of 451 and terminate</li>
|
||||||
|
* <li>Send an initial reply of 150</li>
|
||||||
|
* <li>If the optional pathname parameter is missing, then send a directory listing for
|
||||||
|
* the current directory across the data connection</li>
|
||||||
|
* <li>Otherwise, if the optional pathname parameter specifies a directory or group of files,
|
||||||
|
* then send a directory listing for the specified directory across the data connection</li>
|
||||||
|
* <li>Otherwise, if the pathname parameter does not specify an existing directory or group of files,
|
||||||
|
* then send an empty response across the data connection</li>
|
||||||
|
* <li>Send a final reply with 226</li>
|
||||||
|
* </ol>
|
||||||
|
* The directory listing sent includes filenames only, separated by end-of-line characters.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class NlstCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
String path = getRealPath(session, command.getParameter(0));
|
||||||
|
|
||||||
|
// User must have read permission to the path
|
||||||
|
if (getFileSystem().exists(path)) {
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
verifyReadPermission(session, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.SYSTEM_ERROR;
|
||||||
|
List names = getFileSystem().listNames(path);
|
||||||
|
String directoryListing = StringUtil.join(names, endOfLine());
|
||||||
|
directoryListing += directoryListing.length() > 0 ? endOfLine() : "";
|
||||||
|
|
||||||
|
sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);
|
||||||
|
|
||||||
|
session.openDataConnection();
|
||||||
|
session.sendData(directoryListing.getBytes(), directoryListing.length());
|
||||||
|
session.closeDataConnection();
|
||||||
|
|
||||||
|
sendReply(session, ReplyCodes.TRANSFER_DATA_FINAL_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the NOOP command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>Reply with 200</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class NoopCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
sendReply(session, ReplyCodes.NOOP_OK, "noop");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
import org.xbib.files.ftp.mock.fake.UserAccount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the PASS command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the required password parameter is missing, then reply with 501</li>
|
||||||
|
* <li>If this command was not preceded by a valid USER command, then reply with 503</li>
|
||||||
|
* <li>If the user account configured for the named user does not exist or is not valid, then reply with 530</li>
|
||||||
|
* <li>If the specified password is not correct, then reply with 530</li>
|
||||||
|
* <li>Otherwise, reply with 230</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class PassCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
String password = command.getRequiredParameter(0);
|
||||||
|
String username = (String) getRequiredSessionAttribute(session, SessionKeys.USERNAME);
|
||||||
|
|
||||||
|
if (validateUserAccount(username, session)) {
|
||||||
|
UserAccount userAccount = getServerConfiguration().getUserAccount(username);
|
||||||
|
if (userAccount.isValidPassword(password)) {
|
||||||
|
int replyCode = (userAccount.isAccountRequiredForLogin()) ? ReplyCodes.PASS_NEED_ACCOUNT : ReplyCodes.PASS_OK;
|
||||||
|
String replyMessageKey = (userAccount.isAccountRequiredForLogin()) ? "pass.needAccount" : "pass";
|
||||||
|
login(userAccount, session, replyCode, replyMessageKey);
|
||||||
|
} else {
|
||||||
|
sendReply(session, ReplyCodes.PASS_LOG_IN_FAILED, "pass.loginFailed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.PortParser;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the PASV command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>Otherwise, request the Session to switch to passive data connection mode. Return a reply code of 227,
|
||||||
|
* along with response text of the form: "<i>(h1,h2,h3,h4,p1,p2)</i>", where <i>h1..h4</i> are the
|
||||||
|
* 4 bytes of the 32-bit internet host address of the server, and <i>p1..p2</i> are the 2
|
||||||
|
* bytes of the 16-bit TCP port address of the data connection on the server to which
|
||||||
|
* the client must connect. See RFC959 for more information.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class PasvCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
|
||||||
|
int port = session.switchToPassiveMode();
|
||||||
|
InetAddress server = session.getServerHost();
|
||||||
|
LOG.log(Level.FINE, "server=" + server + " port=" + port);
|
||||||
|
String hostAndPort = PortParser.convertHostAndPortToCommaDelimitedBytes(server, port);
|
||||||
|
|
||||||
|
sendReply(session, ReplyCodes.PASV_OK, "pasv", list(hostAndPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.HostAndPort;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.PortParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the PORT command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>Parse the client data host (InetAddress) submitted from parameters 1-4
|
||||||
|
* <li>Parse the port number submitted on the invocation from parameter 5-6
|
||||||
|
* <li>Send backa a reply of 200</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
* @see PortParser
|
||||||
|
*/
|
||||||
|
public class PortCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
HostAndPort client = PortParser.parseHostAndPort(command.getParameters());
|
||||||
|
LOG.log(Level.FINE, "host=" + client.host + " port=" + client.port);
|
||||||
|
session.setClientDataHost(client.host);
|
||||||
|
session.setClientDataPort(client.port);
|
||||||
|
sendReply(session, ReplyCodes.PORT_OK, "port");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the PWD command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the required "current directory" property is missing from the session, then reply with 550</li>
|
||||||
|
* <li>Otherwise, reply with 257 and the current directory</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class PwdCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
String currentDirectory = (String) session.getAttribute(SessionKeys.CURRENT_DIRECTORY);
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
verifyFileSystemCondition(notNullOrEmpty(currentDirectory), currentDirectory, "filesystem.currentDirectoryNotSet");
|
||||||
|
sendReply(session, ReplyCodes.PWD_OK, "pwd", list(currentDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the QUIT command. Return a reply code of 221 and close the current session.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class QuitCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
sendReply(session, ReplyCodes.QUIT_OK, "quit");
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the REIN command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>Terminates (logs out) the current user, if there is one</li>
|
||||||
|
* <li>Reply with 220</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class ReinCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
session.removeAttribute(SessionKeys.USER_ACCOUNT);
|
||||||
|
sendReply(session, ReplyCodes.REIN_OK, "rein");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the REST command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>Otherwise, reply with 350</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class RestCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
sendReply(session, ReplyCodes.REST_OK, "rest");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.IoUtil;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemException;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the RETR command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530 and terminate</li>
|
||||||
|
* <li>If the required pathname parameter is missing, then reply with 501 and terminate</li>
|
||||||
|
* <li>If the pathname parameter does not specify a valid, existing filename, then reply with 550 and terminate</li>
|
||||||
|
* <li>If the current user does not have read access to the file at the specified path or execute permission to its directory, then reply with 550 and terminate</li>
|
||||||
|
* <li>Send an initial reply of 150</li>
|
||||||
|
* <li>Send the contents of the named file across the data connection</li>
|
||||||
|
* <li>If there is an error reading the file, then reply with 550 and terminate</li>
|
||||||
|
* <li>Send a final reply with 226</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class RetrCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
|
||||||
|
String path = getRealPath(session, command.getRequiredParameter(0));
|
||||||
|
FileSystemEntry entry = getFileSystem().getEntry(path);
|
||||||
|
verifyFileSystemCondition(entry != null, path, "filesystem.doesNotExist");
|
||||||
|
verifyFileSystemCondition(!entry.isDirectory(), path, "filesystem.isNotAFile");
|
||||||
|
FileEntry fileEntry = (FileEntry) entry;
|
||||||
|
|
||||||
|
// User must have read permission to the file
|
||||||
|
verifyReadPermission(session, path);
|
||||||
|
|
||||||
|
// User must have execute permission to the parent directory
|
||||||
|
verifyExecutePermission(session, getFileSystem().getParent(path));
|
||||||
|
|
||||||
|
sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);
|
||||||
|
InputStream input = fileEntry.createInputStream();
|
||||||
|
session.openDataConnection();
|
||||||
|
byte[] bytes = null;
|
||||||
|
try {
|
||||||
|
bytes = IoUtil.readBytes(input);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error reading from file [" + fileEntry.getPath() + "]", e);
|
||||||
|
throw new FileSystemException(fileEntry.getPath(), null, e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
try {
|
||||||
|
input.close();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Error closing InputStream for file [" + fileEntry.getPath() + "]", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAsciiMode(session)) {
|
||||||
|
bytes = convertLfToCrLf(bytes);
|
||||||
|
}
|
||||||
|
session.sendData(bytes, bytes.length);
|
||||||
|
session.closeDataConnection();
|
||||||
|
sendReply(session, ReplyCodes.TRANSFER_DATA_FINAL_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Within the specified byte array, replace all LF (\n) that are NOT preceded by a CR (\r) into CRLF (\r\n).
|
||||||
|
*
|
||||||
|
* @param bytes - the bytes to be converted
|
||||||
|
* @return the result of converting LF to CRLF
|
||||||
|
*/
|
||||||
|
protected byte[] convertLfToCrLf(byte[] bytes) {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
char lastChar = ' ';
|
||||||
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
|
char ch = (char) bytes[i];
|
||||||
|
if (ch == '\n' && lastChar != '\r') {
|
||||||
|
out.write('\r');
|
||||||
|
out.write('\n');
|
||||||
|
} else {
|
||||||
|
out.write(bytes[i]);
|
||||||
|
}
|
||||||
|
lastChar = ch;
|
||||||
|
}
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAsciiMode(Session session) {
|
||||||
|
// Defaults to true
|
||||||
|
return session.getAttribute(SessionKeys.ASCII_TYPE) != Boolean.FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the RMD command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>If the required pathname parameter is missing, then reply with 501</li>
|
||||||
|
* <li>If the pathname parameter does not specify an existing, empty directory, then reply with 550</li>
|
||||||
|
* <li>If the current user does not have write access to the parent directory, then reply with 550</li>
|
||||||
|
* <li>Otherwise, delete the named directory and reply with 250</li>
|
||||||
|
* </ol>
|
||||||
|
* The supplied pathname may be absolute or relative to the current directory.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class RmdCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
String path = getRealPath(session, command.getRequiredParameter(0));
|
||||||
|
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
verifyFileSystemCondition(getFileSystem().exists(path), path, "filesystem.doesNotExist");
|
||||||
|
verifyFileSystemCondition(getFileSystem().isDirectory(path), path, "filesystem.isNotADirectory");
|
||||||
|
verifyFileSystemCondition(getFileSystem().listNames(path).size() == 0, path, "filesystem.directoryIsNotEmpty");
|
||||||
|
|
||||||
|
// User must have write permission to the parent directory
|
||||||
|
verifyWritePermission(session, getFileSystem().getParent(path));
|
||||||
|
|
||||||
|
getFileSystem().delete(path);
|
||||||
|
sendReply(session, ReplyCodes.RMD_OK, "rmd", list(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the RNFR command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>If the required FROM pathname parameter is missing, then reply with 501</li>
|
||||||
|
* <li>If the FROM pathname parameter does not specify a valid file or directory, then reply with 550</li>
|
||||||
|
* <li>If the current user does not have read access to the path, then reply with 550</li>
|
||||||
|
* <li>Otherwise, reply with 350 and store the FROM path in the session</li>
|
||||||
|
* </ol>
|
||||||
|
* The supplied pathname may be absolute or relative to the current directory.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class RnfrCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
String fromPath = getRealPath(session, command.getRequiredParameter(0));
|
||||||
|
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;
|
||||||
|
verifyFileSystemCondition(getFileSystem().exists(fromPath), fromPath, "filesystem.doesNotExist");
|
||||||
|
|
||||||
|
// User must have read permission to the file
|
||||||
|
verifyReadPermission(session, fromPath);
|
||||||
|
|
||||||
|
session.setAttribute(SessionKeys.RENAME_FROM, fromPath);
|
||||||
|
sendReply(session, ReplyCodes.RNFR_OK, "rnfr");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the RNTO command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>If this command was not preceded by a valid RNFR command, then reply with 503</li>
|
||||||
|
* <li>If the required TO pathname parameter is missing, then reply with 501</li>
|
||||||
|
* <li>If the TO pathname parameter does not specify a valid filename, then reply with 553</li>
|
||||||
|
* <li>If the TO pathname parameter specifies an existing directory, then reply with 553</li>
|
||||||
|
* <li>If the current user does not have write access to the parent directory, then reply with 553</li>
|
||||||
|
* <li>Otherwise, rename the file, remove the FROM path stored in the session by the RNFR command, and reply with 250</li>
|
||||||
|
* </ol>
|
||||||
|
* The supplied pathname may be absolute or relative to the current directory.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class RntoCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
String toPath = getRealPath(session, command.getRequiredParameter(0));
|
||||||
|
String fromPath = (String) getRequiredSessionAttribute(session, SessionKeys.RENAME_FROM);
|
||||||
|
|
||||||
|
this.replyCodeForFileSystemException = ReplyCodes.WRITE_FILE_ERROR;
|
||||||
|
verifyFileSystemCondition(!getFileSystem().isDirectory(toPath), toPath, "filesystem.isDirectory");
|
||||||
|
|
||||||
|
// User must have write permission to the directory
|
||||||
|
String parentPath = getFileSystem().getParent(toPath);
|
||||||
|
verifyFileSystemCondition(notNullOrEmpty(parentPath), parentPath, "filesystem.doesNotExist");
|
||||||
|
verifyFileSystemCondition(getFileSystem().exists(parentPath), parentPath, "filesystem.doesNotExist");
|
||||||
|
verifyWritePermission(session, parentPath);
|
||||||
|
|
||||||
|
getFileSystem().rename(fromPath, toPath);
|
||||||
|
|
||||||
|
session.removeAttribute(SessionKeys.RENAME_FROM);
|
||||||
|
sendReply(session, ReplyCodes.RNTO_OK, "rnto", list(fromPath, toPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the SITE command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>Reply with 200</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class SiteCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
sendReply(session, ReplyCodes.SITE_OK, "site");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
|
||||||
|
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the SIZE command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530 and terminate</li>
|
||||||
|
* <li>If the required pathname parameter is missing, then reply with 501 and terminate</li>
|
||||||
|
* <li>If the pathname parameter does not specify a valid, existing filename, then reply with 550 and terminate</li>
|
||||||
|
* <li>
|
||||||
|
* If the current user does not have read access to the file at the specified path or execute permission
|
||||||
|
* to its directory, then reply with 550 and terminate
|
||||||
|
* </li>
|
||||||
|
* <li>Otherwise, reply with 213</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Edoardo Luppi
|
||||||
|
*/
|
||||||
|
public class SizeCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
@Override
|
||||||
|
protected void handle(final Command command, final Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
|
||||||
|
final String path = getRealPath(session, command.getRequiredParameter(0));
|
||||||
|
final FileSystemEntry entry = getFileSystem().getEntry(path);
|
||||||
|
|
||||||
|
verifyFileSystemCondition(entry != null, path, "filesystem.doesNotExist");
|
||||||
|
verifyFileSystemCondition(!entry.isDirectory(), path, "filesystem.doesNotExist");
|
||||||
|
verifyReadPermission(session, path);
|
||||||
|
verifyExecutePermission(session, getFileSystem().getParent(path));
|
||||||
|
|
||||||
|
final FileEntry fileEntry = (FileEntry) entry;
|
||||||
|
final String size = String.valueOf(fileEntry.getSize());
|
||||||
|
sendReply(session, ReplyCodes.SIZE_OK, "size", Collections.singletonList(size));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the SMNT command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>Otherwise, reply with 250</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class SmntCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
sendReply(session, ReplyCodes.SMNT_OK, "smnt");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.fake.FakeFtpServer;
|
||||||
|
import org.xbib.files.ftp.mock.fake.ServerConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the STAT command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>Reply with 211 along with the system status text that has been configured on the
|
||||||
|
* {@link FakeFtpServer}.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
* @see ServerConfiguration
|
||||||
|
* @see FakeFtpServer
|
||||||
|
*/
|
||||||
|
public class StatCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
String systemStatus = getServerConfiguration().getSystemStatus();
|
||||||
|
sendReply(session, ReplyCodes.STAT_SYSTEM_OK, "stat", list(systemStatus));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the STOR command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530 and terminate</li>
|
||||||
|
* <li>If the required pathname parameter is missing, then reply with 501 and terminate</li>
|
||||||
|
* <li>If the pathname parameter does not specify a valid filename, then reply with 553 and terminate</li>
|
||||||
|
* <li>If the current user does not have write access to the named file, if it already exists, or else to its
|
||||||
|
* parent directory, then reply with 553 and terminate</li>
|
||||||
|
* <li>If the current user does not have execute access to the parent directory, then reply with 553 and terminate</li>
|
||||||
|
* <li>Send an initial reply of 150</li>
|
||||||
|
* <li>Read all available bytes from the data connection and write out to the named file in the server file system</li>
|
||||||
|
* <li>If file write/store fails, then reply with 553 and terminate</li>
|
||||||
|
* <li>Send a final reply with 226</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class StorCommandHandler extends AbstractStoreFileCommandHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the message key for the reply message sent with the final (226) reply
|
||||||
|
*/
|
||||||
|
protected String getMessageKey() {
|
||||||
|
return "stor";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the STOU command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530 and terminate</li>
|
||||||
|
* <li>Create new unique filename within the current directory</li>
|
||||||
|
* <li>If the current user does not have write access to the named file, if it already exists, or else to its
|
||||||
|
* parent directory, then reply with 553 and terminate</li>
|
||||||
|
* <li>If the current user does not have execute access to the parent directory, then reply with 553 and terminate</li>
|
||||||
|
* <li>Send an initial reply of 150</li>
|
||||||
|
* <li>Read all available bytes from the data connection and write out to the unique file in the server file system</li>
|
||||||
|
* <li>If file write/store fails, then reply with 553 and terminate</li>
|
||||||
|
* <li>Send a final reply with 226, along with the new unique filename</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class StouCommandHandler extends AbstractStoreFileCommandHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the message key for the reply message sent with the final (226) reply
|
||||||
|
*/
|
||||||
|
protected String getMessageKey() {
|
||||||
|
return "stou";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the path (absolute or relative) for the output file.
|
||||||
|
*/
|
||||||
|
protected String getOutputFile(Command command) {
|
||||||
|
String baseName = defaultIfNullOrEmpty(command.getOptionalString(0), "Temp");
|
||||||
|
String suffix = Long.toString(System.currentTimeMillis());
|
||||||
|
return baseName + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the STRU command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>Otherwise, reply with 200</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class StruCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
sendReply(session, ReplyCodes.STRU_OK, "stru");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.fake.FakeFtpServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the SYST command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>Reply with 215 along with the system name</li>
|
||||||
|
* </ol>
|
||||||
|
* The default system name is "WINDOWS", but it can be customized on the
|
||||||
|
* {@link FakeFtpServer} .
|
||||||
|
*
|
||||||
|
* <p>See the available system names listed in the Assigned Numbers document (RFC 943).
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
* @see <a href="http://www.ietf.org/rfc/rfc943">RFC943</a>
|
||||||
|
*/
|
||||||
|
public class SystCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
sendReply(session, ReplyCodes.SYST_OK, "syst", list(getServerConfiguration().getSystemName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the TYPE command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the user has not logged in, then reply with 530</li>
|
||||||
|
* <li>Otherwise, reply with 200</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class TypeCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
verifyLoggedIn(session);
|
||||||
|
String type = command.getRequiredParameter(0);
|
||||||
|
boolean asciiType = type == "A";
|
||||||
|
session.setAttribute(SessionKeys.ASCII_TYPE, Boolean.valueOf(asciiType));
|
||||||
|
sendReply(session, ReplyCodes.TYPE_OK, "type");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.command;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.command.Command;
|
||||||
|
import org.xbib.files.ftp.mock.core.command.ReplyCodes;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.Session;
|
||||||
|
import org.xbib.files.ftp.mock.core.session.SessionKeys;
|
||||||
|
import org.xbib.files.ftp.mock.fake.UserAccount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommandHandler for the USER command. Handler logic:
|
||||||
|
* <ol>
|
||||||
|
* <li>If the required pathname parameter is missing, then reply with 501</li>
|
||||||
|
* <li>If the user account configured for the named user is not valid, then reply with 530</li>
|
||||||
|
* <li>If the named user does not need a password for login, then set the UserAccount and
|
||||||
|
* current directory in the session, and reply with 230</li>
|
||||||
|
* <li>Otherwise, set the username in the session and reply with 331</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class UserCommandHandler extends AbstractFakeCommandHandler {
|
||||||
|
|
||||||
|
protected void handle(Command command, Session session) {
|
||||||
|
String username = command.getRequiredParameter(0);
|
||||||
|
UserAccount userAccount = getServerConfiguration().getUserAccount(username);
|
||||||
|
|
||||||
|
if (userAccount != null) {
|
||||||
|
if (!validateUserAccount(username, session)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the UserAccount is configured to not require password for login
|
||||||
|
if (!userAccount.isPasswordRequiredForLogin()) {
|
||||||
|
login(userAccount, session, ReplyCodes.USER_LOGGED_IN_OK, "user.loggedIn");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.setAttribute(SessionKeys.USERNAME, username);
|
||||||
|
sendReply(session, ReplyCodes.USER_NEED_PASSWORD_OK, "user.needPassword");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,677 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.filesystem;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.PatternUtil;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.StringUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import org.xbib.files.ftp.mock.core.util.AssertFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract superclass for implementation of the FileSystem interface that manage the files
|
||||||
|
* and directories in memory, simulating a real file system.
|
||||||
|
*
|
||||||
|
* <p>If the <code>createParentDirectoriesAutomatically</code> property is set to <code>true</code>,
|
||||||
|
* then creating a directory or file will automatically create any parent directories (recursively)
|
||||||
|
* that do not already exist. If <code>false</code>, then creating a directory or file throws an
|
||||||
|
* exception if its parent directory does not exist. This value defaults to <code>true</code>.
|
||||||
|
*
|
||||||
|
* <p>The <code>directoryListingFormatter</code> property holds an instance of {@link DirectoryListingFormatter} ,
|
||||||
|
* used by the <code>formatDirectoryListing</code> method to format directory listings in a
|
||||||
|
* filesystem-specific manner. This property must be initialized by concrete subclasses.
|
||||||
|
*
|
||||||
|
* <p> The <code>systemName</code> property holds the default value returned by this FileSystem for the FTP SYST command.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public abstract class AbstractFakeFileSystem implements FileSystem {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(AbstractFakeFileSystem.class.getName());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If <code>true</code>, creating a directory or file will automatically create
|
||||||
|
* any parent directories (recursively) that do not already exist. If <code>false</code>,
|
||||||
|
* then creating a directory or file throws an exception if its parent directory
|
||||||
|
* does not exist. This value defaults to <code>true</code>.
|
||||||
|
*/
|
||||||
|
private boolean createParentDirectoriesAutomatically = true;
|
||||||
|
|
||||||
|
private String systemName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link DirectoryListingFormatter} used by the {@link #formatDirectoryListing(FileSystemEntry)}
|
||||||
|
* method. This must be initialized by concrete subclasses.
|
||||||
|
*/
|
||||||
|
private DirectoryListingFormatter directoryListingFormatter;
|
||||||
|
|
||||||
|
private Map entries = new ConcurrentHashMap();
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// Public API
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public boolean isCreateParentDirectoriesAutomatically() {
|
||||||
|
return createParentDirectoriesAutomatically;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreateParentDirectoriesAutomatically(boolean createParentDirectoriesAutomatically) {
|
||||||
|
this.createParentDirectoriesAutomatically = createParentDirectoriesAutomatically;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DirectoryListingFormatter getDirectoryListingFormatter() {
|
||||||
|
return directoryListingFormatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDirectoryListingFormatter(DirectoryListingFormatter directoryListingFormatter) {
|
||||||
|
this.directoryListingFormatter = directoryListingFormatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add each of the entries in the specified List to this filesystem. Note that this does not affect
|
||||||
|
* entries already existing within this filesystem.
|
||||||
|
*
|
||||||
|
* @param entriesToAdd - the List of FileSystemEntry entries to add
|
||||||
|
*/
|
||||||
|
public void setEntries(List entriesToAdd) {
|
||||||
|
for (Iterator iter = entriesToAdd.iterator(); iter.hasNext();) {
|
||||||
|
FileSystemEntry entry = (FileSystemEntry) iter.next();
|
||||||
|
add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the specified file system entry (file or directory) to this file system
|
||||||
|
*
|
||||||
|
* @param entry - the FileSystemEntry to add
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void add(FileSystemEntry entry) {
|
||||||
|
String path = entry.getPath();
|
||||||
|
checkForInvalidFilename(path);
|
||||||
|
if (getEntry(path) != null) {
|
||||||
|
throw new FileSystemException(path, "filesystem.pathAlreadyExists");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parentDirectoryExists(path)) {
|
||||||
|
String parent = getParent(path);
|
||||||
|
if (createParentDirectoriesAutomatically) {
|
||||||
|
add(new DirectoryEntry(parent));
|
||||||
|
} else {
|
||||||
|
throw new FileSystemException(parent, "filesystem.parentDirectoryDoesNotExist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set lastModified, if not already set
|
||||||
|
if (entry.getLastModified() == null) {
|
||||||
|
entry.setLastModified(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.put(getFileSystemEntryKey(path), entry);
|
||||||
|
entry.lockPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the file or directory specified by the path. Return true if the file is successfully
|
||||||
|
* deleted, false otherwise. If the path refers to a directory, it must be empty. Return false
|
||||||
|
* if the path does not refer to a valid file or directory or if it is a non-empty directory.
|
||||||
|
*
|
||||||
|
* @param path - the path of the file or directory to delete
|
||||||
|
* @return true if the file or directory is successfully deleted
|
||||||
|
* @throws AssertFailedException
|
||||||
|
* - if path is null
|
||||||
|
* @see FileSystem#delete(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean delete(String path) {
|
||||||
|
Assert.notNull(path, "path");
|
||||||
|
|
||||||
|
if (getEntry(path) != null && !hasChildren(path)) {
|
||||||
|
removeEntry(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if there exists a file or directory at the specified path
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return true if the file/directory exists
|
||||||
|
* @throws AssertionError - if path is null
|
||||||
|
* @see FileSystem#exists(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean exists(String path) {
|
||||||
|
Assert.notNull(path, "path");
|
||||||
|
return getEntry(path) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the specified path designates an existing directory, false otherwise
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return true if path is a directory, false otherwise
|
||||||
|
* @throws AssertionError - if path is null
|
||||||
|
* @see FileSystem#isDirectory(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isDirectory(String path) {
|
||||||
|
Assert.notNull(path, "path");
|
||||||
|
FileSystemEntry entry = getEntry(path);
|
||||||
|
return entry != null && entry.isDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the specified path designates an existing file, false otherwise
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return true if path is a file, false otherwise
|
||||||
|
* @throws AssertionError - if path is null
|
||||||
|
* @see FileSystem#isFile(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isFile(String path) {
|
||||||
|
Assert.notNull(path, "path");
|
||||||
|
FileSystemEntry entry = getEntry(path);
|
||||||
|
return entry != null && !entry.isDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the List of FileSystemEntry objects for the files in the specified directory or group of
|
||||||
|
* files. If the path specifies a single file, then return a list with a single FileSystemEntry
|
||||||
|
* object representing that file. If the path does not refer to an existing directory or
|
||||||
|
* group of files, then an empty List is returned.
|
||||||
|
*
|
||||||
|
* @param path - the path specifying a directory or group of files; may contain wildcards (? or *)
|
||||||
|
* @return the List of FileSystemEntry objects for the specified directory or file; may be empty
|
||||||
|
* @see FileSystem#listFiles(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List listFiles(String path) {
|
||||||
|
if (isFile(path)) {
|
||||||
|
return Collections.singletonList(getEntry(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
List entryList = new ArrayList();
|
||||||
|
List children = children(path);
|
||||||
|
Iterator iter = children.iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
String childPath = (String) iter.next();
|
||||||
|
FileSystemEntry fileSystemEntry = getEntry(childPath);
|
||||||
|
entryList.add(fileSystemEntry);
|
||||||
|
}
|
||||||
|
return entryList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the List of filenames in the specified directory path or file path. If the path specifies
|
||||||
|
* a single file, then return that single filename. The returned filenames do not
|
||||||
|
* include a path. If the path does not refer to a valid directory or file path, then an empty List
|
||||||
|
* is returned.
|
||||||
|
*
|
||||||
|
* @param path - the path specifying a directory or group of files; may contain wildcards (? or *)
|
||||||
|
* @return the List of filenames (not including paths) for all files in the specified directory
|
||||||
|
* or file path; may be empty
|
||||||
|
* @throws AssertionError - if path is null
|
||||||
|
* @see FileSystem#listNames(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List listNames(String path) {
|
||||||
|
if (isFile(path)) {
|
||||||
|
return Collections.singletonList(getName(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
List filenames = new ArrayList();
|
||||||
|
List children = children(path);
|
||||||
|
Iterator iter = children.iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
String childPath = (String) iter.next();
|
||||||
|
FileSystemEntry fileSystemEntry = getEntry(childPath);
|
||||||
|
filenames.add(fileSystemEntry.getName());
|
||||||
|
}
|
||||||
|
return filenames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename the file or directory. Specify the FROM path and the TO path. Throw an exception if the FROM path or
|
||||||
|
* the parent directory of the TO path do not exist; or if the rename fails for another reason.
|
||||||
|
*
|
||||||
|
* @param fromPath - the source (old) path + filename
|
||||||
|
* @param toPath - the target (new) path + filename
|
||||||
|
* @throws AssertionError - if fromPath or toPath is null
|
||||||
|
* @throws FileSystemException - if the rename fails.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void rename(String fromPath, String toPath) {
|
||||||
|
Assert.notNull(toPath, "toPath");
|
||||||
|
Assert.notNull(fromPath, "fromPath");
|
||||||
|
|
||||||
|
FileSystemEntry entry = getRequiredEntry(fromPath);
|
||||||
|
|
||||||
|
if (exists(toPath)) {
|
||||||
|
throw new FileSystemException(toPath, "filesystem.alreadyExists");
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalizedFromPath = normalize(fromPath);
|
||||||
|
String normalizedToPath = normalize(toPath);
|
||||||
|
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
renamePath(entry, normalizedToPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedToPath.startsWith(normalizedFromPath + this.getSeparator())) {
|
||||||
|
throw new FileSystemException(toPath, "filesystem.renameFailed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the TO directory entry first so that the destination path exists when you
|
||||||
|
// move the children. Remove the FROM path after all children have been moved
|
||||||
|
add(new DirectoryEntry(normalizedToPath));
|
||||||
|
|
||||||
|
List children = descendants(fromPath);
|
||||||
|
Collections.sort(children); // ensure that parents are listed before children
|
||||||
|
Iterator iter = children.iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
String childPath = (String) iter.next();
|
||||||
|
FileSystemEntry child = getRequiredEntry(childPath);
|
||||||
|
String normalizedChildPath = normalize(child.getPath());
|
||||||
|
Assert.isTrue(normalizedChildPath.startsWith(normalizedFromPath), "Starts with FROM path");
|
||||||
|
String childToPath = normalizedToPath + normalizedChildPath.substring(normalizedFromPath.length());
|
||||||
|
renamePath(child, childToPath);
|
||||||
|
}
|
||||||
|
Assert.isTrue(children(normalizedFromPath).isEmpty(), "Must have no children: " + normalizedFromPath);
|
||||||
|
removeEntry(normalizedFromPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.getClass().getName() + entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the formatted directory listing entry for the file represented by the specified FileSystemEntry
|
||||||
|
*
|
||||||
|
* @param fileSystemEntry - the FileSystemEntry representing the file or directory entry to be formatted
|
||||||
|
* @return the the formatted directory listing entry
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String formatDirectoryListing(FileSystemEntry fileSystemEntry) {
|
||||||
|
Assert.notNull(directoryListingFormatter, "directoryListingFormatter");
|
||||||
|
Assert.notNull(fileSystemEntry, "fileSystemEntry");
|
||||||
|
return directoryListingFormatter.format(fileSystemEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a path from the two path components. Concatenate path1 and path2. Insert the path
|
||||||
|
* separator character in between if necessary (i.e., if both are non-empty and path1 does not already
|
||||||
|
* end with a separator character AND path2 does not begin with one).
|
||||||
|
*
|
||||||
|
* @param path1 - the first path component may be null or empty
|
||||||
|
* @param path2 - the second path component may be null or empty
|
||||||
|
* @return the normalized path resulting from concatenating path1 to path2
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String path(String path1, String path2) {
|
||||||
|
StringBuffer buf = new StringBuffer();
|
||||||
|
if (path1 != null && path1.length() > 0) {
|
||||||
|
buf.append(path1);
|
||||||
|
}
|
||||||
|
if (path2 != null && path2.length() > 0) {
|
||||||
|
if ((path1 != null && path1.length() > 0)
|
||||||
|
&& (!isSeparator(path1.charAt(path1.length() - 1)))
|
||||||
|
&& (!isSeparator(path2.charAt(0)))) {
|
||||||
|
buf.append(this.getSeparator());
|
||||||
|
}
|
||||||
|
buf.append(path2);
|
||||||
|
}
|
||||||
|
return normalize(buf.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the parent path of the specified path. If <code>path</code> specifies a filename,
|
||||||
|
* then this method returns the path of the directory containing that file. If <code>path</code>
|
||||||
|
* specifies a directory, the this method returns its parent directory. If <code>path</code> is
|
||||||
|
* empty or does not have a parent component, then return an empty string.
|
||||||
|
*
|
||||||
|
* <p>All path separators in the returned path are converted to the system-dependent separator character.
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return the parent of the specified path, or null if <code>path</code> has no parent
|
||||||
|
* @throws AssertionError - if path is null
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getParent(String path) {
|
||||||
|
List parts = normalizedComponents(path);
|
||||||
|
if (parts.size() < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
parts.remove(parts.size() - 1);
|
||||||
|
return componentsToPath(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the file or directory denoted by this abstract
|
||||||
|
* pathname. This is just the last name in the pathname's name
|
||||||
|
* sequence. If the pathname's name sequence is empty, then the empty string is returned.
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return The name of the file or directory denoted by this abstract pathname, or the
|
||||||
|
* empty string if this pathname's name sequence is empty
|
||||||
|
*/
|
||||||
|
public String getName(String path) {
|
||||||
|
Assert.notNull(path, "path");
|
||||||
|
String normalized = normalize(path);
|
||||||
|
int separatorIndex = normalized.lastIndexOf(this.getSeparator());
|
||||||
|
return (separatorIndex == -1) ? normalized : normalized.substring(separatorIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the FileSystemEntry object representing the file system entry at the specified path, or null
|
||||||
|
* if the path does not specify an existing file or directory within this file system.
|
||||||
|
*
|
||||||
|
* @param path - the path of the file or directory within this file system
|
||||||
|
* @return the FileSystemEntry containing the information for the file or directory, or else null
|
||||||
|
* @see FileSystem#getEntry(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public FileSystemEntry getEntry(String path) {
|
||||||
|
return (FileSystemEntry) entries.get(getFileSystemEntryKey(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSystemName() {
|
||||||
|
return systemName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSystemName(String systemName) {
|
||||||
|
this.systemName = systemName;
|
||||||
|
}
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// Abstract Methods
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param path - the path
|
||||||
|
* @return true if the specified dir/file path name is valid according to the current filesystem.
|
||||||
|
*/
|
||||||
|
protected abstract boolean isValidName(String path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the file system-specific file separator as a char
|
||||||
|
*/
|
||||||
|
protected abstract char getSeparatorChar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pathComponent - the component (piece) of the path to check
|
||||||
|
* @return true if the specified path component is a root for this filesystem
|
||||||
|
*/
|
||||||
|
protected abstract boolean isRoot(String pathComponent);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the specified char is a separator character for this filesystem
|
||||||
|
*
|
||||||
|
* @param c - the character to test
|
||||||
|
* @return true if the specified char is a separator character
|
||||||
|
*/
|
||||||
|
protected abstract boolean isSeparator(char c);
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// Internal Helper Methods
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the file system-specific file separator as a String
|
||||||
|
*/
|
||||||
|
protected String getSeparator() {
|
||||||
|
return Character.toString(getSeparatorChar());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the normalized and unique key used to access the file system entry
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return the corresponding normalized key
|
||||||
|
*/
|
||||||
|
protected String getFileSystemEntryKey(String path) {
|
||||||
|
return normalize(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the standard, normalized form of the path.
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return the path in a standard, unique, canonical form
|
||||||
|
* @throws AssertionError - if path is null
|
||||||
|
*/
|
||||||
|
protected String normalize(String path) {
|
||||||
|
return componentsToPath(normalizedComponents(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw an InvalidFilenameException if the specified path is not valid.
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
*/
|
||||||
|
protected void checkForInvalidFilename(String path) {
|
||||||
|
if (!isValidName(path)) {
|
||||||
|
throw new InvalidFilenameException(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename the file system entry to the specified path name
|
||||||
|
*
|
||||||
|
* @param entry - the file system entry
|
||||||
|
* @param toPath - the TO path (normalized)
|
||||||
|
*/
|
||||||
|
protected void renamePath(FileSystemEntry entry, String toPath) {
|
||||||
|
String normalizedFrom = normalize(entry.getPath());
|
||||||
|
String normalizedTo = normalize(toPath);
|
||||||
|
LOG.info("renaming from [" + normalizedFrom + "] to [" + normalizedTo + "]");
|
||||||
|
FileSystemEntry newEntry = entry.cloneWithNewPath(normalizedTo);
|
||||||
|
add(newEntry);
|
||||||
|
// Do this at the end, in case the addEntry() failed
|
||||||
|
removeEntry(normalizedFrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the FileSystemEntry for the specified path. Throw FileSystemException if the
|
||||||
|
* specified path does not exist.
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return the FileSystemEntry
|
||||||
|
* @throws FileSystemException - if the specified path does not exist
|
||||||
|
*/
|
||||||
|
protected FileSystemEntry getRequiredEntry(String path) {
|
||||||
|
FileSystemEntry entry = getEntry(path);
|
||||||
|
if (entry == null) {
|
||||||
|
LOG.log(Level.SEVERE, "Path does not exist: " + path);
|
||||||
|
throw new FileSystemException(normalize(path), "filesystem.doesNotExist");
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the components of the specified path as a List. The components are normalized, and
|
||||||
|
* the returned List does not include path separator characters.
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return the List of normalized components
|
||||||
|
*/
|
||||||
|
protected List normalizedComponents(String path) {
|
||||||
|
Assert.notNull(path, "path");
|
||||||
|
char otherSeparator = this.getSeparatorChar() == '/' ? '\\' : '/';
|
||||||
|
String p = path.replace(otherSeparator, this.getSeparatorChar());
|
||||||
|
|
||||||
|
// TODO better way to do this
|
||||||
|
if (p.equals(this.getSeparator())) {
|
||||||
|
return Collections.singletonList("");
|
||||||
|
}
|
||||||
|
List result = new ArrayList();
|
||||||
|
if (p.length() > 0) {
|
||||||
|
String[] parts = p.split("\\" + this.getSeparator());
|
||||||
|
for (int i = 0; i < parts.length; i++) {
|
||||||
|
String part = parts[i];
|
||||||
|
if (part.equals("..")) {
|
||||||
|
result.remove(result.size() - 1);
|
||||||
|
} else if (!part.equals(".")) {
|
||||||
|
result.add(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a path from the specified list of path components
|
||||||
|
*
|
||||||
|
* @param components - the list of path components
|
||||||
|
* @return the resulting path
|
||||||
|
*/
|
||||||
|
protected String componentsToPath(List components) {
|
||||||
|
if (components.size() == 1) {
|
||||||
|
String first = (String) components.get(0);
|
||||||
|
if (first.length() == 0 || isRoot(first)) {
|
||||||
|
return first + this.getSeparator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StringUtil.join(components, this.getSeparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the specified path designates an absolute file path.
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return true if path is absolute, false otherwise
|
||||||
|
* @throws AssertionError - if path is null
|
||||||
|
*/
|
||||||
|
public boolean isAbsolute(String path) {
|
||||||
|
return isValidName(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the specified path exists
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return true if the path exists
|
||||||
|
*/
|
||||||
|
private boolean pathExists(String path) {
|
||||||
|
return getEntry(path) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the specified path has a parent, then verify that the parent exists
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return true if the parent of the specified path exists
|
||||||
|
*/
|
||||||
|
private boolean parentDirectoryExists(String path) {
|
||||||
|
String parent = getParent(path);
|
||||||
|
return parent == null || pathExists(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the specified path represents a directory that contains one or more files or subdirectories
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return true if the path has child entries
|
||||||
|
*/
|
||||||
|
private boolean hasChildren(String path) {
|
||||||
|
if (!isDirectory(path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String key = getFileSystemEntryKey(path);
|
||||||
|
Iterator iter = entries.keySet().iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
String p = (String) iter.next();
|
||||||
|
if (p.startsWith(key) && !key.equals(p)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the List of files or subdirectory paths that are descendants of the specified path
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return the List of the paths for the files and subdirectories that are children, grandchildren, etc.
|
||||||
|
*/
|
||||||
|
private List descendants(String path) {
|
||||||
|
if (isDirectory(path)) {
|
||||||
|
String normalizedPath = getFileSystemEntryKey(path);
|
||||||
|
String separator = (normalizedPath.endsWith(getSeparator())) ? "" : getSeparator();
|
||||||
|
String normalizedDirPrefix = normalizedPath + separator;
|
||||||
|
List descendants = new ArrayList();
|
||||||
|
Iterator iter = entries.entrySet().iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
Map.Entry mapEntry = (Map.Entry) iter.next();
|
||||||
|
String p = (String) mapEntry.getKey();
|
||||||
|
if (p.startsWith(normalizedDirPrefix) && !normalizedPath.equals(p)) {
|
||||||
|
FileSystemEntry fileSystemEntry = (FileSystemEntry) mapEntry.getValue();
|
||||||
|
descendants.add(fileSystemEntry.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return descendants;
|
||||||
|
}
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the List of files or subdirectory paths that are children of the specified path
|
||||||
|
*
|
||||||
|
* @param path - the path
|
||||||
|
* @return the List of the paths for the files and subdirectories that are children
|
||||||
|
*/
|
||||||
|
private List children(String path) {
|
||||||
|
String lastComponent = getName(path);
|
||||||
|
boolean containsWildcards = PatternUtil.containsWildcards(lastComponent);
|
||||||
|
String dir = containsWildcards ? getParent(path) : path;
|
||||||
|
String pattern = containsWildcards ? PatternUtil.convertStringWithWildcardsToRegex(getName(path)) : null;
|
||||||
|
LOG.log(Level.FINE, "path=" + path + " lastComponent=" + lastComponent + " containsWildcards=" + containsWildcards + " dir=" + dir + " pattern=" + pattern);
|
||||||
|
|
||||||
|
List descendents = descendants(dir);
|
||||||
|
List children = new ArrayList();
|
||||||
|
String normalizedDir = normalize(dir);
|
||||||
|
Iterator iter = descendents.iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
String descendentPath = (String) iter.next();
|
||||||
|
|
||||||
|
boolean patternEmpty = pattern == null || pattern.length() == 0;
|
||||||
|
if (normalizedDir.equals(getParent(descendentPath)) &&
|
||||||
|
(patternEmpty || (getName(descendentPath).matches(pattern)))) {
|
||||||
|
children.add(descendentPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeEntry(String path) {
|
||||||
|
entries.remove(getFileSystemEntryKey(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.filesystem;
|
||||||
|
|
||||||
|
import org.xbib.files.ftp.mock.core.util.Assert;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The abstract superclass for concrete file system entry classes representing files and directories.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public abstract class AbstractFileSystemEntry implements FileSystemEntry {
|
||||||
|
|
||||||
|
private String path;
|
||||||
|
private boolean pathLocked = false;
|
||||||
|
|
||||||
|
private Date lastModified;
|
||||||
|
private String owner;
|
||||||
|
private String group;
|
||||||
|
|
||||||
|
public Date getLastModified() {
|
||||||
|
return lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastModified(Date lastModified) {
|
||||||
|
this.lastModified = lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOwner() {
|
||||||
|
return owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwner(String owner) {
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGroup() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroup(String group) {
|
||||||
|
this.group = group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Permissions getPermissions() {
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPermissions(Permissions permissions) {
|
||||||
|
this.permissions = permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Permissions permissions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance without setting its path
|
||||||
|
*/
|
||||||
|
public AbstractFileSystemEntry() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the specified value for its path
|
||||||
|
*
|
||||||
|
* @param path - the value for path
|
||||||
|
*/
|
||||||
|
public AbstractFileSystemEntry(String path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the path for this entry
|
||||||
|
*/
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the file name or directory name (no path) for this entry
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
int separatorIndex1 = path.lastIndexOf('/');
|
||||||
|
int separatorIndex2 = path.lastIndexOf('\\');
|
||||||
|
// int separatorIndex = [separatorIndex1, separatorIndex2].max();
|
||||||
|
int separatorIndex = separatorIndex1 > separatorIndex2 ? separatorIndex1 : separatorIndex2;
|
||||||
|
return (separatorIndex == -1) ? path : path.substring(separatorIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the path for this entry. Throw an exception if pathLocked is true.
|
||||||
|
*
|
||||||
|
* @param path - the new path value
|
||||||
|
*/
|
||||||
|
public void setPath(String path) {
|
||||||
|
Assert.isFalse(pathLocked, "path is locked");
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void lockPath() {
|
||||||
|
this.pathLocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPermissionsFromString(String permissionsString) {
|
||||||
|
this.permissions = new Permissions(permissionsString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract method -- must be implemented within concrete subclasses
|
||||||
|
*
|
||||||
|
* @return true if this file system entry represents a directory
|
||||||
|
*/
|
||||||
|
public abstract boolean isDirectory();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.xbib.files.ftp.mock.fake.filesystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File system entry representing a directory
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public class DirectoryEntry extends AbstractFileSystemEntry {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance without setting its path
|
||||||
|
*/
|
||||||
|
public DirectoryEntry() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the specified value for its path
|
||||||
|
*
|
||||||
|
* @param path - the value for path
|
||||||
|
*/
|
||||||
|
public DirectoryEntry(String path) {
|
||||||
|
super(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true to indicate that this entry represents a directory
|
||||||
|
*
|
||||||
|
* @return true
|
||||||
|
*/
|
||||||
|
public boolean isDirectory() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the size of this directory. This method returns zero.
|
||||||
|
*
|
||||||
|
* @return the file size in bytes
|
||||||
|
*/
|
||||||
|
public long getSize() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Object#toString()
|
||||||
|
*/
|
||||||
|
public String toString() {
|
||||||
|
return "Directory['" + getPath() + "' lastModified=" + getLastModified() + " owner=" + getOwner() +
|
||||||
|
" group=" + getGroup() + " permissions=" + getPermissions() + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new FileSystemEntry that is a clone of this object, except having the specified path
|
||||||
|
*
|
||||||
|
* @param path - the new path value for the cloned file system entry
|
||||||
|
* @return a new FileSystemEntry that has all the same values as this object except for its path
|
||||||
|
*/
|
||||||
|
public FileSystemEntry cloneWithNewPath(String path) {
|
||||||
|
DirectoryEntry clone = new DirectoryEntry(path);
|
||||||
|
clone.setLastModified(getLastModified());
|
||||||
|
clone.setOwner(getOwner());
|
||||||
|
clone.setGroup(getGroup());
|
||||||
|
clone.setPermissions(getPermissions());
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2008 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.xbib.files.ftp.mock.fake.filesystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for an object that can format a file system directory listing.
|
||||||
|
*
|
||||||
|
* @author Chris Mair
|
||||||
|
*/
|
||||||
|
public interface DirectoryListingFormatter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the directory listing for a single file/directory entry.
|
||||||
|
*
|
||||||
|
* @param fileSystemEntry - the FileSystemEntry for a single file system entry
|
||||||
|
* @return the formatted directory listing
|
||||||
|
*/
|
||||||
|
String format(FileSystemEntry fileSystemEntry);
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue