integrate our own copy of mock ftp server for JPMS

This commit is contained in:
Jörg Prante 2024-05-01 22:52:19 +02:00
parent 651417ce00
commit 0d0fbe167b
246 changed files with 18589 additions and 359 deletions

View file

@ -10,7 +10,10 @@ import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
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.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.UserPrincipal;
import java.time.Instant;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
@ -31,9 +35,7 @@ import java.util.stream.Stream;
public class DefaultFileService implements FileService {
private static final int READ_BUFFER_SIZE = 128 * 1024;
private static final int WRITE_BUFFER_SIZE = 128 * 1024;
private static final int BUFFER_SIZE = 128 * 1024;
private static final Set<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
PosixFilePermissions.fromString("rwxr-xr-x");
@ -110,23 +112,61 @@ public class DefaultFileService implements FileService {
}
@Override
public void setOwner(String path, UserPrincipal userPrincipal) throws IOException {
Files.setOwner(Paths.get(path), userPrincipal);
public void setPosixFileAttributes(String path,
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
public UserPrincipal getOwner(String path) throws IOException {
return Files.getOwner(Paths.get(path));
public PosixFileAttributes getPosixFileAttributes(String path) throws IOException {
return Files.getFileAttributeView(FileSystems.getDefault().getPath(path),
PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes();
}
@Override
public void setLastModifiedTime(String path, FileTime fileTime) throws IOException {
Files.setLastModifiedTime(Paths.get(path), fileTime);
public void setOwner(String path, String owner) throws IOException {
Files.setOwner(Paths.get(path),
FileSystems.getDefault().getUserPrincipalLookupService().lookupPrincipalByName(owner));
}
@Override
public FileTime getLastModifiedTime(String path) throws IOException {
return Files.getLastModifiedTime(Paths.get(path));
public String getOwner(String path) throws IOException {
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
@ -145,43 +185,87 @@ public class DefaultFileService implements FileService {
}
@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);
}
@Override
public void upload(Path source, String target, CopyOption... copyOptions) throws IOException {
upload(source, Paths.get(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 {
upload(Files.newByteChannel(source), Paths.get(target), dirPerms, filePerms, copyOptions);
}
@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);
}
@Override
public void upload(InputStream source, String target, CopyOption... copyOptions) throws IOException {
upload(source, Paths.get(target), DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
public void upload(Path source,
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
public void download(Path source, Path target, CopyOption... copyOptions) throws IOException {
download(source, target, READ_BUFFER_SIZE, copyOptions);
download(source, target, copyOptions);
}
@Override
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
public void download(Path source, OutputStream target) throws IOException {
download(source, target, READ_BUFFER_SIZE);
download(source, target);
}
@Override
public void download(String source, OutputStream target) throws IOException {
download(Paths.get(source), target, READ_BUFFER_SIZE);
download(Paths.get(source), target);
}
@Override
@ -224,28 +308,13 @@ public class DefaultFileService implements FileService {
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,
Path target,
int bufferSize,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws IOException {
prepareForWrite(target, dirPerms, filePerms);
transfer(source, Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
transfer(source, Files.newByteChannel(target, prepareWriteOptions(copyOptions)));
}
private void download(Path source,
@ -257,8 +326,7 @@ public class DefaultFileService implements FileService {
private void download(Path source,
WritableByteChannel writableByteChannel,
int bufferSize) throws IOException {
transfer(Files.newByteChannel(source, prepareReadOptions()), writableByteChannel,
bufferSize);
transfer(Files.newByteChannel(source, prepareReadOptions()), writableByteChannel);
}
private void download(Path source,
@ -267,7 +335,7 @@ public class DefaultFileService implements FileService {
CopyOption... copyOptions) throws IOException {
prepareForWrite(target);
transfer(Files.newByteChannel(source, prepareReadOptions(copyOptions)),
Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize);
Files.newByteChannel(target, prepareWriteOptions(copyOptions)));
}
private void prepareForWrite(Path path) throws IOException {
@ -328,9 +396,8 @@ public class DefaultFileService implements FileService {
}
private void transfer(ReadableByteChannel readableByteChannel,
WritableByteChannel writableByteChannel,
int bufferSize) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
WritableByteChannel writableByteChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
int read;
while ((read = readableByteChannel.read(buffer)) > 0) {
buffer.flip();

View file

@ -10,9 +10,9 @@ import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
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.UserPrincipal;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@ -72,13 +72,26 @@ public interface FileService {
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;
@ -86,21 +99,59 @@ public interface FileService {
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;

View file

@ -1,11 +1,11 @@
dependencies {
api project(':files-api')
api project(':files-ftp')
testImplementation testLibs.mockftpserver
testImplementation testLibs.junit.jupiter.params
testImplementation testLibs.mockito.core
testImplementation testLibs.mockito.junit.jupiter
testImplementation testLibs.slf4j
testImplementation project(':files-ftp-mock')
}
def moduleName = 'org.xbib.io.ftp.test'

View file

@ -1,7 +1,7 @@
import java.nio.file.spi.FileSystemProvider;
import org.xbib.files.FileServiceProvider;
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 {
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.spi;
provides FileSystemProvider with FTPFileSystemProvider;
provides FileServiceProvider with FTPFilesProvider;
provides FileServiceProvider with FTPFileServiceProvider;
}

View file

@ -1,9 +1,5 @@
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.InputStream;
import java.io.OutputStream;
@ -16,6 +12,7 @@ import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
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.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.UserPrincipal;
import java.time.Instant;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
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 WRITE_BUFFER_SIZE = 128 * 1024;
private static final int BUFFER_SIZE = 128 * 1024;
private static final Set<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
PosixFilePermissions.fromString("rwxr-xr-x");
@ -47,7 +46,7 @@ public class FTPFiles implements FileService {
private final Map<String, ?> env;
public FTPFiles(URI uri, Map<String, ?> env) {
public FTPFileService(URI uri, Map<String, ?> env) {
this.uri = uri;
this.env = env;
}
@ -133,30 +132,176 @@ public class FTPFiles implements FileService {
}
@Override
public void setLastModifiedTime(String path, FileTime fileTime) throws IOException {
performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime));
public void setLastModifiedTime(String path, Instant lastModified) throws IOException {
performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), FileTime.from(lastModified)));
}
@Override
public FileTime getLastModifiedTime(String path) throws IOException{
return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)));
public Instant getLastModifiedTime(String path) throws IOException{
return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)).toInstant());
}
@Override
public void setOwner(String path, UserPrincipal userPrincipal) throws IOException {
performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal));
public void setPosixFileAttributes(String path,
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
public UserPrincipal getOwner(String path) throws IOException {
return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path)));
public PosixFileAttributes getPosixFileAttributes(String path) throws IOException {
return performWithContext(ctx -> Files.getFileAttributeView(ctx.fileSystem.getPath(path),
PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes());
}
@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);
}
@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
public DirectoryStream<Path> stream(String path, String glob) throws IOException {
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);
}
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, 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;
});
}
@Override
public void copy(String source, String target, CopyOption... copyOptions) throws IOException {
performWithContext(ctx -> {
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 {
performWithContext(ctx -> {
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 {
performWithContext(ctx -> {
Files.deleteIfExists(ctx.fileSystem.getPath(source));
@ -295,37 +359,32 @@ public class FTPFiles implements FileService {
private void upload(FTPContext ctx,
ReadableByteChannel source,
Path target,
int bufferSize,
Set<PosixFilePermission> dirPerms,
Set<PosixFilePermission> filePerms,
CopyOption... copyOptions) throws IOException {
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,
Path source,
OutputStream outputStream,
int bufferSize) throws IOException {
download(ctx, source, Channels.newChannel(outputStream), bufferSize);
OutputStream outputStream) throws IOException {
download(ctx, source, Channels.newChannel(outputStream));
}
private void download(FTPContext ctx,
Path source,
WritableByteChannel writableByteChannel,
int bufferSize) throws IOException {
transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel,
bufferSize);
WritableByteChannel writableByteChannel) throws IOException {
transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel);
}
private void download(FTPContext ctx,
Path source,
Path target,
int bufferSize,
CopyOption... copyOptions) throws IOException {
prepareForWrite(target);
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 {
@ -386,9 +445,8 @@ public class FTPFiles implements FileService {
}
private void transfer(ReadableByteChannel readableByteChannel,
WritableByteChannel writableByteChannel,
int bufferSize) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
WritableByteChannel writableByteChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
int read;
while ((read = readableByteChannel.read(buffer)) > 0) {
buffer.flip();

View file

@ -6,9 +6,10 @@ import org.xbib.files.FileServiceProvider;
import java.net.URI;
import java.util.Map;
public class FTPFilesProvider implements FileServiceProvider {
public class FTPFileServiceProvider implements FileServiceProvider {
@Override
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;
}
}

View file

@ -1 +1 @@
org.xbib.io.ftp.fs.spi.FTPFilesProvider
org.xbib.io.ftp.fs.spi.FTPFileServiceProvider

View file

@ -3,10 +3,11 @@ module org.xbib.files.ftp.fs.test {
requires org.junit.jupiter.api;
requires org.junit.jupiter.params;
requires org.mockito;
requires MockFtpServer;
requires org.slf4j;
requires org.xbib.files.ftp;
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.server;
opens org.xbib.io.ftp.fs.test to org.junit.platform.commons;

View file

@ -4,14 +4,14 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
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.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.FTPEnvironment;
import org.xbib.io.ftp.fs.FTPFileSystem;

View file

@ -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.assertEquals;
import org.junit.jupiter.api.Test;
import org.mockftpserver.fake.filesystem.FileEntry;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
public class FTPFileSystemInputStreamTest extends AbstractFTPFileSystemTest {

View file

@ -2,10 +2,10 @@ package org.xbib.io.ftp.fs.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.mockftpserver.fake.filesystem.FileEntry;
import java.io.IOException;
import java.io.OutputStream;
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
public class FTPFileSystemOutputStreamTest extends AbstractFTPFileSystemTest {

View file

@ -2,7 +2,6 @@ package org.xbib.io.ftp.fs.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockftpserver.fake.filesystem.FileEntry;
import java.io.IOException;
import java.io.OutputStream;
@ -18,6 +17,7 @@ import java.nio.file.attribute.PosixFileAttributeView;
import java.util.HashMap;
import java.util.Map;
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.FTPFileSystemProvider;
import org.xbib.io.ftp.fs.FTPPath;

View file

@ -2,10 +2,10 @@ package org.xbib.io.ftp.fs.test;
import org.junit.jupiter.api.Assertions;
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.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.fs.FTPFileSystem;
import org.xbib.io.ftp.fs.FTPFileSystemException;

View file

@ -1,7 +1,7 @@
package org.xbib.io.ftp.fs.test.server;
import org.mockftpserver.fake.filesystem.FileSystemEntry;
import org.mockftpserver.fake.filesystem.UnixDirectoryListingFormatter;
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
import org.xbib.files.ftp.mock.fake.filesystem.UnixDirectoryListingFormatter;
/**
* An extended version of {@link UnixDirectoryListingFormatter} that supports symbolic links.

View file

@ -1,9 +1,10 @@
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 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.

View file

@ -1,16 +1,16 @@
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.Iterator;
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.
@ -69,7 +69,7 @@ public class ListHiddenFilesCommandHandler extends ListCommandHandler {
sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);
session.openDataConnection();
LOG.info("Sending [" + result + "]");
LOG.log(Level.INFO, "Sending [" + result + "]");
session.sendData(result.getBytes(), result.length());
session.closeDataConnection();

View file

@ -1,14 +1,14 @@
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.util.Date;
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.

View file

@ -1,9 +1,8 @@
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 org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
import org.xbib.files.ftp.mock.fake.filesystem.Permissions;
/**
* A representation of symbolic links.

View file

@ -0,0 +1,5 @@
dependencies {
testImplementation testLibs.junit.jupiter.params
testImplementation project(':files-ftp')
testImplementation testLibs.mockito.core
}

View 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;
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}

View file

@ -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() {
}
}

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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 + "]";
}
}

View file

@ -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() {
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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";
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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() {
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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() {
}
}

View file

@ -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() {
}
}

View file

@ -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() {
}
}

View file

@ -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() {
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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));
}
}

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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));
}
}

View file

@ -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");
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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");
}
}

View file

@ -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)));
}
}

View file

@ -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));
}
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -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");
}
}
}
}

View file

@ -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));
}
}

View file

@ -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");
}
}

View file

@ -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));
}
}

View file

@ -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();
}
}

View file

@ -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");
}
}

View file

@ -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");
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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");
}
}

View file

@ -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));
}
}

View file

@ -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");
}
}

View file

@ -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));
}
}

View file

@ -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");
}
}

View file

@ -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));
}
}

View file

@ -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";
}
}

View file

@ -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;
}
}

View file

@ -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");
}
}

View file

@ -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()));
}
}

View file

@ -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");
}
}

View file

@ -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");
}
}

View file

@ -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));
}
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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